Send all responses as sexp wire format with client-side rendering
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 5m35s
- Server sends sexp source text, client (sexp.js) renders everything - SexpExpr marker class for nested sexp composition in serialize() - sexp_page() HTML shell with data-mount="body" for full page loads - sexp_response() returns text/sexp for OOB/partial responses - ~app-body layout component replaces ~app-layout (no raw!) - ~rich-text is the only component using raw! (for CMS HTML content) - Fragment endpoints return text/sexp, auto-wrapped in SexpExpr - All _*_html() helpers converted to _*_sexp() returning sexp source - Head auto-hoist: sexp.js moves meta/title/link/script[ld+json] from rendered body to document.head automatically - Unknown components render warning box instead of crashing page - Component kwargs preserve AST for lazy rendering (fixes <> in kwargs) - Fix unterminated paren in events/sexp/tickets.sexpr Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -21,6 +21,37 @@ from typing import Any
|
||||
from .types import Keyword, Symbol, NIL
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# SexpExpr — pre-built sexp source marker
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class SexpExpr:
|
||||
"""Pre-built sexp source that serialize() outputs unquoted.
|
||||
|
||||
Use this to nest sexp call strings inside other sexp_call() invocations
|
||||
without them being quoted as strings::
|
||||
|
||||
sexp_call("parent", child=SexpExpr(sexp_call("child", x=1)))
|
||||
# => (~parent :child (~child :x 1))
|
||||
"""
|
||||
__slots__ = ("source",)
|
||||
|
||||
def __init__(self, source: str):
|
||||
self.source = source
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"SexpExpr({self.source!r})"
|
||||
|
||||
def __str__(self) -> str:
|
||||
return self.source
|
||||
|
||||
def __add__(self, other: object) -> "SexpExpr":
|
||||
return SexpExpr(self.source + str(other))
|
||||
|
||||
def __radd__(self, other: object) -> "SexpExpr":
|
||||
return SexpExpr(str(other) + self.source)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Errors
|
||||
# ---------------------------------------------------------------------------
|
||||
@@ -230,6 +261,9 @@ def _parse_map(tok: Tokenizer) -> dict[str, Any]:
|
||||
|
||||
def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str:
|
||||
"""Serialize a value back to s-expression text."""
|
||||
if isinstance(expr, SexpExpr):
|
||||
return expr.source
|
||||
|
||||
if isinstance(expr, list):
|
||||
if not expr:
|
||||
return "()"
|
||||
@@ -269,6 +303,13 @@ def serialize(expr: Any, indent: int = 0, pretty: bool = False) -> str:
|
||||
items.append(serialize(v, indent, pretty))
|
||||
return "{" + " ".join(items) + "}"
|
||||
|
||||
# Catch callables (Python functions leaked into sexp data)
|
||||
if callable(expr):
|
||||
import logging
|
||||
logging.getLogger("sexp").error(
|
||||
"serialize: callable leaked into sexp data: %r", expr)
|
||||
return "nil"
|
||||
|
||||
# Fallback for Lambda/Component — show repr
|
||||
return repr(expr)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user