From 715df11f82d550f70a70457627a3e384d1b80803 Mon Sep 17 00:00:00 2001 From: giles Date: Wed, 4 Mar 2026 15:27:41 +0000 Subject: [PATCH] Phase 8-9: Convert events + sx layouts, add missing JS primitives Events (Phase 8): - Create events/sx/layouts.sx with 18 defcomps for all 9 layout pairs - Convert all layout functions to render_to_sx_with_env + _ctx_to_env - Convert 5 render functions to eliminate root_header_sx calls - Zero root_header_sx references remain in events SX Docs (Phase 9): - Create sx/sx/layouts.sx with layout defcomps - Convert 4 layout functions to render_to_sx_with_env + _ctx_to_env JS primitives: - Add slice, replace, upper, lower, trim, escape, strip-tags, split, join, pluralize, clamp, parse-int, format-decimal, format-date, parse-datetime, split-ids, starts-with?, ends-with?, dissoc, into - Fix contains? for strings (indexOf instead of in operator) - Prevents "Undefined symbol" errors when .sx expressions using server-side primitives are evaluated client-side Co-Authored-By: Claude Opus 4.6 --- events/sx/layouts.sx | 112 +++++++++++++ events/sxc/pages/__init__.py | 303 ++++++++++++++++++++--------------- shared/static/scripts/sx.js | 52 ++++++ sx/sx/layouts.sx | 21 +++ sx/sxc/pages/__init__.py | 28 ++-- 5 files changed, 373 insertions(+), 143 deletions(-) create mode 100644 events/sx/layouts.sx create mode 100644 sx/sx/layouts.sx diff --git a/events/sx/layouts.sx b/events/sx/layouts.sx new file mode 100644 index 0000000..2ba7825 --- /dev/null +++ b/events/sx/layouts.sx @@ -0,0 +1,112 @@ +;; Events layout defcomps — root header from env free variables, +;; events-specific headers passed as &key params. + +;; --- Calendar admin layout: root + post + child(admin + cal + cal-admin) --- + +(defcomp ~events-cal-admin-layout-full (&key post-header admin-header + calendar-header calendar-admin-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + post-header + (~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header)))) + +(defcomp ~events-cal-admin-layout-oob (&key admin-oob cal-oob cal-admin-oob-wrap clear-oob) + (<> admin-oob cal-oob cal-admin-oob-wrap clear-oob)) + +;; --- Slots layout: same full as cal-admin --- + +(defcomp ~events-slots-layout-oob (&key admin-oob cal-admin-oob clear-oob) + (<> admin-oob cal-admin-oob clear-oob)) + +;; --- Slot detail layout: root + post + child(admin + cal + cal-admin + slot) --- + +(defcomp ~events-slot-layout-full (&key post-header admin-header + calendar-header calendar-admin-header slot-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + post-header + (~header-child-sx :inner (<> admin-header calendar-header calendar-admin-header slot-header)))) + +(defcomp ~events-slot-layout-oob (&key admin-oob cal-admin-oob slot-oob-wrap clear-oob) + (<> admin-oob cal-admin-oob slot-oob-wrap clear-oob)) + +;; --- Day admin layout: root + post + child(admin + cal + day + day-admin) --- + +(defcomp ~events-day-admin-layout-full (&key post-header admin-header + calendar-header day-header day-admin-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + post-header + (~header-child-sx :inner (<> admin-header calendar-header day-header day-admin-header)))) + +(defcomp ~events-day-admin-layout-oob (&key admin-oob cal-oob day-admin-oob-wrap clear-oob) + (<> admin-oob cal-oob day-admin-oob-wrap clear-oob)) + +;; --- Entry layout: root + child(post + cal + day + entry) --- + +(defcomp ~events-entry-layout-full (&key post-header calendar-header day-header entry-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + (~header-child-sx :inner (<> post-header calendar-header day-header entry-header)))) + +(defcomp ~events-entry-layout-oob (&key day-oob entry-oob-wrap clear-oob) + (<> day-oob entry-oob-wrap clear-oob)) + +;; --- Entry admin layout: root + post + child(admin + cal + day + entry + entry-admin) --- + +(defcomp ~events-entry-admin-layout-full (&key post-header admin-header + calendar-header day-header + entry-header entry-admin-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + post-header + (~header-child-sx :inner (<> admin-header calendar-header day-header + entry-header entry-admin-header)))) + +(defcomp ~events-entry-admin-layout-oob (&key admin-oob entry-oob entry-admin-oob-wrap clear-oob) + (<> admin-oob entry-oob entry-admin-oob-wrap clear-oob)) + +;; --- Ticket types layout: root + child(post + cal + day + entry + entry-admin + ticket-types) --- + +(defcomp ~events-ticket-types-layout-full (&key post-header calendar-header day-header + entry-header entry-admin-header + ticket-types-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + (~header-child-sx :inner (<> post-header calendar-header day-header + entry-header entry-admin-header ticket-types-header)))) + +(defcomp ~events-ticket-types-layout-oob (&key entry-admin-oob ticket-types-oob-wrap) + (<> entry-admin-oob ticket-types-oob-wrap)) + +;; --- Ticket type detail layout: root + child(post + cal + day + entry + entry-admin + types + type) --- + +(defcomp ~events-ticket-type-layout-full (&key post-header calendar-header day-header + entry-header entry-admin-header + ticket-types-header ticket-type-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + (~header-child-sx :inner (<> post-header calendar-header day-header + entry-header entry-admin-header + ticket-types-header ticket-type-header)))) + +(defcomp ~events-ticket-type-layout-oob (&key ticket-types-oob ticket-type-oob-wrap) + (<> ticket-types-oob ticket-type-oob-wrap)) + +;; --- Markets layout: root + child(post + markets) --- + +(defcomp ~events-markets-layout-full (&key post-header markets-header) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + (~header-child-sx :inner (<> post-header markets-header)))) + +(defcomp ~events-markets-layout-oob (&key post-oob markets-oob-wrap) + (<> post-oob markets-oob-wrap)) diff --git a/events/sxc/pages/__init__.py b/events/sxc/pages/__init__.py index 3aac88f..8b85f33 100644 --- a/events/sxc/pages/__init__.py +++ b/events/sxc/pages/__init__.py @@ -7,7 +7,8 @@ from markupsafe import escape from shared.sx.parser import SxExpr from shared.sx.helpers import ( call_url, get_asset_url, render_to_sx, - root_header_sx, post_header_sx, post_admin_header_sx, + render_to_sx_with_env, _ctx_to_env, + 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, @@ -1226,7 +1227,7 @@ 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 = await root_header_sx(ctx) + hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) return await full_page_sx(ctx, header_rows=hdr, content=content) @@ -1287,7 +1288,7 @@ async def render_page_summary_page(ctx: dict, entries, has_more, pending_tickets is_page_scoped=True, post=post, ) - hdr = await root_header_sx(ctx) + hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) hdr += await header_child_sx(await _post_header_sx(ctx)) return await full_page_sx(ctx, header_rows=hdr, content=content) @@ -1342,7 +1343,7 @@ async def render_calendars_page(ctx: dict) -> str: content = await _calendars_main_panel_sx(ctx) ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = await root_header_sx(ctx) + root_hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) post_hdr = await _post_header_sx(ctx) admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars") return await full_page_sx(ctx, header_rows=root_hdr + post_hdr + admin_hdr, content=content) @@ -1366,7 +1367,7 @@ async def render_calendars_oob(ctx: dict) -> str: async def render_calendar_page(ctx: dict) -> str: """Full page: calendar month view.""" content = await _calendar_main_panel_html(ctx) - hdr = await root_header_sx(ctx) + hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) child = await _post_header_sx(ctx) + await _calendar_header_sx(ctx) hdr += await header_child_sx(child) return await full_page_sx(ctx, header_rows=hdr, content=content) @@ -1390,7 +1391,7 @@ async def render_calendar_oob(ctx: dict) -> str: async def render_day_page(ctx: dict) -> str: """Full page: day detail.""" content = await _day_main_panel_html(ctx) - hdr = await root_header_sx(ctx) + hdr = await render_to_sx_with_env("layout-root-full", _ctx_to_env(ctx)) child = (await _post_header_sx(ctx) + await _calendar_header_sx(ctx) + await _day_header_sx(ctx)) hdr += await header_child_sx(child) @@ -3227,29 +3228,33 @@ def _register_events_layouts() -> None: # --- Calendar admin layout (root + post + child(post-admin + calendar + cal-admin)) --- async def _cal_admin_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = await root_header_sx(ctx) - post_hdr = await _post_header_sx(ctx) - admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars") - child = admin_hdr + await _calendar_header_sx(ctx) + await _calendar_admin_header_sx(ctx) - return root_hdr + post_hdr + await header_child_sx(child) + return await render_to_sx_with_env("events-cal-admin-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")), + calendar_header=SxExpr(await _calendar_header_sx(ctx)), + calendar_admin_header=SxExpr(await _calendar_admin_header_sx(ctx)), + ) async def _cal_admin_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import post_admin_header_sx, oob_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars") - + await _calendar_header_sx(ctx, oob=True)) - oobs += await oob_header_sx("calendar-header-child", "calendar-admin-header-child", - await _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 oobs + return await render_to_sx_with_env("events-cal-admin-layout-oob", _ctx_to_env(ctx, oob=True), + admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")), + cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)), + cal_admin_oob_wrap=SxExpr(await oob_header_sx("calendar-header-child", + "calendar-admin-header-child", await _calendar_admin_header_sx(ctx))), + clear_oob=SxExpr(_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")), + ) # --- Slots layout (same full as cal-admin but different OOB) --- @@ -3259,185 +3264,221 @@ async def _slots_full(ctx: dict, **kw: Any) -> str: async def _slots_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import post_admin_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav({**ctx, "is_admin_section": True}) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars") - + await _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 oobs + return await render_to_sx_with_env("events-slots-layout-oob", _ctx_to_env(ctx, oob=True), + admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")), + cal_admin_oob=SxExpr(await _calendar_admin_header_sx(ctx, oob=True)), + clear_oob=SxExpr(_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 detail layout (extends cal-admin with slot header) --- async def _slot_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav({**ctx, "is_admin_section": True}) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = await root_header_sx(ctx) - post_hdr = await _post_header_sx(ctx) - admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars") - child = (admin_hdr + await _calendar_header_sx(ctx) - + await _calendar_admin_header_sx(ctx) + await _slot_header_html(ctx)) - return root_hdr + post_hdr + await header_child_sx(child) + return await render_to_sx_with_env("events-slot-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")), + calendar_header=SxExpr(await _calendar_header_sx(ctx)), + calendar_admin_header=SxExpr(await _calendar_admin_header_sx(ctx)), + slot_header=SxExpr(await _slot_header_html(ctx)), + ) async def _slot_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import post_admin_header_sx, oob_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav({**ctx, "is_admin_section": True}) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars") - + await _calendar_admin_header_sx(ctx, oob=True)) - oobs += await oob_header_sx("calendar-admin-header-child", "slot-header-child", - await _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 oobs + return await render_to_sx_with_env("events-slot-layout-oob", _ctx_to_env(ctx, oob=True), + admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")), + cal_admin_oob=SxExpr(await _calendar_admin_header_sx(ctx, oob=True)), + slot_oob_wrap=SxExpr(await oob_header_sx("calendar-admin-header-child", + "slot-header-child", await _slot_header_html(ctx))), + clear_oob=SxExpr(_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")), + ) # --- Day admin layout (root + post + post-admin + child(cal + day + day-admin)) --- async def _day_admin_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = await root_header_sx(ctx) - post_hdr = await _post_header_sx(ctx) - admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars") - child = (admin_hdr + await _calendar_header_sx(ctx) + await _day_header_sx(ctx) - + await _day_admin_header_sx(ctx)) - return root_hdr + post_hdr + await header_child_sx(child) + return await render_to_sx_with_env("events-day-admin-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")), + calendar_header=SxExpr(await _calendar_header_sx(ctx)), + day_header=SxExpr(await _day_header_sx(ctx)), + day_admin_header=SxExpr(await _day_admin_header_sx(ctx)), + ) async def _day_admin_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import post_admin_header_sx, oob_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars") - + await _calendar_header_sx(ctx, oob=True)) - oobs += await oob_header_sx("day-header-child", "day-admin-header-child", - await _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 oobs + return await render_to_sx_with_env("events-day-admin-layout-oob", _ctx_to_env(ctx, oob=True), + admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")), + cal_oob=SxExpr(await _calendar_header_sx(ctx, oob=True)), + day_admin_oob_wrap=SxExpr(await oob_header_sx("day-header-child", + "day-admin-header-child", await _day_admin_header_sx(ctx))), + clear_oob=SxExpr(_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")), + ) # --- Entry layout (root + child(post + cal + day + entry), + menu) --- async def _entry_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, header_child_sx - root_hdr = await root_header_sx(ctx) - child = (await _post_header_sx(ctx) + await _calendar_header_sx(ctx) - + await _day_header_sx(ctx) + await _entry_header_html(ctx)) - return root_hdr + await header_child_sx(child) + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-entry-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + calendar_header=SxExpr(await _calendar_header_sx(ctx)), + day_header=SxExpr(await _day_header_sx(ctx)), + entry_header=SxExpr(await _entry_header_html(ctx)), + ) async def _entry_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import oob_header_sx - oobs = await _day_header_sx(ctx, oob=True) - oobs += await oob_header_sx("day-header-child", "entry-header-child", - await _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") - return oobs + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-entry-layout-oob", _ctx_to_env(ctx, oob=True), + day_oob=SxExpr(await _day_header_sx(ctx, oob=True)), + entry_oob_wrap=SxExpr(await oob_header_sx("day-header-child", + "entry-header-child", await _entry_header_html(ctx))), + clear_oob=SxExpr(_clear_deeper_oob("post-row", "post-header-child", + "calendar-row", "calendar-header-child", + "day-row", "day-header-child", + "entry-row", "entry-header-child")), + ) # --- Entry admin layout (root + post + child(post-admin + cal + day + entry + entry-admin), + menu) --- async def _entry_admin_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, post_admin_header_sx, header_child_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - root_hdr = await root_header_sx(ctx) - post_hdr = await _post_header_sx(ctx) - admin_hdr = await post_admin_header_sx(ctx, slug, selected="calendars") - child = (admin_hdr + await _calendar_header_sx(ctx) + await _day_header_sx(ctx) - + await _entry_header_html(ctx) + await _entry_admin_header_html(ctx)) - return root_hdr + post_hdr + await header_child_sx(child) + return await render_to_sx_with_env("events-entry-admin-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + admin_header=SxExpr(await post_admin_header_sx(ctx, slug, selected="calendars")), + calendar_header=SxExpr(await _calendar_header_sx(ctx)), + day_header=SxExpr(await _day_header_sx(ctx)), + entry_header=SxExpr(await _entry_header_html(ctx)), + entry_admin_header=SxExpr(await _entry_admin_header_html(ctx)), + ) async def _entry_admin_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import post_admin_header_sx, oob_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, post_admin_header_sx, oob_header_sx + from shared.sx.parser import SxExpr ctx = await _ensure_container_nav(ctx) slug = (ctx.get("post") or {}).get("slug", "") - oobs = (await post_admin_header_sx(ctx, slug, oob=True, selected="calendars") - + await _entry_header_html(ctx, oob=True)) - oobs += await oob_header_sx("entry-header-child", "entry-admin-header-child", - await _entry_admin_header_html(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", - "entry-row", "entry-header-child", - "entry-admin-row", "entry-admin-header-child") - return oobs + return await render_to_sx_with_env("events-entry-admin-layout-oob", _ctx_to_env(ctx, oob=True), + admin_oob=SxExpr(await post_admin_header_sx(ctx, slug, oob=True, selected="calendars")), + entry_oob=SxExpr(await _entry_header_html(ctx, oob=True)), + entry_admin_oob_wrap=SxExpr(await oob_header_sx("entry-header-child", + "entry-admin-header-child", await _entry_admin_header_html(ctx))), + clear_oob=SxExpr(_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", + "entry-row", "entry-header-child", + "entry-admin-row", "entry-admin-header-child")), + ) # --- Ticket types layout (extends entry admin with ticket-types header, + menu) --- async def _ticket_types_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, header_child_sx - root_hdr = await root_header_sx(ctx) - child = (await _post_header_sx(ctx) + await _calendar_header_sx(ctx) - + await _day_header_sx(ctx) + await _entry_header_html(ctx) - + await _entry_admin_header_html(ctx) + await _ticket_types_header_html(ctx)) - return root_hdr + await header_child_sx(child) + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-ticket-types-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + calendar_header=SxExpr(await _calendar_header_sx(ctx)), + day_header=SxExpr(await _day_header_sx(ctx)), + entry_header=SxExpr(await _entry_header_html(ctx)), + entry_admin_header=SxExpr(await _entry_admin_header_html(ctx)), + ticket_types_header=SxExpr(await _ticket_types_header_html(ctx)), + ) async def _ticket_types_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import oob_header_sx - oobs = await _entry_admin_header_html(ctx, oob=True) - oobs += await oob_header_sx("entry-admin-header-child", "ticket_types-header-child", - await _ticket_types_header_html(ctx)) - return oobs + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-ticket-types-layout-oob", _ctx_to_env(ctx, oob=True), + entry_admin_oob=SxExpr(await _entry_admin_header_html(ctx, oob=True)), + ticket_types_oob_wrap=SxExpr(await oob_header_sx("entry-admin-header-child", + "ticket_types-header-child", await _ticket_types_header_html(ctx))), + ) # --- Ticket type detail layout (extends ticket types with ticket-type header, + menu) --- async def _ticket_type_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, header_child_sx - root_hdr = await root_header_sx(ctx) - child = (await _post_header_sx(ctx) + await _calendar_header_sx(ctx) - + await _day_header_sx(ctx) + await _entry_header_html(ctx) - + await _entry_admin_header_html(ctx) + await _ticket_types_header_html(ctx) - + await _ticket_type_header_html(ctx)) - return root_hdr + await header_child_sx(child) + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-ticket-type-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + calendar_header=SxExpr(await _calendar_header_sx(ctx)), + day_header=SxExpr(await _day_header_sx(ctx)), + entry_header=SxExpr(await _entry_header_html(ctx)), + entry_admin_header=SxExpr(await _entry_admin_header_html(ctx)), + ticket_types_header=SxExpr(await _ticket_types_header_html(ctx)), + ticket_type_header=SxExpr(await _ticket_type_header_html(ctx)), + ) async def _ticket_type_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import oob_header_sx - oobs = await _ticket_types_header_html(ctx, oob=True) - oobs += await oob_header_sx("ticket_types-header-child", "ticket_type-header-child", - await _ticket_type_header_html(ctx)) - return oobs + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-ticket-type-layout-oob", _ctx_to_env(ctx, oob=True), + ticket_types_oob=SxExpr(await _ticket_types_header_html(ctx, oob=True)), + ticket_type_oob_wrap=SxExpr(await oob_header_sx("ticket_types-header-child", + "ticket_type-header-child", await _ticket_type_header_html(ctx))), + ) # --- Markets layout (root + child(post + markets)) --- async def _markets_full(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import root_header_sx, header_child_sx - root_hdr = await root_header_sx(ctx) - child = await _post_header_sx(ctx) + await _markets_header_sx(ctx) - return root_hdr + await header_child_sx(child) + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-markets-layout-full", _ctx_to_env(ctx), + post_header=SxExpr(await _post_header_sx(ctx)), + markets_header=SxExpr(await _markets_header_sx(ctx)), + ) async def _markets_oob(ctx: dict, **kw: Any) -> str: - from shared.sx.helpers import oob_header_sx - oobs = await _post_header_sx(ctx, oob=True) - oobs += await oob_header_sx("post-header-child", "markets-header-child", - await _markets_header_sx(ctx)) - return oobs + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx + from shared.sx.parser import SxExpr + return await render_to_sx_with_env("events-markets-layout-oob", _ctx_to_env(ctx, oob=True), + post_oob=SxExpr(await _post_header_sx(ctx, oob=True)), + markets_oob_wrap=SxExpr(await oob_header_sx("post-header-child", + "markets-header-child", await _markets_header_sx(ctx))), + ) # --------------------------------------------------------------------------- diff --git a/shared/static/scripts/sx.js b/shared/static/scripts/sx.js index f92f056..f3c1bd6 100644 --- a/shared/static/scripts/sx.js +++ b/shared/static/scripts/sx.js @@ -320,6 +320,7 @@ PRIMITIVES["last"] = function (c) { return c && c.length > 0 ? c[c.length - 1] : NIL; }; PRIMITIVES["rest"] = function (c) { return c ? c.slice(1) : []; }; PRIMITIVES["nth"] = function (c, n) { return c && n < c.length ? c[n] : NIL; }; + PRIMITIVES["slice"] = function (c, start, end) { return c ? (end !== undefined && end !== NIL ? c.slice(start, end) : c.slice(start)) : c; }; PRIMITIVES["cons"] = function (x, c) { return [x].concat(c || []); }; PRIMITIVES["append"] = function (c, x) { return (c || []).concat([x]); }; PRIMITIVES["keys"] = function (d) { return Object.keys(d || {}); }; @@ -340,6 +341,57 @@ for (var i = a; step > 0 ? i < b : i > b; i += step) r.push(i); return r; }; + PRIMITIVES["dissoc"] = function (d) { + var out = {}; for (var k in d) out[k] = d[k]; + for (var i = 1; i < arguments.length; i++) delete out[arguments[i]]; + return out; + }; + PRIMITIVES["into"] = function (target, src) { + if (Array.isArray(target)) return target.concat(src || []); + var out = {}; for (var k in target) out[k] = target[k]; for (var k2 in src) out[k2] = src[k2]; return out; + }; + + // String operations + PRIMITIVES["replace"] = function (s, from, to) { return s ? String(s).split(from).join(to) : ""; }; + PRIMITIVES["upper"] = function (s) { return s ? String(s).toUpperCase() : ""; }; + PRIMITIVES["lower"] = function (s) { return s ? String(s).toLowerCase() : ""; }; + PRIMITIVES["trim"] = function (s) { return s ? String(s).trim() : ""; }; + PRIMITIVES["starts-with?"] = function (s, pfx) { return s ? String(s).indexOf(pfx) === 0 : false; }; + PRIMITIVES["ends-with?"] = function (s, sfx) { var str = String(s || ""); return str.indexOf(sfx, str.length - sfx.length) !== -1; }; + PRIMITIVES["escape"] = function (s) { + if (!s) return ""; + return String(s).replace(/&/g, "&").replace(//g, ">").replace(/"/g, """); + }; + PRIMITIVES["strip-tags"] = function (s) { return s ? String(s).replace(/<[^>]*>/g, "") : ""; }; + PRIMITIVES["split"] = function (s, sep) { return s ? String(s).split(sep) : []; }; + PRIMITIVES["join"] = function (lst, sep) { return (lst || []).join(sep !== undefined ? sep : ""); }; + PRIMITIVES["pluralize"] = function (n, singular, plural) { return n === 1 ? singular : (plural || singular + "s"); }; + + // Numeric + PRIMITIVES["clamp"] = function (val, lo, hi) { return Math.max(lo, Math.min(hi, val)); }; + PRIMITIVES["parse-int"] = function (s, def) { var n = parseInt(s, 10); return isNaN(n) ? (def !== undefined ? def : 0) : n; }; + PRIMITIVES["format-decimal"] = function (n, places) { return Number(n || 0).toFixed(places !== undefined ? places : 2); }; + + // Date formatting (basic) + PRIMITIVES["format-date"] = function (s, fmt) { + if (!s) return ""; + try { + var d = new Date(s); + if (isNaN(d.getTime())) return String(s); + // Basic strftime-like: %Y %m %d %H %M %B %b %-d + var months = ["January","February","March","April","May","June","July","August","September","October","November","December"]; + var short_months = ["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"]; + return fmt.replace(/%-d/g, d.getDate()).replace(/%d/g, ("0"+d.getDate()).slice(-2)) + .replace(/%B/g, months[d.getMonth()]).replace(/%b/g, short_months[d.getMonth()]) + .replace(/%Y/g, d.getFullYear()).replace(/%m/g, ("0"+(d.getMonth()+1)).slice(-2)) + .replace(/%H/g, ("0"+d.getHours()).slice(-2)).replace(/%M/g, ("0"+d.getMinutes()).slice(-2)); + } catch (e) { return String(s); } + }; + PRIMITIVES["parse-datetime"] = function (s) { return s ? String(s) : NIL; }; + PRIMITIVES["split-ids"] = function (s) { + if (!s) return []; + return String(s).split(",").map(function(x) { return x.trim(); }).filter(function(x) { return x; }); + }; // --- Evaluator --- diff --git a/sx/sx/layouts.sx b/sx/sx/layouts.sx new file mode 100644 index 0000000..0a32026 --- /dev/null +++ b/sx/sx/layouts.sx @@ -0,0 +1,21 @@ +;; SX docs layout defcomps — root header from env free variables, +;; sx-specific headers passed as &key params. + +;; --- SX home layout: root + sx menu row --- + +(defcomp ~sx-layout-full (&key sx-row) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + sx-row)) + +(defcomp ~sx-layout-oob (&key root-header sx-row) + (<> root-header sx-row)) + +;; --- SX section layout: root + sx row (with child sub-row) --- + +(defcomp ~sx-section-layout-full (&key sx-row) + (<> (~root-header :cart-mini cart-mini :blog-url blog-url :site-title site-title + :app-label app-label :nav-tree nav-tree :auth-menu auth-menu + :nav-panel nav-panel :settings-url settings-url :is-admin is-admin) + sx-row)) diff --git a/sx/sxc/pages/__init__.py b/sx/sxc/pages/__init__.py index 1380c13..10f6256 100644 --- a/sx/sxc/pages/__init__.py +++ b/sx/sxc/pages/__init__.py @@ -3061,28 +3061,31 @@ def _register_sx_layouts() -> None: async def _sx_full_headers(ctx: dict, **kw: Any) -> str: """Full headers for sx home page: root + sx menu row.""" - from shared.sx.helpers import root_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env + from shared.sx.parser import SxExpr main_nav = await _main_nav_sx(kw.get("section")) - root_hdr = await root_header_sx(ctx) sx_row = await _sx_header_sx(main_nav) - return "(<> " + root_hdr + " " + sx_row + ")" + return await render_to_sx_with_env("sx-layout-full", _ctx_to_env(ctx), + sx_row=SxExpr(sx_row)) async def _sx_oob_headers(ctx: dict, **kw: Any) -> str: """OOB headers for sx home page.""" - from shared.sx.helpers import root_header_sx, oob_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx + from shared.sx.parser import SxExpr - root_hdr = await root_header_sx(ctx) main_nav = await _main_nav_sx(kw.get("section")) sx_row = await _sx_header_sx(main_nav) - rows = "(<> " + root_hdr + " " + sx_row + ")" + rows = await render_to_sx_with_env("sx-layout-full", _ctx_to_env(ctx), + sx_row=SxExpr(sx_row)) return await oob_header_sx("root-header-child", "sx-header-child", rows) async def _sx_section_full_headers(ctx: dict, **kw: Any) -> str: """Full headers for sx section pages: root + sx row + sub row.""" - from shared.sx.helpers import root_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env + from shared.sx.parser import SxExpr section = kw.get("section", "") sub_label = kw.get("sub_label", section) @@ -3090,16 +3093,17 @@ async def _sx_section_full_headers(ctx: dict, **kw: Any) -> str: sub_nav = kw.get("sub_nav", "") selected = kw.get("selected", "") - root_hdr = await root_header_sx(ctx) main_nav = await _main_nav_sx(section) sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected) sx_row = await _sx_header_sx(main_nav, child=sub_row) - return "(<> " + root_hdr + " " + sx_row + ")" + return await render_to_sx_with_env("sx-section-layout-full", _ctx_to_env(ctx), + sx_row=SxExpr(sx_row)) async def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str: """OOB headers for sx section pages.""" - from shared.sx.helpers import root_header_sx, oob_header_sx + from shared.sx.helpers import render_to_sx_with_env, _ctx_to_env, oob_header_sx + from shared.sx.parser import SxExpr section = kw.get("section", "") sub_label = kw.get("sub_label", section) @@ -3107,11 +3111,11 @@ async def _sx_section_oob_headers(ctx: dict, **kw: Any) -> str: sub_nav = kw.get("sub_nav", "") selected = kw.get("selected", "") - root_hdr = await root_header_sx(ctx) main_nav = await _main_nav_sx(section) sub_row = await _sub_row_sx(sub_label, sub_href, sub_nav, selected) sx_row = await _sx_header_sx(main_nav, child=sub_row) - rows = "(<> " + root_hdr + " " + sx_row + ")" + rows = await render_to_sx_with_env("sx-section-layout-full", _ctx_to_env(ctx), + sx_row=SxExpr(sx_row)) return await oob_header_sx("root-header-child", "sx-header-child", rows)