Wire sexp.js into page template with auto-init and HTMX integration

- Load sexp.js in ~app-layout before body.js
- Auto-process <script type="text/sexp"> tags on DOMContentLoaded
- Re-process after htmx:afterSwap for dynamic content
- Sexp.mount(target, expr, env) for rendering into DOM elements
- Sexp.processScripts() picks up data-components and data-mount tags
- client_components_tag() Python helper serializes Component objects
  back to sexp source for client-side consumption
- 37 parity tests all passing

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-28 23:36:49 +00:00
parent 2df1014ee3
commit 39e013a75e
4 changed files with 132 additions and 0 deletions

View File

@@ -201,6 +201,40 @@ def _get_request_context():
# Quart integration
# ---------------------------------------------------------------------------
def client_components_tag(*names: str) -> str:
"""Emit a <script type="text/sexp"> tag with component definitions.
Reads the source definitions from loaded .sexpr files and sends them
to the client so sexp.js can render them identically.
Usage in Python::
body_end_html = client_components_tag("test-filter-card", "test-row")
Or send all loaded components::
body_end_html = client_components_tag()
"""
from .parser import serialize
parts = []
for key, val in _COMPONENT_ENV.items():
if not isinstance(val, Component):
continue
if names and val.name not in names and key.lstrip("~") not in names:
continue
# Reconstruct defcomp source from the Component object
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sexp = "(" + " ".join(param_strs) + ")"
body_sexp = serialize(val.body, pretty=True)
parts.append(f"(defcomp ~{val.name} {params_sexp} {body_sexp})")
if not parts:
return ""
source = "\n".join(parts)
return f'<script type="text/sexp" data-components>{source}</script>'
def setup_sexp_bridge(app: Any) -> None:
"""Register s-expression helpers with a Quart app's Jinja environment.