Files
rose-ash/sx/sxc/pages/sx_router.py
giles f96506024e Add CEK Machine section under Geography with live island demos
geography/cek.sx: overview page (three registers, deref-as-shift
explanation) + demo page with 5 live islands (counter, computed chain,
reactive attrs, stopwatch effect+cleanup, batch coalescing). Nav entry,
router routes, defpage definitions. CEK exports (cekRun, makeCekState,
makeReactiveResetFrame, evalExpr) added to Sx public API via
platform_js.py.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-14 01:37:16 +00:00

319 lines
14 KiB
Python

"""GraphSX URL router — evaluate s-expression URLs.
Handles URLs like /(language.(doc.introduction)) by:
1. Delegating to the bootstrapped spec (router.sx → prepare_url_expr):
- Dots → spaces (URL-safe whitespace encoding)
- Parse as SX expression
- Auto-quote unknowns (symbols not in env → strings)
2. Evaluating the prepared expression against page functions
3. Wrapping the result in (~layouts/doc :path "..." content)
4. Returning full page or OOB response
The URL evaluation logic lives in the SX spec (shared/sx/ref/router.sx)
and is bootstrapped to Python (sx_ref.py) and JavaScript (sx-browser.js).
This handler is generic infrastructure — all routing semantics are in SX.
Special cases:
- "/" → home page
- Streaming pages → delegate to existing PageDef infrastructure
- Direct component URLs: /(~component-name) → render component
"""
from __future__ import annotations
import logging
import re
from typing import Any
from urllib.parse import unquote
logger = logging.getLogger("sx.router")
# ---------------------------------------------------------------------------
# Streaming detection (host-level concern, not in spec)
# ---------------------------------------------------------------------------
_STREAMING_PAGES = {
# URL slug → defpage name for streaming pages
"streaming": "streaming-demo",
}
def _is_streaming_url(expr: Any) -> str | None:
"""Check if expression resolves to a streaming page.
Returns the defpage name if streaming, None otherwise.
Detects: (geography (isomorphism "streaming"))
"""
from shared.sx.types import Symbol
# Check for (geography (isomorphism "streaming"))
if not isinstance(expr, list) or len(expr) < 2:
return None
# Walk nested structure to find isomorphism call
def _find_slug(e: Any) -> str | None:
if not isinstance(e, list) or not e:
return None
head = e[0]
if isinstance(head, Symbol) and head.name == "isomorphism":
for arg in e[1:]:
if isinstance(arg, str) and arg in _STREAMING_PAGES:
return _STREAMING_PAGES[arg]
if isinstance(arg, Symbol) and arg.name in _STREAMING_PAGES:
return _STREAMING_PAGES[arg.name]
# Recurse into nested calls
for arg in e[1:]:
result = _find_slug(arg)
if result:
return result
return None
return _find_slug(expr)
# ---------------------------------------------------------------------------
# Main eval handler
# ---------------------------------------------------------------------------
async def eval_sx_url(raw_path: str) -> Any:
"""Evaluate an SX URL and return the response.
This is the main entry point for the catch-all route handler.
Returns a Quart Response object, or None if the path isn't an SX URL.
URL parsing and auto-quoting are delegated to the bootstrapped spec
functions (prepare_url_expr from router.sx). This handler provides
only the host-level concerns: building the env, async eval, response
formatting, streaming detection.
"""
from quart import make_response, Response
from shared.sx.jinja_bridge import get_component_env, _get_request_context
from shared.sx.pages import get_page, get_page_helpers, _eval_slot
from shared.sx.types import Symbol, Keyword
from shared.sx.helpers import full_page_sx, oob_page_sx, sx_response
from shared.sx.page import get_template_context
from shared.browser.app.utils.htmx import is_htmx_request
from shared.sx.ref.sx_ref import prepare_url_expr
path = unquote(raw_path).strip()
# Home page
if path == "/":
expr = [Symbol("home")]
else:
# SX URLs: /(expr) or /~component
if not (path.startswith("/(") or path.startswith("/~")):
return None # not an SX URL — let other handlers try
# Build env for auto-quoting: components + page helpers
env = dict(get_component_env())
env.update(get_page_helpers("sx"))
# Use the bootstrapped spec: parse URL, auto-quote unknowns
expr = prepare_url_expr(path[1:], env) # strip leading /
# Bare symbol (e.g. /~comp) → wrap in list for eval
if isinstance(expr, Symbol):
expr = [expr]
if not expr:
return None
# Check for streaming page BEFORE eval
streaming_page = _is_streaming_url(expr)
if streaming_page:
page_def = get_page("sx", streaming_page)
if page_def:
from shared.sx.pages import execute_page_streaming, execute_page_streaming_oob
if is_htmx_request():
gen = await execute_page_streaming_oob(page_def, "sx")
return Response(gen, content_type="text/sx; charset=utf-8")
gen = await execute_page_streaming(page_def, "sx")
return Response(gen, content_type="text/html; charset=utf-8")
# Build env if not already built (home case)
if path == "/":
env = dict(get_component_env())
env.update(get_page_helpers("sx"))
ctx = _get_request_context()
# Nav hrefs use /sx/ prefix — reconstruct the full path for nav matching
path_str = f"/sx{raw_path}" if raw_path != "/" else "/sx/"
# Component calls go straight to _eval_slot (aser handles expansion).
# Page function calls need async_eval first (routing + data fetching).
head = expr[0] if isinstance(expr, list) and expr else None
is_component_call = isinstance(head, Symbol) and head.name.startswith("~")
if is_component_call:
page_ast = expr
else:
import os
if os.environ.get("SX_USE_REF") == "1":
from shared.sx.ref.async_eval_ref import async_eval
else:
from shared.sx.async_eval import async_eval
try:
page_ast = await async_eval(expr, env, ctx)
except Exception as e:
logger.error("SX URL page-fn eval failed for %s: %s", raw_path, e, exc_info=True)
return None
if page_ast is None:
page_ast = []
wrapped_ast = [
Symbol("~layouts/doc"), Keyword("path"), path_str,
page_ast,
]
try:
content_sx = await _eval_slot(wrapped_ast, env, ctx)
except Exception as e:
logger.error("SX URL render failed for %s: %s", raw_path, e, exc_info=True)
return None
# Return response
if is_htmx_request():
return sx_response(await oob_page_sx(content=content_sx))
else:
tctx = await get_template_context()
html = await full_page_sx(tctx, header_rows="", content=content_sx)
return await make_response(html, 200)
# ---------------------------------------------------------------------------
# Old URL → SX URL redirect patterns
# ---------------------------------------------------------------------------
_REDIRECT_PATTERNS = [
# --- API endpoint redirects (most specific first) ---
# Reference API: /geography/hypermedia/reference/api/item/<id>
(re.compile(r"^/geography/hypermedia/reference/api/item/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(reference.(api.(item.{m.group(1)})))))"),
# Reference API: /geography/hypermedia/reference/api/<name>
(re.compile(r"^/geography/hypermedia/reference/api/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(reference.(api.{m.group(1)}))))"),
# Example API: sub-paths with params
(re.compile(r"^/geography/hypermedia/examples/api/editrow/(.+?)/cancel/?$"),
lambda m: f"/sx/(geography.(hypermedia.(example.(api.(editrow-cancel.{m.group(1)})))))"),
(re.compile(r"^/geography/hypermedia/examples/api/editrow/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(example.(api.(editrow.{m.group(1)})))))"),
(re.compile(r"^/geography/hypermedia/examples/api/delete/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(example.(api.(delete.{m.group(1)})))))"),
(re.compile(r"^/geography/hypermedia/examples/api/tabs/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(example.(api.(tabs.{m.group(1)})))))"),
# Example API: sub-paths collapsed to hyphens
(re.compile(r"^/geography/hypermedia/examples/api/edit/cancel/?$"),
"/sx/(geography.(hypermedia.(example.(api.edit-cancel))))"),
(re.compile(r"^/geography/hypermedia/examples/api/progress/start/?$"),
"/sx/(geography.(hypermedia.(example.(api.progress-start))))"),
(re.compile(r"^/geography/hypermedia/examples/api/progress/status/?$"),
"/sx/(geography.(hypermedia.(example.(api.progress-status))))"),
(re.compile(r"^/geography/hypermedia/examples/api/validate/submit/?$"),
"/sx/(geography.(hypermedia.(example.(api.validate-submit))))"),
(re.compile(r"^/geography/hypermedia/examples/api/dialog/close/?$"),
"/sx/(geography.(hypermedia.(example.(api.dialog-close))))"),
(re.compile(r"^/geography/hypermedia/examples/api/putpatch/edit-all/?$"),
"/sx/(geography.(hypermedia.(example.(api.putpatch-edit-all))))"),
(re.compile(r"^/geography/hypermedia/examples/api/putpatch/cancel/?$"),
"/sx/(geography.(hypermedia.(example.(api.putpatch-cancel))))"),
# Example API: simple names
(re.compile(r"^/geography/hypermedia/examples/api/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(example.(api.{m.group(1)}))))"),
# Reactive API: sub-paths collapsed
(re.compile(r"^/geography/reactive/api/search/(.+?)/?$"),
lambda m: f"/sx/(geography.(reactive.(api.search-{m.group(1)})))"),
(re.compile(r"^/geography/reactive/api/(.+?)/?$"),
lambda m: f"/sx/(geography.(reactive.(api.{m.group(1)})))"),
# --- Page redirects ---
# More specific first
(re.compile(r"^/language/specs/explore/(.+?)/?$"),
lambda m: f"/sx/(language.(spec.(explore.{m.group(1)})))"),
(re.compile(r"^/language/docs/(.+?)/?$"),
lambda m: f"/sx/(language.(doc.{m.group(1)}))"),
(re.compile(r"^/language/docs/?$"),
"/sx/(language.(doc))"),
(re.compile(r"^/language/specs/(.+?)/?$"),
lambda m: f"/sx/(language.(spec.{m.group(1)}))"),
(re.compile(r"^/language/bootstrappers/page-helpers/?$"),
"/sx/(language.(bootstrapper.page-helpers))"),
(re.compile(r"^/language/bootstrappers/(.+?)/?$"),
lambda m: f"/sx/(language.(bootstrapper.{m.group(1)}))"),
(re.compile(r"^/language/testing/(.+?)/?$"),
lambda m: f"/sx/(language.(test.{m.group(1)}))"),
(re.compile(r"^/geography/hypermedia/reference/attributes/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(reference-detail.attributes.{m.group(1)})))"),
(re.compile(r"^/geography/hypermedia/reference/headers/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(reference-detail.headers.{m.group(1)})))"),
(re.compile(r"^/geography/hypermedia/reference/events/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(reference-detail.events.{m.group(1)})))"),
(re.compile(r"^/geography/hypermedia/reference/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(reference.{m.group(1)})))"),
(re.compile(r"^/geography/hypermedia/examples/(.+?)/?$"),
lambda m: f"/sx/(geography.(hypermedia.(example.{m.group(1)})))"),
(re.compile(r"^/geography/hypermedia/reference/?$"),
"/sx/(geography.(hypermedia.(reference)))"),
(re.compile(r"^/geography/hypermedia/examples/?$"),
"/sx/(geography.(hypermedia.(example)))"),
(re.compile(r"^/geography/reactive/(.+?)/?$"),
lambda m: f"/sx/(geography.(reactive.{m.group(1)}))"),
(re.compile(r"^/geography/isomorphism/(.+?)/?$"),
lambda m: f"/sx/(geography.(isomorphism.{m.group(1)}))"),
(re.compile(r"^/geography/cek/(.+?)/?$"),
lambda m: f"/sx/(geography.(cek.{m.group(1)}))"),
(re.compile(r"^/geography/spreads/?$"),
"/sx/(geography.(spreads))"),
(re.compile(r"^/geography/marshes/?$"),
"/sx/(geography.(marshes))"),
(re.compile(r"^/applications/cssx/(.+?)/?$"),
lambda m: f"/sx/(applications.(cssx.{m.group(1)}))"),
(re.compile(r"^/applications/protocols/(.+?)/?$"),
lambda m: f"/sx/(applications.(protocol.{m.group(1)}))"),
(re.compile(r"^/etc/essays/(.+?)/?$"),
lambda m: f"/sx/(etc.(essay.{m.group(1)}))"),
(re.compile(r"^/etc/philosophy/(.+?)/?$"),
lambda m: f"/sx/(etc.(philosophy.{m.group(1)}))"),
(re.compile(r"^/etc/plans/(.+?)/?$"),
lambda m: f"/sx/(etc.(plan.{m.group(1)}))"),
# Section indices
(re.compile(r"^/language/docs/?$"), "/sx/(language.(doc))"),
(re.compile(r"^/language/specs/?$"), "/sx/(language.(spec))"),
(re.compile(r"^/language/bootstrappers/?$"), "/sx/(language.(bootstrapper))"),
(re.compile(r"^/language/testing/?$"), "/sx/(language.(test))"),
(re.compile(r"^/language/?$"), "/sx/(language)"),
(re.compile(r"^/geography/hypermedia/?$"), "/sx/(geography.(hypermedia))"),
(re.compile(r"^/geography/reactive/?$"), "/sx/(geography.(reactive))"),
(re.compile(r"^/geography/isomorphism/?$"), "/sx/(geography.(isomorphism))"),
(re.compile(r"^/geography/cek/?$"), "/sx/(geography.(cek))"),
(re.compile(r"^/geography/?$"), "/sx/(geography)"),
(re.compile(r"^/applications/cssx/?$"), "/sx/(applications.(cssx))"),
(re.compile(r"^/applications/protocols/?$"), "/sx/(applications.(protocol))"),
(re.compile(r"^/applications/?$"), "/sx/(applications)"),
(re.compile(r"^/etc/essays/?$"), "/sx/(etc.(essay))"),
(re.compile(r"^/etc/philosophy/?$"), "/sx/(etc.(philosophy))"),
(re.compile(r"^/etc/plans/?$"), "/sx/(etc.(plan))"),
(re.compile(r"^/etc/?$"), "/sx/(etc)"),
]
def redirect_old_url(path: str) -> str | None:
"""Check if an old-style path should redirect to an SX URL.
Returns the new SX URL if a redirect is needed, None otherwise.
"""
for pattern, replacement in _REDIRECT_PATTERNS:
m = pattern.match(path)
if m:
if callable(replacement):
return replacement(m)
return replacement
return None