Phase 5 cleanup: remove legacy HTML components, fix nav-tree fragment
All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 1m43s

- Remove old raw! layout components (~app-head, ~app-layout, ~oob-response,
  ~header-row, ~menu-row, ~oob-header, ~header-child) from layout.sexp
- Convert nav-tree fragment from Jinja HTML to sexp source, fixing the
  "Unexpected character: ." parse error caused by HTML leaking into sexp
- Add _as_sexp() helper to safely coerce HTML fragments to ~rich-text
- Fix federation/sexp/search.sexpr extra closing paren
- Remove dead _html() wrappers from blog and account sexp_components
- Remove stale render import from cart sexp_components
- Add dev_watcher.py to auto-reload on .sexp/.sexpr/.js/.css changes
- Add test_parse_all.py to parse-check all 59 sexpr/sexp files
- Fix test assertions for sx- attribute prefix (was hx-)
- Add sexp.js version logging for cache debugging

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-01 10:12:03 +00:00
parent 22802bd36b
commit a643b3532d
21 changed files with 225 additions and 232 deletions

View File

@@ -36,19 +36,36 @@ def get_asset_url(ctx: dict) -> str:
# Sexp-native helper functions — return sexp source (not HTML)
# ---------------------------------------------------------------------------
def _as_sexp(val: Any) -> SexpExpr | None:
"""Coerce a fragment value to SexpExpr.
If *val* is already a ``SexpExpr`` (from a ``text/sexp`` fragment),
return it as-is. If it's a non-empty string (HTML from a
``text/html`` fragment), wrap it in ``~rich-text``. Otherwise
return ``None``.
"""
if not val:
return None
if isinstance(val, SexpExpr):
return val
html = str(val)
escaped = html.replace("\\", "\\\\").replace('"', '\\"')
return SexpExpr(f'(~rich-text :html "{escaped}")')
def root_header_sexp(ctx: dict, *, oob: bool = False) -> str:
"""Build the root header row as a sexp call string."""
rights = ctx.get("rights") or {}
is_admin = rights.get("admin") if isinstance(rights, dict) else getattr(rights, "admin", False)
settings_url = call_url(ctx, "blog_url", "/settings/") if is_admin else ""
return sexp_call("header-row-sx",
cart_mini=ctx.get("cart_mini") and SexpExpr(str(ctx.get("cart_mini"))),
cart_mini=_as_sexp(ctx.get("cart_mini")),
blog_url=call_url(ctx, "blog_url", ""),
site_title=ctx.get("base_title", ""),
app_label=ctx.get("app_label", ""),
nav_tree=ctx.get("nav_tree") and SexpExpr(str(ctx.get("nav_tree"))),
auth_menu=ctx.get("auth_menu") and SexpExpr(str(ctx.get("auth_menu"))),
nav_panel=ctx.get("nav_panel") and SexpExpr(str(ctx.get("nav_panel"))),
nav_tree=_as_sexp(ctx.get("nav_tree")),
auth_menu=_as_sexp(ctx.get("auth_menu")),
nav_panel=_as_sexp(ctx.get("nav_panel")),
settings_url=settings_url,
is_admin=is_admin,
oob=oob,
@@ -285,19 +302,6 @@ def sexp_response(source_or_component: str, status: int = 200,
return resp
def oob_page(ctx: dict, *, oobs_html: str = "",
filter_html: str = "", aside_html: str = "",
content_html: str = "", menu_html: str = "") -> str:
"""Render an OOB response with standard swap targets."""
return render(
"oob-response",
oobs_html=oobs_html,
filter_html=filter_html,
aside_html=aside_html,
menu_html=menu_html,
content_html=content_html,
)
# ---------------------------------------------------------------------------
# Sexp wire-format full page shell