From da1ca6009afd4382da42523f0a5ca2479a69e24e Mon Sep 17 00:00:00 2001 From: giles Date: Thu, 12 Mar 2026 09:51:04 +0000 Subject: [PATCH] GraphSX URL routing: s-expression URLs for sx-docs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace path-based URLs with nested s-expression URLs across the sx app. URLs like /language/docs/introduction become /(language.(doc.introduction)), making the URL simultaneously a query, render instruction, and address. - Add sx_router.py: catch-all route evaluator with dot→space conversion, auto-quoting slugs, two-phase eval, streaming detection, 301 redirects - Add page-functions.sx: section + page functions for URL dispatch - Rewrite nav-data.sx: ~200 hrefs to SX expression format, tree-descent nav matching via has-descendant-href? (replaces prefix heuristics) - Convert ~120 old-style hrefs across 26 .sx content files - Add SX Protocol proposal (etc/plans/sx-protocol) - Wire catch-all route in app.py with before_request redirect handler Co-Authored-By: Claude Opus 4.6 --- sx/app.py | 36 ++ sx/sx/analyzer.sx | 2 +- sx/sx/cssx.sx | 2 +- sx/sx/docs-content.sx | 2 +- sx/sx/essays/continuations.sx | 2 +- sx/sx/essays/godel-escher-bach.sx | 4 +- sx/sx/essays/reflexive-web.sx | 8 +- sx/sx/essays/sx-and-ai.sx | 4 +- sx/sx/essays/tail-call-optimization.sx | 2 +- sx/sx/essays/zero-tooling.sx | 2 +- sx/sx/examples.sx | 16 +- sx/sx/nav-data.sx | 412 ++++++++-------- sx/sx/page-functions.sx | 521 ++++++++++++++++++++ sx/sx/plans/content-addressed-components.sx | 8 +- sx/sx/plans/environment-images.sx | 6 +- sx/sx/plans/isomorphic.sx | 42 +- sx/sx/plans/js-bootstrapper.sx | 2 +- sx/sx/plans/predictive-prefetch.sx | 2 +- sx/sx/plans/reader-macros.sx | 2 +- sx/sx/plans/runtime-slicing.sx | 14 +- sx/sx/plans/self-hosting-bootstrapper.sx | 2 +- sx/sx/plans/status.sx | 30 +- sx/sx/plans/sx-protocol.sx | 197 ++++++++ sx/sx/plans/sx-urls.sx | 302 ++++++++++++ sx/sx/plans/typed-sx.sx | 14 +- sx/sx/reactive-islands/event-bridge.sx | 2 +- sx/sx/reactive-islands/index.sx | 2 +- sx/sx/reactive-islands/plan.sx | 4 +- sx/sx/routing-analyzer.sx | 4 +- sx/sx/specs.sx | 94 ++-- sx/sx/testing.sx | 12 +- sx/sxc/pages/sx_router.py | 359 ++++++++++++++ 32 files changed, 1763 insertions(+), 348 deletions(-) create mode 100644 sx/sx/page-functions.sx create mode 100644 sx/sx/plans/sx-protocol.sx create mode 100644 sx/sx/plans/sx-urls.sx create mode 100644 sx/sxc/pages/sx_router.py diff --git a/sx/app.py b/sx/app.py index bba5790..797a9ce 100644 --- a/sx/app.py +++ b/sx/app.py @@ -114,10 +114,37 @@ def create_app() -> "Quart": from shared.sx.pages import auto_mount_pages auto_mount_pages(app, "sx") + # --- GraphSX catch-all route: SX expression URLs --- + from sxc.pages.sx_router import eval_sx_url, redirect_old_url + + @app.before_request + async def sx_url_redirect(): + """Redirect old-style paths to SX expression URLs (301).""" + from quart import request, redirect as q_redirect + path = request.path + # Skip non-page paths + if path.startswith(("/static/", "/internal/", "/auth/", "/sx/")): + return None + # Skip SX expression URLs (already in new format) + if path.startswith("/(") or path.startswith("/~"): + return None + # Skip API/handler paths + if "/api/" in path: + return None + new_url = redirect_old_url(path) + if new_url: + qs = request.query_string.decode() + if qs: + new_url += "?" + qs + return q_redirect(new_url, 301) + @app.before_request async def trailing_slash_redirect(): from quart import request, redirect path = request.path + # Skip SX expression URLs — they don't use trailing slashes + if "(" in path or path.startswith("/~"): + return None if (path != "/" and not path.endswith("/") and request.method == "GET" @@ -128,6 +155,15 @@ def create_app() -> "Quart": target = path + "/" + ("?" + qs if qs else "") return redirect(target, 301) + @app.get("/") + async def sx_eval_route(expr): + """Catch-all: evaluate SX expression URLs.""" + result = await eval_sx_url(f"/{expr}") + if result is None: + from quart import abort + abort(404) + return result + @app.errorhandler(404) async def sx_not_found(e): from quart import request, make_response diff --git a/sx/sx/analyzer.sx b/sx/sx/analyzer.sx index 34a2e9b..e3a4595 100644 --- a/sx/sx/analyzer.sx +++ b/sx/sx/analyzer.sx @@ -12,7 +12,7 @@ "Each bar shows how many of the " (strong (str total-components)) " total components a page actually needs, computed by the " - (a :href "/language/specs/deps" :class "text-violet-700 underline" "deps.sx") + (a :href "/(language.(spec.deps))" :class "text-violet-700 underline" "deps.sx") " transitive closure algorithm. " "Click a page to see its component tree; expand a component to see its SX source.") diff --git a/sx/sx/cssx.sx b/sx/sx/cssx.sx index e2e6277..e40b264 100644 --- a/sx/sx/cssx.sx +++ b/sx/sx/cssx.sx @@ -373,7 +373,7 @@ "directly. They arrive as part of the rendered HTML — keyframes, custom properties, " "scoped rules, anything.") (li (strong "External stylesheets: ") "Components can reference pre-loaded CSS files " - "or lazy-load them via " (code "~suspense") " (see " (a :href "/applications/cssx/async" "Async CSS") ").") + "or lazy-load them via " (code "~suspense") " (see " (a :href "/(applications.(cssx.async))" "Async CSS") ").") (li (strong "Custom properties: ") "A " (code "~theme") " component sets " (code "--color-primary") " etc. via a " (code "