diff --git a/events/sx/sx_components.py b/events/sx/sx_components.py index 2a34e3e..ce1e7d8 100644 --- a/events/sx/sx_components.py +++ b/events/sx/sx_components.py @@ -791,7 +791,7 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str: type_name=tt.name if tt else None, time_str=time_str or None, cal_name=cal.name if cal else None, - badge=_ticket_state_badge_html(state), + badge=SxExpr(_ticket_state_badge_html(state)), code_prefix=ticket.code[:8])) cards_html = "".join(ticket_cards) @@ -904,7 +904,7 @@ def _ticket_admin_main_panel_html(ctx: dict, tickets: list, stats: dict) -> str: entry_name=entry.name if entry else "\u2014", date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", - badge=_ticket_state_badge_html(state), + badge=SxExpr(_ticket_state_badge_html(state)), action=SxExpr(action_html)) return sx_call("events-ticket-admin-panel", @@ -973,7 +973,7 @@ def _entry_card_html(entry, page_info: dict, pending_tickets: dict, if tp is not None: qty = pending_tickets.get(entry.id, 0) widget_html = sx_call("events-entry-widget-wrapper", - widget=_ticket_widget_html(entry, qty, ticket_url, ctx={})) + widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={}))) return sx_call("events-entry-card", title=SxExpr(title_html), badges=SxExpr(badges_html), @@ -1036,7 +1036,7 @@ def _entry_card_tile_html(entry, page_info: dict, pending_tickets: dict, if tp is not None: qty = pending_tickets.get(entry.id, 0) widget_html = sx_call("events-entry-tile-widget-wrapper", - widget=_ticket_widget_html(entry, qty, ticket_url, ctx={})) + widget=SxExpr(_ticket_widget_html(entry, qty, ticket_url, ctx={}))) return sx_call("events-entry-card-tile", title=SxExpr(title_html), badges=SxExpr(badges_html), @@ -1154,7 +1154,7 @@ def _view_toggle_html(ctx: dict, view: str) -> str: list_href=list_href, tile_href=tile_href, hx_select=hx_select, list_cls=list_active, tile_cls=tile_active, storage_key="events_view", - list_svg=_get_list_svg(), tile_svg=_get_tile_svg()) + list_svg=SxExpr(_get_list_svg()), tile_svg=SxExpr(_get_tile_svg())) def _events_main_panel_html(ctx: dict, entries, has_more, pending_tickets, page_info, @@ -1619,7 +1619,7 @@ def render_checkin_result(success: bool, error: str | None, ticket) -> str: entry_name=entry.name if entry else "\u2014", date=SxExpr(date_html), type_name=tt.name if tt else "\u2014", - badge=_ticket_state_badge_html("checked_in"), + badge=SxExpr(_ticket_state_badge_html("checked_in")), time_str=time_str) @@ -1656,7 +1656,7 @@ def render_lookup_result(ticket, error: str | None) -> str: if cal: info_html += sx_call("events-lookup-cal", cal_name=cal.name) info_html += sx_call("events-lookup-status", - badge=_ticket_state_badge_html(state), code=code) + badge=SxExpr(_ticket_state_badge_html(state)), code=code) if checked_in_at: info_html += sx_call("events-lookup-checkin-time", date_str=checked_in_at.strftime("%B %d, %Y at %H:%M")) @@ -1709,7 +1709,7 @@ def render_entry_tickets_admin(entry, tickets: list) -> str: rows_html += sx_call("events-entry-tickets-admin-row", code=code, code_short=code[:12] + "...", type_name=tt.name if tt else "\u2014", - badge=_ticket_state_badge_html(state), + badge=SxExpr(_ticket_state_badge_html(state)), action=SxExpr(action_html)) if tickets: @@ -1783,7 +1783,7 @@ def _entry_main_panel_html(ctx: dict) -> str: # State state_html = _field("State", sx_call("events-entry-state-field", entry_id=str(eid), - badge=_entry_state_badge_html(state))) + badge=SxExpr(_entry_state_badge_html(state)))) # Cost cost = getattr(entry, "cost", None) @@ -1794,7 +1794,7 @@ def _entry_main_panel_html(ctx: dict) -> str: # Ticket Configuration (admin) tickets_html = _field("Tickets", sx_call("events-entry-tickets-field", entry_id=str(eid), - tickets_config=render_entry_tickets_config(entry, calendar, day, month, year))) + tickets_config=SxExpr(render_entry_tickets_config(entry, calendar, day, month, year)))) # Buy Tickets (public-facing) ticket_remaining = ctx.get("ticket_remaining") @@ -1814,7 +1814,7 @@ def _entry_main_panel_html(ctx: dict) -> str: entry_posts = ctx.get("entry_posts") or [] posts_html = _field("Associated Posts", sx_call("events-entry-posts-field", entry_id=str(eid), - posts_panel=render_entry_posts_panel(entry_posts, entry, calendar, day, month, year))) + posts_panel=SxExpr(render_entry_posts_panel(entry_posts, entry, calendar, day, month, year)))) # Options and Edit Button edit_url = url_for( @@ -1830,7 +1830,7 @@ def _entry_main_panel_html(ctx: dict) -> str: cost=SxExpr(cost_html), tickets=SxExpr(tickets_html), buy=SxExpr(buy_html), date=SxExpr(date_html), posts=SxExpr(posts_html), - options=_entry_options_html(entry, calendar, day, month, year), + options=SxExpr(_entry_options_html(entry, calendar, day, month, year)), pre_action=pre_action, edit_url=edit_url) @@ -1861,8 +1861,8 @@ def _entry_header_html(ctx: dict, *, oob: bool = False) -> str: ) label_html = sx_call("events-entry-label", entry_id=str(entry.id), - title=_entry_title_html(entry), - times=_entry_times_html(entry)) + title=SxExpr(_entry_title_html(entry)), + times=SxExpr(_entry_times_html(entry))) nav_html = _entry_nav_html(ctx) @@ -1988,7 +1988,7 @@ def _entry_title_html(entry) -> str: state = getattr(entry, "state", "pending") or "pending" return sx_call("events-entry-title", name=entry.name, - badge=_entry_state_badge_html(state)) + badge=SxExpr(_entry_state_badge_html(state))) def _entry_options_html(entry, calendar, day, month, year) -> str: @@ -2578,13 +2578,13 @@ def render_buy_form(entry, ticket_remaining, ticket_sold_count, cost_str = f"\u00a3{tt.cost:.2f}" if tt.cost is not None else "\u00a30.00" type_items += sx_call("events-buy-type-item", type_name=tt.name, cost_str=cost_str, - adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id)) + adjust_controls=SxExpr(_ticket_adjust_controls(csrf, adjust_url, target, eid, type_count, ticket_type_id=tt.id))) body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items)) else: qty = user_ticket_count or 0 body_html = sx_call("events-buy-default", price_str=f"\u00a3{tp:.2f}", - adjust_controls=_ticket_adjust_controls(csrf, adjust_url, target, eid, qty)) + adjust_controls=SxExpr(_ticket_adjust_controls(csrf, adjust_url, target, eid, qty))) return sx_call("events-buy-panel", entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html)) @@ -3491,7 +3491,7 @@ def render_fragment_account_tickets(tickets) -> str: items_html += sx_call("events-frag-ticket-item", href=href, entry_name=ticket.entry_name, date_str=date_str, calendar_name=cal_name, - type_name=type_name, badge=badge_html) + type_name=type_name, badge=SxExpr(badge_html)) body = sx_call("events-frag-tickets-list", items=SxExpr(items_html)) else: body = sx_call("empty-state", message="No tickets yet.", @@ -3526,7 +3526,7 @@ def render_fragment_account_bookings(bookings) -> str: name=booking.name, date_str=date_str + date_str_extra, calendar_name=cal_name, cost_str=cost_str, - badge=badge_html) + badge=SxExpr(badge_html)) body = sx_call("events-frag-bookings-list", items=SxExpr(items_html)) else: body = sx_call("empty-state", message="No bookings yet.", diff --git a/shared/static/scripts/sx.js b/shared/static/scripts/sx.js index 4f536b3..59623c3 100644 --- a/shared/static/scripts/sx.js +++ b/shared/static/scripts/sx.js @@ -1262,9 +1262,23 @@ // DOM Renderer render: function (exprOrText, extraEnv) { - var expr = typeof exprOrText === "string" ? parse(exprOrText) : exprOrText; var env = extraEnv ? merge({}, _componentEnv, extraEnv) : _componentEnv; - return renderDOM(expr, env); + if (typeof exprOrText === "string") { + // Try single expression first; fall back to multi-expression fragment + try { + return renderDOM(parse(exprOrText), env); + } catch (e) { + var exprs = parseAll(exprOrText); + if (exprs.length === 0) throw e; + var frag = document.createDocumentFragment(); + for (var i = 0; i < exprs.length; i++) { + var node = renderDOM(exprs[i], env); + if (node) frag.appendChild(node); + } + return frag; + } + } + return renderDOM(exprOrText, env); }, // String Renderer (matches Python html.render output) diff --git a/shared/sx/helpers.py b/shared/sx/helpers.py index 8c0f827..a317781 100644 --- a/shared/sx/helpers.py +++ b/shared/sx/helpers.py @@ -431,6 +431,10 @@ def sx_response(source_or_component: str, status: int = 200, if new_rules: body = f'\n{body}' + # Dev mode: pretty-print sx source for readable Network tab responses + if _is_dev_mode(): + body = _pretty_print_sx_body(body) + resp = Response(body, status=status, content_type="text/sx") if new_classes: resp.headers["SX-Css-Add"] = ",".join(sorted(new_classes)) @@ -545,6 +549,14 @@ def sx_page(ctx: dict, page_sx: str, *, title = ctx.get("base_title", "Rose Ash") csrf = _get_csrf_token() + # Dev mode: pretty-print page sx for readable View Source + if _is_dev_mode() and page_sx and page_sx.startswith("("): + from .parser import parse as _parse, serialize as _serialize + try: + page_sx = _serialize(_parse(page_sx), pretty=True) + except Exception: + pass + return _SX_PAGE_TEMPLATE.format( title=_html_escape(title), asset_url=asset_url, @@ -592,6 +604,50 @@ def _get_sx_comp_cookie() -> str: return "" +def _is_dev_mode() -> bool: + """Check if running in dev mode (RELOAD=true).""" + import os + return os.getenv("RELOAD") == "true" + + +def _pretty_print_sx_body(body: str) -> str: + """Pretty-print the sx portion of a response body, preserving HTML blocks.""" + import re + from .parser import parse_all as _parse_all, serialize as _serialize + + # Split HTML prefix blocks (