Fix missing SxExpr wraps in events + pretty-print sx in dev mode + multi-expr render
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m39s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m39s
- Wrap 15 call sites in events/sx_components.py where sx-generating functions were passed as plain strings to sx_call(), causing raw s-expression source to leak into the rendered page. - Add dev-mode pretty-printing (RELOAD=true) for sx responses and full page sx source — indented output in Network tab and View Source. - Fix Sx.render to handle multiple top-level expressions by falling back to parseAll and returning a DocumentFragment. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -791,7 +791,7 @@ def _tickets_main_panel_html(ctx: dict, tickets: list) -> str:
|
|||||||
type_name=tt.name if tt else None,
|
type_name=tt.name if tt else None,
|
||||||
time_str=time_str or None,
|
time_str=time_str or None,
|
||||||
cal_name=cal.name if cal else 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]))
|
code_prefix=ticket.code[:8]))
|
||||||
|
|
||||||
cards_html = "".join(ticket_cards)
|
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",
|
entry_name=entry.name if entry else "\u2014",
|
||||||
date=SxExpr(date_html),
|
date=SxExpr(date_html),
|
||||||
type_name=tt.name if tt else "\u2014",
|
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))
|
action=SxExpr(action_html))
|
||||||
|
|
||||||
return sx_call("events-ticket-admin-panel",
|
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:
|
if tp is not None:
|
||||||
qty = pending_tickets.get(entry.id, 0)
|
qty = pending_tickets.get(entry.id, 0)
|
||||||
widget_html = sx_call("events-entry-widget-wrapper",
|
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",
|
return sx_call("events-entry-card",
|
||||||
title=SxExpr(title_html), badges=SxExpr(badges_html),
|
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:
|
if tp is not None:
|
||||||
qty = pending_tickets.get(entry.id, 0)
|
qty = pending_tickets.get(entry.id, 0)
|
||||||
widget_html = sx_call("events-entry-tile-widget-wrapper",
|
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",
|
return sx_call("events-entry-card-tile",
|
||||||
title=SxExpr(title_html), badges=SxExpr(badges_html),
|
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,
|
list_href=list_href, tile_href=tile_href,
|
||||||
hx_select=hx_select, list_cls=list_active,
|
hx_select=hx_select, list_cls=list_active,
|
||||||
tile_cls=tile_active, storage_key="events_view",
|
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,
|
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",
|
entry_name=entry.name if entry else "\u2014",
|
||||||
date=SxExpr(date_html),
|
date=SxExpr(date_html),
|
||||||
type_name=tt.name if tt else "\u2014",
|
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)
|
time_str=time_str)
|
||||||
|
|
||||||
|
|
||||||
@@ -1656,7 +1656,7 @@ def render_lookup_result(ticket, error: str | None) -> str:
|
|||||||
if cal:
|
if cal:
|
||||||
info_html += sx_call("events-lookup-cal", cal_name=cal.name)
|
info_html += sx_call("events-lookup-cal", cal_name=cal.name)
|
||||||
info_html += sx_call("events-lookup-status",
|
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:
|
if checked_in_at:
|
||||||
info_html += sx_call("events-lookup-checkin-time",
|
info_html += sx_call("events-lookup-checkin-time",
|
||||||
date_str=checked_in_at.strftime("%B %d, %Y at %H:%M"))
|
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",
|
rows_html += sx_call("events-entry-tickets-admin-row",
|
||||||
code=code, code_short=code[:12] + "...",
|
code=code, code_short=code[:12] + "...",
|
||||||
type_name=tt.name if tt else "\u2014",
|
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))
|
action=SxExpr(action_html))
|
||||||
|
|
||||||
if tickets:
|
if tickets:
|
||||||
@@ -1783,7 +1783,7 @@ def _entry_main_panel_html(ctx: dict) -> str:
|
|||||||
# State
|
# State
|
||||||
state_html = _field("State", sx_call("events-entry-state-field",
|
state_html = _field("State", sx_call("events-entry-state-field",
|
||||||
entry_id=str(eid),
|
entry_id=str(eid),
|
||||||
badge=_entry_state_badge_html(state)))
|
badge=SxExpr(_entry_state_badge_html(state))))
|
||||||
|
|
||||||
# Cost
|
# Cost
|
||||||
cost = getattr(entry, "cost", None)
|
cost = getattr(entry, "cost", None)
|
||||||
@@ -1794,7 +1794,7 @@ def _entry_main_panel_html(ctx: dict) -> str:
|
|||||||
# Ticket Configuration (admin)
|
# Ticket Configuration (admin)
|
||||||
tickets_html = _field("Tickets", sx_call("events-entry-tickets-field",
|
tickets_html = _field("Tickets", sx_call("events-entry-tickets-field",
|
||||||
entry_id=str(eid),
|
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)
|
# Buy Tickets (public-facing)
|
||||||
ticket_remaining = ctx.get("ticket_remaining")
|
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 []
|
entry_posts = ctx.get("entry_posts") or []
|
||||||
posts_html = _field("Associated Posts", sx_call("events-entry-posts-field",
|
posts_html = _field("Associated Posts", sx_call("events-entry-posts-field",
|
||||||
entry_id=str(eid),
|
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
|
# Options and Edit Button
|
||||||
edit_url = url_for(
|
edit_url = url_for(
|
||||||
@@ -1830,7 +1830,7 @@ def _entry_main_panel_html(ctx: dict) -> str:
|
|||||||
cost=SxExpr(cost_html), tickets=SxExpr(tickets_html),
|
cost=SxExpr(cost_html), tickets=SxExpr(tickets_html),
|
||||||
buy=SxExpr(buy_html), date=SxExpr(date_html),
|
buy=SxExpr(buy_html), date=SxExpr(date_html),
|
||||||
posts=SxExpr(posts_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)
|
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",
|
label_html = sx_call("events-entry-label",
|
||||||
entry_id=str(entry.id),
|
entry_id=str(entry.id),
|
||||||
title=_entry_title_html(entry),
|
title=SxExpr(_entry_title_html(entry)),
|
||||||
times=_entry_times_html(entry))
|
times=SxExpr(_entry_times_html(entry)))
|
||||||
|
|
||||||
nav_html = _entry_nav_html(ctx)
|
nav_html = _entry_nav_html(ctx)
|
||||||
|
|
||||||
@@ -1988,7 +1988,7 @@ def _entry_title_html(entry) -> str:
|
|||||||
state = getattr(entry, "state", "pending") or "pending"
|
state = getattr(entry, "state", "pending") or "pending"
|
||||||
return sx_call("events-entry-title",
|
return sx_call("events-entry-title",
|
||||||
name=entry.name,
|
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:
|
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"
|
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_items += sx_call("events-buy-type-item",
|
||||||
type_name=tt.name, cost_str=cost_str,
|
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))
|
body_html = sx_call("events-buy-types-wrapper", items=SxExpr(type_items))
|
||||||
else:
|
else:
|
||||||
qty = user_ticket_count or 0
|
qty = user_ticket_count or 0
|
||||||
body_html = sx_call("events-buy-default",
|
body_html = sx_call("events-buy-default",
|
||||||
price_str=f"\u00a3{tp:.2f}",
|
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",
|
return sx_call("events-buy-panel",
|
||||||
entry_id=eid_s, info=SxExpr(info_html), body=SxExpr(body_html))
|
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",
|
items_html += sx_call("events-frag-ticket-item",
|
||||||
href=href, entry_name=ticket.entry_name,
|
href=href, entry_name=ticket.entry_name,
|
||||||
date_str=date_str, calendar_name=cal_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))
|
body = sx_call("events-frag-tickets-list", items=SxExpr(items_html))
|
||||||
else:
|
else:
|
||||||
body = sx_call("empty-state", message="No tickets yet.",
|
body = sx_call("empty-state", message="No tickets yet.",
|
||||||
@@ -3526,7 +3526,7 @@ def render_fragment_account_bookings(bookings) -> str:
|
|||||||
name=booking.name,
|
name=booking.name,
|
||||||
date_str=date_str + date_str_extra,
|
date_str=date_str + date_str_extra,
|
||||||
calendar_name=cal_name, cost_str=cost_str,
|
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))
|
body = sx_call("events-frag-bookings-list", items=SxExpr(items_html))
|
||||||
else:
|
else:
|
||||||
body = sx_call("empty-state", message="No bookings yet.",
|
body = sx_call("empty-state", message="No bookings yet.",
|
||||||
|
|||||||
@@ -1262,9 +1262,23 @@
|
|||||||
|
|
||||||
// DOM Renderer
|
// DOM Renderer
|
||||||
render: function (exprOrText, extraEnv) {
|
render: function (exprOrText, extraEnv) {
|
||||||
var expr = typeof exprOrText === "string" ? parse(exprOrText) : exprOrText;
|
|
||||||
var env = extraEnv ? merge({}, _componentEnv, extraEnv) : _componentEnv;
|
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)
|
// String Renderer (matches Python html.render output)
|
||||||
|
|||||||
@@ -431,6 +431,10 @@ def sx_response(source_or_component: str, status: int = 200,
|
|||||||
if new_rules:
|
if new_rules:
|
||||||
body = f'<style data-sx-css>{new_rules}</style>\n{body}'
|
body = f'<style data-sx-css>{new_rules}</style>\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")
|
resp = Response(body, status=status, content_type="text/sx")
|
||||||
if new_classes:
|
if new_classes:
|
||||||
resp.headers["SX-Css-Add"] = ",".join(sorted(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")
|
title = ctx.get("base_title", "Rose Ash")
|
||||||
csrf = _get_csrf_token()
|
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(
|
return _SX_PAGE_TEMPLATE.format(
|
||||||
title=_html_escape(title),
|
title=_html_escape(title),
|
||||||
asset_url=asset_url,
|
asset_url=asset_url,
|
||||||
@@ -592,6 +604,50 @@ def _get_sx_comp_cookie() -> str:
|
|||||||
return ""
|
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 (<style>, <script>) from the sx tail
|
||||||
|
# These are always at the start, each on its own line
|
||||||
|
parts: list[str] = []
|
||||||
|
rest = body
|
||||||
|
while rest.startswith("<"):
|
||||||
|
end = rest.find(">", rest.find("</")) + 1
|
||||||
|
if end <= 0:
|
||||||
|
break
|
||||||
|
# Find end of the closing tag
|
||||||
|
tag_match = re.match(r'<(style|script)[^>]*>[\s\S]*?</\1>', rest)
|
||||||
|
if tag_match:
|
||||||
|
parts.append(tag_match.group(0))
|
||||||
|
rest = rest[tag_match.end():].lstrip("\n")
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
sx_source = rest.strip()
|
||||||
|
if not sx_source or sx_source[0] != "(":
|
||||||
|
return body
|
||||||
|
|
||||||
|
try:
|
||||||
|
exprs = _parse_all(sx_source)
|
||||||
|
if len(exprs) == 1:
|
||||||
|
parts.append(_serialize(exprs[0], pretty=True))
|
||||||
|
else:
|
||||||
|
# Multiple top-level expressions — indent each
|
||||||
|
pretty_parts = [_serialize(expr, pretty=True) for expr in exprs]
|
||||||
|
parts.append("\n\n".join(pretty_parts))
|
||||||
|
return "\n\n".join(parts)
|
||||||
|
except Exception:
|
||||||
|
return body
|
||||||
|
|
||||||
|
|
||||||
def _html_escape(s: str) -> str:
|
def _html_escape(s: str) -> str:
|
||||||
"""Minimal HTML escaping for attribute values."""
|
"""Minimal HTML escaping for attribute values."""
|
||||||
return (s.replace("&", "&")
|
return (s.replace("&", "&")
|
||||||
|
|||||||
Reference in New Issue
Block a user