From cf5e767510bf43359a2d83e0918a91d6e0177324 Mon Sep 17 00:00:00 2001 From: giles Date: Fri, 6 Mar 2026 15:47:56 +0000 Subject: [PATCH] Phase 3: Client-side routing with SX page registry + routing analyzer demo MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add client-side route matching so pure pages (no IO deps) can render instantly without a server roundtrip. Page metadata serialized as SX dict literals (not JSON) in + @@ -638,6 +639,66 @@ details.group{{overflow:hidden}}details.group>summary{{list-style:none}}details. """ +def _build_pages_sx(service: str) -> str: + """Build SX page registry for client-side routing. + + Returns SX dict literals (one per page) parseable by the client's + ``parse`` function. Each dict has keys: name, path, auth, has-data, + content, closure. + """ + from .pages import get_all_pages + from .parser import serialize as sx_serialize + + pages = get_all_pages(service) + if not pages: + return "" + + entries = [] + for page_def in pages.values(): + content_src = "" + if page_def.content_expr is not None: + try: + content_src = sx_serialize(page_def.content_expr) + except Exception: + pass + + auth = page_def.auth if isinstance(page_def.auth, str) else "custom" + has_data = "true" if page_def.data_expr is not None else "false" + + # Build closure as SX dict + closure_parts: list[str] = [] + for k, v in page_def.closure.items(): + if isinstance(v, (str, int, float, bool)): + closure_parts.append(f":{k} {_sx_literal(v)}") + closure_sx = "{" + " ".join(closure_parts) + "}" + + entry = ( + "{:name " + _sx_literal(page_def.name) + + " :path " + _sx_literal(page_def.path) + + " :auth " + _sx_literal(auth) + + " :has-data " + has_data + + " :content " + _sx_literal(content_src) + + " :closure " + closure_sx + "}" + ) + entries.append(entry) + + return "\n".join(entries) + + +def _sx_literal(v: object) -> str: + """Serialize a Python value as an SX literal.""" + if v is None: + return "nil" + if isinstance(v, bool): + return "true" if v else "false" + if isinstance(v, (int, float)): + return str(v) + if isinstance(v, str): + escaped = v.replace("\\", "\\\\").replace('"', '\\"').replace("\n", "\\n") + return f'"{escaped}"' + return "nil" + + def sx_page(ctx: dict, page_sx: str, *, meta_html: str = "") -> str: """Return a minimal HTML shell that boots the page from sx source. @@ -692,6 +753,14 @@ def sx_page(ctx: dict, page_sx: str, *, else: styles_json = _build_style_dict_json() + # Page registry for client-side routing + pages_sx = "" + try: + from quart import current_app + pages_sx = _build_pages_sx(current_app.name) + except Exception: + pass + return _SX_PAGE_TEMPLATE.format( title=_html_escape(title), asset_url=asset_url, @@ -701,6 +770,7 @@ def sx_page(ctx: dict, page_sx: str, *, component_defs=component_defs, styles_hash=styles_hash, styles_json=styles_json, + pages_sx=pages_sx, page_sx=page_sx, sx_css=sx_css, sx_css_classes=sx_css_classes, diff --git a/shared/sx/ref/boot.sx b/shared/sx/ref/boot.sx index 94dc7dc..41e7fec 100644 --- a/shared/sx/ref/boot.sx +++ b/shared/sx/ref/boot.sx @@ -295,6 +295,33 @@ scripts)))) +;; -------------------------------------------------------------------------- +;; Page registry for client-side routing +;; -------------------------------------------------------------------------- + +(define _page-routes (list)) + +(define process-page-scripts + (fn () + ;; Process