Fix missing SxExpr wraps in events + pretty-print sx in dev mode + multi-expr render
- 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:
@@ -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)
|
||||
|
||||
@@ -431,6 +431,10 @@ def sx_response(source_or_component: str, status: int = 200,
|
||||
if new_rules:
|
||||
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")
|
||||
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 (<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:
|
||||
"""Minimal HTML escaping for attribute values."""
|
||||
return (s.replace("&", "&")
|
||||
|
||||
Reference in New Issue
Block a user