Files
rose-ash/sx/sxc/sx_components.py
giles fd67f202c2
Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Add Continuations essay to SX docs
Covers server-side (suspendable rendering, streaming, error boundaries),
client-side (linear async flows, wizard forms, cooperative scheduling,
undo), and implementation path from the existing TCO trampoline. Updates
TCO essay's continuations section to link to the new essay instead of
dismissing the idea. Fixes "What sx is not" to acknowledge macros + TCO.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-04 10:41:22 +00:00

3020 lines
155 KiB
Python

"""SX docs site s-expression page components."""
from __future__ import annotations
import os
from shared.sx.jinja_bridge import load_sx_dir, watch_sx_dir
from shared.sx.helpers import (
render_to_sx, SxExpr, get_asset_url,
)
from content.highlight import highlight
# Load .sx components from sxc/ directory (not sx/ to avoid name collision)
_sxc_dir = os.path.dirname(__file__)
load_sx_dir(_sxc_dir)
watch_sx_dir(_sxc_dir)
def _code(code: str, language: str = "lisp") -> str:
"""Build a ~doc-code component with highlighted content."""
highlighted = highlight(code, language)
return f'(~doc-code :code {highlighted})'
def _example_code(code: str, language: str = "lisp") -> str:
"""Build an ~example-source component with highlighted content."""
highlighted = highlight(code, language)
return f'(~example-source :code {highlighted})'
def _placeholder(div_id: str) -> str:
"""Empty placeholder that will be filled by OOB swap on interaction."""
return (f'(div :id "{div_id}"'
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3"'
f' (p :class "text-stone-400 italic text-sm"'
f' "Trigger the demo to see the actual content.")))')
def _component_source_text(*names: str) -> str:
"""Get defcomp source text for named components."""
from shared.sx.jinja_bridge import _COMPONENT_ENV
from shared.sx.types import Component
from shared.sx.parser import serialize
parts = []
for name in names:
key = name if name.startswith("~") else f"~{name}"
val = _COMPONENT_ENV.get(key)
if isinstance(val, Component):
param_strs = ["&key"] + list(val.params)
if val.has_children:
param_strs.extend(["&rest", "children"])
params_sx = "(" + " ".join(param_strs) + ")"
body_sx = serialize(val.body, pretty=True)
parts.append(f"(defcomp ~{val.name} {params_sx}\n{body_sx})")
return "\n\n".join(parts)
def _oob_code(target_id: str, text: str) -> str:
"""OOB swap that displays plain code in a styled block."""
escaped = text.replace('\\', '\\\\').replace('"', '\\"')
return (f'(div :id "{target_id}" :sx-swap-oob "innerHTML"'
f' (div :class "bg-stone-50 border border-stone-200 rounded p-4 mt-3 overflow-x-auto"'
f' (pre :class "text-sm whitespace-pre-wrap"'
f' (code "{escaped}"))))')
def _clear_components_btn() -> str:
"""Button that clears the client-side component cache (localStorage + in-memory)."""
js = ("localStorage.removeItem('sx-components-hash');"
"localStorage.removeItem('sx-components-src');"
"var e=Sx.getEnv();Object.keys(e).forEach(function(k){if(k.charAt(0)==='~')delete e[k]});"
"var b=this;b.textContent='Cleared!';setTimeout(function(){b.textContent='Clear component cache'},2000)")
return (f'(button :onclick "{js}"'
f' :class "text-xs text-stone-400 hover:text-stone-600 border border-stone-200'
f' rounded px-2 py-1 transition-colors"'
f' "Clear component cache")')
def _full_wire_text(sx_src: str, *comp_names: str) -> str:
"""Build the full wire response text showing component defs + CSS note + sx source.
Only includes component definitions the client doesn't already have,
matching the real behaviour of sx_response().
"""
from quart import request
parts = []
if comp_names:
# Check which components the client already has
loaded_raw = request.headers.get("SX-Components", "")
loaded = set(loaded_raw.split(",")) if loaded_raw else set()
missing = [n for n in comp_names
if f"~{n}" not in loaded and n not in loaded]
if missing:
comp_text = _component_source_text(*missing)
if comp_text:
parts.append(f'<script type="text/sx" data-components>\n{comp_text}\n</script>')
parts.append('<style data-sx-css>/* new CSS rules */</style>')
# Pretty-print the sx source for readable display
try:
from shared.sx.parser import parse as _parse, serialize as _serialize
parts.append(_serialize(_parse(sx_src), pretty=True))
except Exception:
parts.append(sx_src)
return "\n\n".join(parts)
# ---------------------------------------------------------------------------
# Navigation helpers
# ---------------------------------------------------------------------------
async def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> str:
"""Build nav link items as sx."""
parts = []
for label, href in items:
parts.append(await render_to_sx("nav-link",
href=href, label=label,
is_selected="true" if current == label else None,
select_colours="aria-selected:bg-violet-200 aria-selected:text-violet-900",
))
return "(<> " + " ".join(parts) + ")"
async def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
"""Build the sx docs menu-row."""
return await render_to_sx("menu-row-sx",
id="sx-row", level=1, colour="violet",
link_href="/", link_label="sx",
link_label_content=SxExpr('(span :class "font-mono" "(</>) sx")'),
nav=SxExpr(nav) if nav else None,
child_id="sx-header-child",
child=SxExpr(child) if child else None,
)
async def _docs_nav_sx(current: str | None = None) -> str:
from content.pages import DOCS_NAV
return await _nav_items_sx(DOCS_NAV, current)
async def _reference_nav_sx(current: str | None = None) -> str:
from content.pages import REFERENCE_NAV
return await _nav_items_sx(REFERENCE_NAV, current)
async def _protocols_nav_sx(current: str | None = None) -> str:
from content.pages import PROTOCOLS_NAV
return await _nav_items_sx(PROTOCOLS_NAV, current)
async def _examples_nav_sx(current: str | None = None) -> str:
from content.pages import EXAMPLES_NAV
return await _nav_items_sx(EXAMPLES_NAV, current)
async def _essays_nav_sx(current: str | None = None) -> str:
from content.pages import ESSAYS_NAV
return await _nav_items_sx(ESSAYS_NAV, current)
async def _main_nav_sx(current_section: str | None = None) -> str:
from content.pages import MAIN_NAV
return await _nav_items_sx(MAIN_NAV, current_section)
async def _sub_row_sx(sub_label: str, sub_href: str, sub_nav: str,
selected: str = "") -> str:
"""Build the level-2 sub-section menu-row."""
return await render_to_sx("menu-row-sx",
id="sx-sub-row", level=2, colour="violet",
link_href=sub_href, link_label=sub_label,
selected=selected or None,
nav=SxExpr(sub_nav),
)
# ---------------------------------------------------------------------------
# Content builders — return sx source strings
# ---------------------------------------------------------------------------
async def _doc_nav_sx(items: list[tuple[str, str]], current: str) -> str:
"""Build the in-page doc navigation pills."""
items_sx = " ".join(
f'(list "{label}" "{href}")'
for label, href in items
)
return await render_to_sx("doc-nav", items=SxExpr(f"(list {items_sx})"), current=current)
async def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
"""Build an attribute reference table."""
from content.pages import ATTR_DETAILS
rows = []
for attr, desc, exists in attrs:
href = f"/reference/attributes/{attr}" if exists and attr in ATTR_DETAILS else None
rows.append(await render_to_sx("doc-attr-row", attr=attr, description=desc,
exists="true" if exists else None,
href=href))
return (
f'(div :class "space-y-3"'
f' (h3 :class "text-xl font-semibold text-stone-700" "{title}")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Attribute")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")'
f' (th :class "px-3 py-2 font-medium text-stone-600 text-center w-20" "In sx?")))'
f' (tbody {" ".join(rows)}))))'
)
async def _primitives_section_sx() -> str:
"""Build the primitives section."""
from content.pages import PRIMITIVES
parts = []
for category, prims in PRIMITIVES.items():
prims_sx = " ".join(f'"{p}"' for p in prims)
parts.append(await render_to_sx("doc-primitives-table",
category=category,
primitives=SxExpr(f"(list {prims_sx})")))
return " ".join(parts)
def _headers_table_sx(title: str, headers: list[tuple[str, str, str]]) -> str:
"""Build a headers reference table."""
rows = []
for name, value, desc in headers:
rows.append(
f'(tr :class "border-b border-stone-100"'
f' (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" "{name}")'
f' (td :class "px-3 py-2 font-mono text-sm text-stone-500" "{value}")'
f' (td :class "px-3 py-2 text-stone-700 text-sm" "{desc}"))'
)
return (
f'(div :class "space-y-3"'
f' (h3 :class "text-xl font-semibold text-stone-700" "{title}")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Header")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Value")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")))'
f' (tbody {" ".join(rows)}))))'
)
async def _docs_content_sx(slug: str) -> str:
"""Route to the right docs content builder."""
import inspect
builders = {
"introduction": _docs_introduction_sx,
"getting-started": _docs_getting_started_sx,
"components": _docs_components_sx,
"evaluator": _docs_evaluator_sx,
"primitives": _docs_primitives_sx,
"css": _docs_css_sx,
"server-rendering": _docs_server_rendering_sx,
}
builder = builders.get(slug, _docs_introduction_sx)
result = builder()
return await result if inspect.isawaitable(result) else result
def _docs_introduction_sx() -> str:
return (
'(~doc-page :title "Introduction"'
' (~doc-section :title "What is sx?" :id "what"'
' (p :class "text-stone-600"'
' "sx is an s-expression language for building web UIs. '
'It combines htmx\'s server-first hypermedia approach with React\'s component model. '
'The server sends s-expression source code over the wire. The client parses, evaluates, and renders it to DOM.")'
' (p :class "text-stone-600"'
' "The same evaluator runs on both server (Python) and client (JavaScript). '
'Components defined once render identically in both environments."))'
' (~doc-section :title "Design decisions" :id "design"'
' (p :class "text-stone-600"'
' "HTML elements are first-class: (div :class \\"card\\" (p \\"hello\\")) renders exactly what you\'d expect. '
'Components use defcomp with keyword parameters and optional children. '
'The evaluator supports let bindings, conditionals, lambda, map/filter/reduce, and ~80 primitives.")'
' (p :class "text-stone-600"'
' "sx replaces the pattern of '
'shipping a JS framework + build step + client-side router + state management library '
'just to render some server data. For most applications, sx eliminates the need for '
'JavaScript entirely — htmx attributes handle interactivity, hyperscript handles small behaviours, '
'and the server handles everything else."))'
' (~doc-section :title "What sx is not" :id "not"'
' (ul :class "space-y-2 text-stone-600"'
' (li "Not a general-purpose programming language — it\'s a UI rendering language")'
' (li "Not a full Lisp — it has macros and TCO, but no continuations or call/cc")'
' (li "Not production-hardened at scale — it runs one website"))))'
)
def _docs_getting_started_sx() -> str:
c1 = _code('(div :class "p-4 bg-white rounded"\n (h1 :class "text-2xl font-bold" "Hello, world!")\n (p "This is rendered from an s-expression."))')
c2 = _code('(button\n :sx-get "/api/data"\n :sx-target "#result"\n :sx-swap "innerHTML"\n "Load data")')
return (
f'(~doc-page :title "Getting Started"'
f' (~doc-section :title "Minimal example" :id "minimal"'
f' (p :class "text-stone-600"'
f' "An sx response is s-expression source code with content type text/sx:")'
f' {c1}'
f' (p :class "text-stone-600"'
f' "Add sx-get to any element to make it fetch and render sx:"))'
f' (~doc-section :title "Hypermedia attributes" :id "attrs"'
f' (p :class "text-stone-600"'
f' "Like htmx, sx adds attributes to HTML elements to trigger HTTP requests:")'
f' {c2}'
f' (p :class "text-stone-600"'
f' "sx-get, sx-post, sx-put, sx-delete, sx-patch — all work the same way. '
f'The response is parsed as sx and rendered into the target element.")))'
)
def _docs_components_sx() -> str:
c1 = _code('(defcomp ~card (&key title subtitle &rest children)\n'
' (div :class "border rounded p-4"\n'
' (h2 :class "font-bold" title)\n'
' (when subtitle (p :class "text-stone-500" subtitle))\n'
' (div :class "mt-3" children)))')
c2 = _code('(~card :title "My Card" :subtitle "A description"\n'
' (p "First child")\n'
' (p "Second child"))')
return (
f'(~doc-page :title "Components"'
f' (~doc-section :title "defcomp" :id "defcomp"'
f' (p :class "text-stone-600"'
f' "Components are defined with defcomp. They take keyword parameters and optional children:")'
f' {c1}'
f' (p :class "text-stone-600"'
f' "Use components with the ~ prefix:")'
f' {c2})'
f' (~doc-section :title "Component caching" :id "caching"'
f' (p :class "text-stone-600"'
f' "Component definitions are sent in a <script type=\\"text/sx\\" data-components> block. '
f'The client caches them in localStorage keyed by a content hash. '
f'On subsequent page loads, the client sends an SX-Components header listing what it has. '
f'The server only sends definitions the client is missing.")'
f' (p :class "text-stone-600"'
f' "This means the first page load sends all component definitions (~5-15KB). '
f'Subsequent navigations send zero component bytes — just the page content."))'
f' (~doc-section :title "Parameters" :id "params"'
f' (p :class "text-stone-600"'
f' "&key declares keyword parameters. &rest children captures remaining positional arguments. '
f'Missing parameters evaluate to nil. Components always receive all declared parameters — '
f'use (when param ...) or (if param ... ...) to handle optional values.")))'
)
def _docs_evaluator_sx() -> str:
c1 = _code(';; Conditionals\n'
'(if condition then-expr else-expr)\n'
'(when condition body...)\n'
'(cond (test1 body1) (test2 body2) (else default))\n\n'
';; Bindings\n'
'(let ((name value) (name2 value2)) body...)\n'
'(define name value)\n\n'
';; Functions\n'
'(lambda (x y) (+ x y))\n'
'(fn (x) (* x x))\n\n'
';; Sequencing\n'
'(do expr1 expr2 expr3)\n'
'(begin expr1 expr2)\n\n'
';; Threading\n'
'(-> value (fn1 arg) (fn2 arg))')
c2 = _code('(map (fn (x) (* x 2)) (list 1 2 3)) ;; => (2 4 6)\n'
'(filter (fn (x) (> x 2)) (list 1 2 3 4 5)) ;; => (3 4 5)\n'
'(reduce (fn (acc x) (+ acc x)) 0 (list 1 2 3)) ;; => 6\n'
'(some (fn (x) (> x 3)) (list 1 2 3 4)) ;; => true\n'
'(every? (fn (x) (> x 0)) (list 1 2 3)) ;; => true')
return (
f'(~doc-page :title "Evaluator"'
f' (~doc-section :title "Special forms" :id "special"'
f' (p :class "text-stone-600"'
f' "Special forms have lazy evaluation — arguments are not evaluated before the form runs:")'
f' {c1})'
f' (~doc-section :title "Higher-order forms" :id "higher"'
f' (p :class "text-stone-600"'
f' "These operate on collections with function arguments:")'
f' {c2}))'
)
async def _docs_primitives_sx() -> str:
prims = await _primitives_section_sx()
return (
f'(~doc-page :title "Primitives"'
f' (~doc-section :title "Built-in functions" :id "builtins"'
f' (p :class "text-stone-600"'
f' "sx provides ~80 built-in pure functions. '
f'They work identically on server (Python) and client (JavaScript).")'
f' (div :class "space-y-6" {prims})))'
)
def _docs_css_sx() -> str:
c1 = _code('# First page load:\n'
'GET / HTTP/1.1\n\n'
'HTTP/1.1 200 OK\n'
'Content-Type: text/html\n'
'# Full CSS in <style id="sx-css"> + hash in <meta name="sx-css-classes">\n\n'
'# Subsequent navigation:\n'
'GET /about HTTP/1.1\n'
'SX-Css: a1b2c3d4\n\n'
'HTTP/1.1 200 OK\n'
'Content-Type: text/sx\n'
'SX-Css-Hash: e5f6g7h8\n'
'SX-Css-Add: bg-blue-500,text-white,rounded-lg\n'
'# Only new rules in <style data-sx-css>', "bash")
return (
f'(~doc-page :title "On-Demand CSS"'
f' (~doc-section :title "How it works" :id "how"'
f' (p :class "text-stone-600"'
f' "sx scans every response for CSS class names used in :class attributes. '
f'It looks up only those classes in a pre-parsed Tailwind CSS registry and ships '
f'just the rules that are needed. No build step. No purging. No unused CSS.")'
f' (p :class "text-stone-600"'
f' "On the first page load, the full set of used classes is embedded in a <style> block. '
f'A hash of the class set is stored. On subsequent navigations, the client sends the hash '
f'in the SX-Css header. The server computes the diff and sends only new rules via '
f'SX-Css-Add and a <style data-sx-css> block."))'
f' (~doc-section :title "The protocol" :id "protocol"'
f' {c1})'
f' (~doc-section :title "Advantages" :id "advantages"'
f' (ul :class "space-y-2 text-stone-600"'
f' (li "Zero build step — no Tailwind CLI, no PostCSS, no purging")'
f' (li "Exact CSS — never ships a rule that isn\'t used on the page")'
f' (li "Incremental — subsequent navigations only ship new rules")'
f' (li "Component-aware — pre-scans component definitions at registration time")))'
f' (~doc-section :title "Disadvantages" :id "disadvantages"'
f' (ul :class "space-y-2 text-stone-600"'
f' (li "Requires the full Tailwind CSS file loaded in memory at startup (~4MB parsed)")'
f' (li "Regex-based class scanning — can miss dynamically constructed class names")'
f' (li "No @apply support — classes must be used directly")'
f' (li "Tied to Tailwind\'s utility class naming conventions"))))'
)
def _docs_server_rendering_sx() -> str:
c1 = _code('from shared.sx.helpers import sx_page, sx_response, sx_call\n'
'from shared.sx.parser import SxExpr\n\n'
'# Build a component call from Python kwargs\n'
'sx_call("card", title="Hello", subtitle="World")\n\n'
'# Return an sx wire-format response\n'
'return sx_response(sx_call("card", title="Hello"))\n\n'
'# Return a full HTML page shell with sx boot\n'
'return sx_page(ctx, page_sx)', "python")
return (
f'(~doc-page :title "Server Rendering"'
f' (~doc-section :title "Python API" :id "python"'
f' (p :class "text-stone-600"'
f' "The server-side sx library provides several entry points for rendering:")'
f' {c1})'
f' (~doc-section :title "sx_call" :id "sx-call"'
f' (p :class "text-stone-600"'
f' "sx_call converts Python kwargs to an s-expression component call. '
f'Snake_case becomes kebab-case. SxExpr values are inlined without quoting. '
f'None becomes nil. Bools become true/false."))'
f' (~doc-section :title "sx_response" :id "sx-response"'
f' (p :class "text-stone-600"'
f' "sx_response returns a Quart Response with content type text/sx. '
f'It prepends missing component definitions, scans for CSS classes, '
f'and sets SX-Css-Hash and SX-Css-Add headers."))'
f' (~doc-section :title "sx_page" :id "sx-page"'
f' (p :class "text-stone-600"'
f' "sx_page returns a minimal HTML document that boots the page from sx source. '
f'The browser loads component definitions and page sx from inline <script> tags, '
f'then sx.js renders everything client-side. CSS rules are pre-scanned and injected.")))'
)
# ---------------------------------------------------------------------------
# Reference pages
# ---------------------------------------------------------------------------
async def _reference_content_sx(slug: str) -> str:
import inspect
builders = {
"attributes": _reference_attrs_sx,
"headers": _reference_headers_sx,
"events": _reference_events_sx,
"js-api": _reference_js_api_sx,
}
result = builders.get(slug or "", _reference_attrs_sx)()
return await result if inspect.isawaitable(result) else result
def _reference_index_sx() -> str:
"""Build the reference index page with links to sub-sections."""
sections = [
("Attributes", "/reference/attributes",
"All sx attributes — request verbs, behavior modifiers, and sx-unique features."),
("Headers", "/reference/headers",
"Custom HTTP headers used to coordinate between the sx client and server."),
("Events", "/reference/events",
"DOM events fired during the sx request lifecycle."),
("JS API", "/reference/js-api",
"JavaScript functions for parsing, evaluating, and rendering s-expressions."),
]
cards = []
for label, href, desc in sections:
cards.append(
f'(a :href "{href}"'
f' :sx-get "{href}" :sx-target "#main-panel" :sx-select "#main-panel"'
f' :sx-swap "outerHTML" :sx-push-url "true"'
f' :class "block p-5 rounded-lg border border-stone-200 hover:border-violet-300'
f' hover:shadow-sm transition-all no-underline"'
f' (h3 :class "text-lg font-semibold text-violet-700 mb-1" "{label}")'
f' (p :class "text-stone-600 text-sm" "{desc}"))'
)
return (
f'(~doc-page :title "Reference"'
f' (p :class "text-stone-600 mb-6"'
f' "Complete reference for the sx client library.")'
f' (div :class "grid gap-4 sm:grid-cols-2"'
f' {" ".join(cards)}))'
)
def _reference_attr_detail_sx(slug: str) -> str:
"""Build a detail page for a single sx attribute."""
from content.pages import ATTR_DETAILS
detail = ATTR_DETAILS.get(slug)
if not detail:
return (
f'(~doc-page :title "Not Found"'
f' (p :class "text-stone-600"'
f' "No documentation found for \\"{slug}\\"."))'
)
title = slug
desc = detail["description"]
escaped_desc = desc.replace('\\', '\\\\').replace('"', '\\"')
# Live demo
demo_name = detail.get("demo")
demo_sx = ""
if demo_name:
demo_sx = (
f' (~example-card :title "Demo"'
f' (~example-demo (~{demo_name})))'
)
# S-expression source
example_sx = _example_code(detail["example"], "lisp")
# Server handler (s-expression)
handler_sx = ""
if "handler" in detail:
handler_sx = (
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6"'
f' "Server handler")'
f' {_example_code(detail["handler"], "lisp")}'
)
# Wire response placeholder (only for attrs with server interaction)
wire_sx = ""
if "handler" in detail:
wire_id = slug.replace(":", "-").replace("*", "star")
wire_sx = (
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6"'
f' "Wire response")'
f' (p :class "text-stone-500 text-sm mb-2"'
f' "Trigger the demo to see the raw response the server sends.")'
f' {_placeholder("ref-wire-" + wire_id)}'
)
return (
f'(~doc-page :title "{title}"'
f' (p :class "text-stone-600 mb-6" "{escaped_desc}")'
f' {demo_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6"'
f' "S-expression")'
f' {example_sx}'
f' {handler_sx}'
f' {wire_sx})'
)
async def _reference_attrs_sx() -> str:
from content.pages import REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS, HTMX_MISSING_ATTRS
req = await _attr_table_sx("Request Attributes", REQUEST_ATTRS)
beh = await _attr_table_sx("Behavior Attributes", BEHAVIOR_ATTRS)
uniq = await _attr_table_sx("Unique to sx", SX_UNIQUE_ATTRS)
missing = await _attr_table_sx("htmx features not yet in sx", HTMX_MISSING_ATTRS)
return (
f'(~doc-page :title "Attribute Reference"'
f' (p :class "text-stone-600 mb-6"'
f' "sx attributes mirror htmx where possible. This table shows what exists, '
f'what\'s unique to sx, and what\'s not yet implemented.")'
f' (div :class "space-y-8"'
f' {req}'
f' {beh}'
f' {uniq}'
f' {missing}))'
)
def _reference_headers_sx() -> str:
from content.pages import REQUEST_HEADERS, RESPONSE_HEADERS
return (
f'(~doc-page :title "Headers"'
f' (p :class "text-stone-600 mb-6"'
f' "sx uses custom HTTP headers to coordinate between client and server.")'
f' (div :class "space-y-8"'
f' {_headers_table_sx("Request Headers", REQUEST_HEADERS)}'
f' {_headers_table_sx("Response Headers", RESPONSE_HEADERS)}))'
)
def _reference_events_sx() -> str:
from content.pages import EVENTS
rows = []
for name, desc in EVENTS:
rows.append(
f'(tr :class "border-b border-stone-100"'
f' (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" "{name}")'
f' (td :class "px-3 py-2 text-stone-700 text-sm" "{desc}"))'
)
return (
f'(~doc-page :title "Events"'
f' (p :class "text-stone-600 mb-6"'
f' "sx fires custom DOM events at various points in the request lifecycle.")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Event")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")))'
f' (tbody {" ".join(rows)}))))'
)
def _reference_js_api_sx() -> str:
from content.pages import JS_API
rows = []
for name, desc in JS_API:
rows.append(
f'(tr :class "border-b border-stone-100"'
f' (td :class "px-3 py-2 font-mono text-sm text-violet-700 whitespace-nowrap" "{name}")'
f' (td :class "px-3 py-2 text-stone-700 text-sm" "{desc}"))'
)
return (
f'(~doc-page :title "JavaScript API"'
f' (p :class "text-stone-600 mb-6"'
f' "The client-side sx.js library exposes a public API for programmatic use.")'
f' (div :class "overflow-x-auto rounded border border-stone-200"'
f' (table :class "w-full text-left text-sm"'
f' (thead (tr :class "border-b border-stone-200 bg-stone-50"'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Method")'
f' (th :class "px-3 py-2 font-medium text-stone-600" "Description")))'
f' (tbody {" ".join(rows)}))))'
)
# ---------------------------------------------------------------------------
# Protocol pages
# ---------------------------------------------------------------------------
def _protocol_content_sx(slug: str) -> str:
builders = {
"wire-format": _protocol_wire_format_sx,
"fragments": _protocol_fragments_sx,
"resolver-io": _protocol_resolver_io_sx,
"internal-services": _protocol_internal_services_sx,
"activitypub": _protocol_activitypub_sx,
"future": _protocol_future_sx,
}
return builders.get(slug, _protocol_wire_format_sx)()
def _protocol_wire_format_sx() -> str:
c1 = _code('HTTP/1.1 200 OK\n'
'Content-Type: text/sx\n'
'SX-Css-Hash: a1b2c3d4\n\n'
'(div :class "p-4"\n'
' (~card :title "Hello"))', "bash")
return (
f'(~doc-page :title "Wire Format"'
f' (~doc-section :title "The text/sx content type" :id "content-type"'
f' (p :class "text-stone-600"'
f' "sx responses use content type text/sx. The body is s-expression source code. '
f'The client parses and evaluates it, then renders the result into the DOM.")'
f' {c1})'
' (~doc-section :title "Request lifecycle" :id "lifecycle"'
' (p :class "text-stone-600"'
' "1. User interacts with an element that has sx-get/sx-post/etc.")'
' (p :class "text-stone-600"'
' "2. sx.js fires sx:beforeRequest, then sends the HTTP request with SX-Request: true header.")'
' (p :class "text-stone-600"'
' "3. Server builds s-expression tree, scans CSS classes, prepends missing component definitions.")'
' (p :class "text-stone-600"'
' "4. Client receives text/sx response, parses it, evaluates it, renders to DOM.")'
' (p :class "text-stone-600"'
' "5. sx.js fires sx:afterSwap and sx:afterSettle.")'
' (p :class "text-stone-600"'
' "6. Any sx-swap-oob elements are swapped into their targets elsewhere in the DOM.")) '
' (~doc-section :title "Component definitions" :id "components"'
' (p :class "text-stone-600"'
' "On full page loads, component definitions are in <script type=\\"text/sx\\" data-components>. '
'On subsequent sx requests, missing definitions are prepended to the response body. '
'The client caches definitions in localStorage keyed by a content hash.")))'
)
def _protocol_fragments_sx() -> str:
c1 = _code('(frag "blog" "link-card" :slug "hello")')
return (
f'(~doc-page :title "Cross-Service Fragments"'
f' (~doc-section :title "Fragment protocol" :id "protocol"'
f' (p :class "text-stone-600"'
f' "Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments '
f'that other services compose into their pages. Fragment endpoints return text/sx or text/html.")'
f' (p :class "text-stone-600"'
f' "The frag resolver is an I/O primitive in the render tree:")'
f' {c1})'
' (~doc-section :title "SxExpr wrapping" :id "wrapping"'
' (p :class "text-stone-600"'
' "When a fragment returns text/sx, the response is wrapped in an SxExpr and embedded directly '
'in the render tree. When it returns text/html, it\'s wrapped in a ~rich-text component that '
'inserts the HTML via raw!. This allows transparent composition across service boundaries."))'
' (~doc-section :title "fetch_fragments()" :id "fetch"'
' (p :class "text-stone-600"'
' "The Python helper fetch_fragments() fetches multiple fragments in parallel via asyncio.gather(). '
'Fragments are cached in Redis with short TTLs. Each fragment request is HMAC-signed for authentication.")))'
)
def _protocol_resolver_io_sx() -> str:
return (
'(~doc-page :title "Resolver I/O"'
' (~doc-section :title "Async I/O primitives" :id "primitives"'
' (p :class "text-stone-600"'
' "The sx resolver identifies I/O nodes in the render tree, groups them, '
'executes them in parallel via asyncio.gather(), and substitutes results back in.")'
' (p :class "text-stone-600"'
' "I/O primitives:")'
' (ul :class "space-y-2 text-stone-600"'
' (li (span :class "font-mono text-violet-700" "frag") " — fetch a cross-service fragment")'
' (li (span :class "font-mono text-violet-700" "query") " — read data from another service")'
' (li (span :class "font-mono text-violet-700" "action") " — execute a write on another service")'
' (li (span :class "font-mono text-violet-700" "current-user") " — resolve the current authenticated user")))'
' (~doc-section :title "Execution model" :id "execution"'
' (p :class "text-stone-600"'
' "The render tree is walked to find I/O nodes. All nodes at the same depth are gathered '
'and executed in parallel. Results replace the I/O nodes in the tree. The walk continues '
'until no more I/O nodes are found. This typically completes in 1-2 passes.")))'
)
def _protocol_internal_services_sx() -> str:
return (
'(~doc-page :title "Internal Services"'
' (~doc-note'
' (p "Honest note: the internal service protocol is JSON, not sx. '
'Sx is the composition layer on top. The protocols below are the plumbing underneath."))'
' (~doc-section :title "HMAC-signed HTTP" :id "hmac"'
' (p :class "text-stone-600"'
' "Services communicate via HMAC-signed HTTP requests with short timeouts:")'
' (ul :class "space-y-2 text-stone-600 font-mono text-sm"'
' (li "GET /internal/data/{query} — read data (3s timeout)")'
' (li "POST /internal/actions/{action} — execute write (5s timeout)")'
' (li "POST /internal/inbox — ActivityPub-shaped event delivery")))'
' (~doc-section :title "fetch_data / call_action" :id "fetch"'
' (p :class "text-stone-600"'
' "Python helpers fetch_data() and call_action() handle HMAC signing, '
'serialization, and error handling. They resolve service URLs from environment variables '
'(INTERNAL_URL_BLOG, etc) and fall back to public URLs in development.")))'
)
def _protocol_activitypub_sx() -> str:
return (
'(~doc-page :title "ActivityPub"'
' (~doc-note'
' (p "Honest note: ActivityPub wire format is JSON-LD, not sx. '
'This documents how AP integrates with the sx rendering layer."))'
' (~doc-section :title "AP activities" :id "activities"'
' (p :class "text-stone-600"'
' "Rose Ash services communicate cross-domain writes via ActivityPub-shaped activities. '
'Each service has a virtual actor. Activities are JSON-LD objects sent to /internal/inbox endpoints. '
'RSA signatures authenticate the sender."))'
' (~doc-section :title "Event bus" :id "bus"'
' (p :class "text-stone-600"'
' "The event bus dispatches activities to registered handlers. '
'Handlers are async functions that process the activity and may trigger side effects. '
'The bus runs as a background processor in each service.")))'
)
def _protocol_future_sx() -> str:
return (
'(~doc-page :title "Future Possibilities"'
' (~doc-note'
' (p "This page is speculative. Nothing here is implemented. '
'It documents ideas that may or may not happen."))'
' (~doc-section :title "Custom protocol schemes" :id "schemes"'
' (p :class "text-stone-600"'
' "sx:// and sxs:// as custom URI schemes for content addressing or deep linking. '
'An sx:// URI could resolve to an sx expression from a federated registry. '
'This is technically feasible but practically unnecessary for a single-site deployment."))'
' (~doc-section :title "Sx as AP serialization" :id "ap-sx"'
' (p :class "text-stone-600"'
' "ActivityPub objects could be serialized as s-expressions instead of JSON-LD. '
'S-expressions are more compact and easier to parse. '
'The practical barrier: the entire AP ecosystem expects JSON-LD."))'
' (~doc-section :title "Sx-native federation" :id "federation"'
' (p :class "text-stone-600"'
' "Federated services could exchange sx fragments directly — render a remote user\'s '
'profile card by fetching its sx source from their server. '
'This requires trust and standardization that doesn\'t exist yet."))'
' (~doc-section :title "Realistic assessment" :id "realistic"'
' (p :class "text-stone-600"'
' "The most likely near-term improvement is sx:// deep linking for client-side component '
'resolution. Everything else requires ecosystem adoption that one project can\'t drive alone.")))'
)
# ---------------------------------------------------------------------------
# Examples pages
# ---------------------------------------------------------------------------
def _examples_content_sx(slug: str) -> str:
builders = {
"click-to-load": _example_click_to_load_sx,
"form-submission": _example_form_submission_sx,
"polling": _example_polling_sx,
"delete-row": _example_delete_row_sx,
"inline-edit": _example_inline_edit_sx,
"oob-swaps": _example_oob_swaps_sx,
"lazy-loading": _example_lazy_loading_sx,
"infinite-scroll": _example_infinite_scroll_sx,
"progress-bar": _example_progress_bar_sx,
"active-search": _example_active_search_sx,
"inline-validation": _example_inline_validation_sx,
"value-select": _example_value_select_sx,
"reset-on-submit": _example_reset_on_submit_sx,
"edit-row": _example_edit_row_sx,
"bulk-update": _example_bulk_update_sx,
"swap-positions": _example_swap_positions_sx,
"select-filter": _example_select_filter_sx,
"tabs": _example_tabs_sx,
"animations": _example_animations_sx,
"dialogs": _example_dialogs_sx,
"keyboard-shortcuts": _example_keyboard_shortcuts_sx,
"put-patch": _example_put_patch_sx,
"json-encoding": _example_json_encoding_sx,
"vals-and-headers": _example_vals_and_headers_sx,
"loading-states": _example_loading_states_sx,
"sync-replace": _example_sync_replace_sx,
"retry": _example_retry_sx,
}
return builders.get(slug, _example_click_to_load_sx)()
def _example_click_to_load_sx() -> str:
c_sx = _example_code('(button\n'
' :sx-get "/examples/api/click"\n'
' :sx-target "#click-result"\n'
' :sx-swap "innerHTML"\n'
' "Load content")')
c_handler = _example_code('@bp.get("/examples/api/click")\n'
'async def api_click():\n'
' now = datetime.now().strftime(...)\n'
' return sx_response(\n'
' f\'(~click-result :time "{now}")\')',
language="python")
return (
f'(~doc-page :title "Click to Load"'
f' (p :class "text-stone-600 mb-6"'
f' "The simplest sx interaction: click a button, fetch content from the server, swap it in.")'
f' (~example-card :title "Demo"'
f' :description "Click the button to load server-rendered content."'
f' (~example-demo (~click-to-load-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("click-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' (p :class "text-stone-500 text-sm mb-2"'
f' "The server responds with content-type text/sx. New CSS rules are prepended as a style tag.'
f' Clear the component cache to see component definitions included in the wire response.")'
f' {_placeholder("click-wire")})'
)
def _example_form_submission_sx() -> str:
c_sx = _example_code('(form\n'
' :sx-post "/examples/api/form"\n'
' :sx-target "#form-result"\n'
' :sx-swap "innerHTML"\n'
' (input :type "text" :name "name")\n'
' (button :type "submit" "Submit"))')
c_handler = _example_code('@bp.post("/examples/api/form")\n'
'async def api_form():\n'
' form = await request.form\n'
' name = form.get("name", "")\n'
' return sx_response(\n'
' f\'(~form-result :name "{name}")\')',
language="python")
return (
f'(~doc-page :title "Form Submission"'
f' (p :class "text-stone-600 mb-6"'
f' "Forms with sx-post submit via AJAX and swap the response into a target.")'
f' (~example-card :title "Demo"'
f' :description "Enter a name and submit."'
f' (~example-demo (~form-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("form-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("form-wire")})'
)
def _example_polling_sx() -> str:
c_sx = _example_code('(div\n'
' :sx-get "/examples/api/poll"\n'
' :sx-trigger "load, every 2s"\n'
' :sx-swap "innerHTML"\n'
' "Loading...")')
c_handler = _example_code('@bp.get("/examples/api/poll")\n'
'async def api_poll():\n'
' poll_count["n"] += 1\n'
' now = datetime.now().strftime("%H:%M:%S")\n'
' count = min(poll_count["n"], 10)\n'
' return sx_response(\n'
' f\'(~poll-result :time "{now}" :count {count})\')',
language="python")
return (
f'(~doc-page :title "Polling"'
f' (p :class "text-stone-600 mb-6"'
f' "Use sx-trigger with \\"every\\" to poll the server at regular intervals.")'
f' (~example-card :title "Demo"'
f' :description "This div polls the server every 2 seconds."'
f' (~example-demo (~polling-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("poll-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' (p :class "text-stone-500 text-sm mb-2"'
f' "Updates every 2 seconds — watch the time and count change.")'
f' {_placeholder("poll-wire")})'
)
def _example_delete_row_sx() -> str:
from content.pages import DELETE_DEMO_ITEMS
items_sx = " ".join(f'(list "{id}" "{name}")' for id, name in DELETE_DEMO_ITEMS)
c_sx = _example_code('(button\n'
' :sx-delete "/api/delete/1"\n'
' :sx-target "#row-1"\n'
' :sx-swap "outerHTML"\n'
' :sx-confirm "Delete this item?"\n'
' "delete")')
c_handler = _example_code('@bp.delete("/examples/api/delete/<item_id>")\n'
'async def api_delete(item_id: str):\n'
' # Empty response — outerHTML swap removes the row\n'
' return Response("", status=200,\n'
' content_type="text/sx")',
language="python")
return (
f'(~doc-page :title "Delete Row"'
f' (p :class "text-stone-600 mb-6"'
f' "sx-delete with sx-swap \\"outerHTML\\" and an empty response removes the row from the DOM.")'
f' (~example-card :title "Demo"'
f' :description "Click delete to remove a row. Uses sx-confirm for confirmation."'
f' (~example-demo (~delete-demo :items (list {items_sx}))))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("delete-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' (p :class "text-stone-500 text-sm mb-2"'
f' "Empty body — outerHTML swap replaces the target element with nothing.")'
f' {_placeholder("delete-wire")})'
)
def _example_inline_edit_sx() -> str:
c_sx = _example_code(';; View mode — shows text + edit button\n'
'(~inline-view :value "some text")\n\n'
';; Edit mode — returned by server on click\n'
'(~inline-edit-form :value "some text")')
c_handler = _example_code('@bp.get("/examples/api/edit")\n'
'async def api_edit_form():\n'
' value = request.args.get("value", "")\n'
' return sx_response(\n'
' f\'(~inline-edit-form :value "{value}")\')\n\n'
'@bp.post("/examples/api/edit")\n'
'async def api_edit_save():\n'
' form = await request.form\n'
' value = form.get("value", "")\n'
' return sx_response(\n'
' f\'(~inline-view :value "{value}")\')',
language="python")
return (
f'(~doc-page :title "Inline Edit"'
f' (p :class "text-stone-600 mb-6"'
f' "Click edit to swap a display view for an edit form. Save swaps back.")'
f' (~example-card :title "Demo"'
f' :description "Click edit, modify the text, save or cancel."'
f' (~example-demo (~inline-edit-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Components")'
f' {_placeholder("edit-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handlers")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("edit-wire")})'
)
def _example_oob_swaps_sx() -> str:
c_sx = _example_code(';; Button targets Box A\n'
'(button\n'
' :sx-get "/examples/api/oob"\n'
' :sx-target "#oob-box-a"\n'
' :sx-swap "innerHTML"\n'
' "Update both boxes")')
c_handler = _example_code('@bp.get("/examples/api/oob")\n'
'async def api_oob():\n'
' now = datetime.now().strftime("%H:%M:%S")\n'
' return sx_response(\n'
' f\'(<>\'\n'
' f\' (p "Box A updated at {now}")\'\n'
' f\' (div :id "oob-box-b"\'\n'
' f\' :sx-swap-oob "innerHTML"\'\n'
' f\' (p "Box B updated at {now}")))\')',
language="python")
return (
f'(~doc-page :title "Out-of-Band Swaps"'
f' (p :class "text-stone-600 mb-6"'
f' "sx-swap-oob lets a single response update multiple elements anywhere in the DOM.")'
f' (~example-card :title "Demo"'
f' :description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."'
f' (~example-demo (~oob-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' (p :class "text-stone-500 text-sm mb-2"'
f' "The fragment contains both the main content and an OOB element. sx.js splits them: main content goes to sx-target, OOB elements find their targets by ID.")'
f' {_placeholder("oob-wire")})'
)
def _example_lazy_loading_sx() -> str:
c_sx = _example_code('(div\n'
' :sx-get "/examples/api/lazy"\n'
' :sx-trigger "load"\n'
' :sx-swap "innerHTML"\n'
' (div :class "animate-pulse" "Loading..."))')
c_handler = _example_code('@bp.get("/examples/api/lazy")\n'
'async def api_lazy():\n'
' now = datetime.now().strftime(...)\n'
' return sx_response(\n'
' f\'(~lazy-result :time "{now}")\')',
language="python")
return (
f'(~doc-page :title "Lazy Loading"'
f' (p :class "text-stone-600 mb-6"'
f' "Use sx-trigger=\\"load\\" to fetch content as soon as the element enters the DOM. '
f'Great for deferring expensive content below the fold.")'
f' (~example-card :title "Demo"'
f' :description "Content loads automatically when the page renders."'
f' (~example-demo (~lazy-loading-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("lazy-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("lazy-wire")})'
)
def _example_infinite_scroll_sx() -> str:
c_sx = _example_code('(div :id "scroll-sentinel"\n'
' :sx-get "/examples/api/scroll?page=2"\n'
' :sx-trigger "intersect once"\n'
' :sx-target "#scroll-items"\n'
' :sx-swap "beforeend"\n'
' "Loading more...")')
c_handler = _example_code('@bp.get("/examples/api/scroll")\n'
'async def api_scroll():\n'
' page = int(request.args.get("page", 2))\n'
' items = [f"Item {i}" for i in range(...)]\n'
' # Include next sentinel if more pages\n'
' return sx_response(items_sx + sentinel_sx)',
language="python")
return (
f'(~doc-page :title "Infinite Scroll"'
f' (p :class "text-stone-600 mb-6"'
f' "A sentinel element at the bottom uses sx-trigger=\\"intersect once\\" '
f'to load the next page when scrolled into view. Each response appends items and a new sentinel.")'
f' (~example-card :title "Demo"'
f' :description "Scroll down in the container to load more items (5 pages total)."'
f' (~example-demo (~infinite-scroll-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("scroll-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("scroll-wire")})'
)
def _example_progress_bar_sx() -> str:
c_sx = _example_code(';; Start the job\n'
'(button\n'
' :sx-post "/examples/api/progress/start"\n'
' :sx-target "#progress-target"\n'
' :sx-swap "innerHTML")\n\n'
';; Each response re-polls via sx-trigger="load"\n'
'(div :sx-get "/api/progress/status?job=ID"\n'
' :sx-trigger "load delay:500ms"\n'
' :sx-target "#progress-target"\n'
' :sx-swap "innerHTML")')
c_handler = _example_code('@bp.post("/examples/api/progress/start")\n'
'async def api_progress_start():\n'
' job_id = str(uuid4())[:8]\n'
' _jobs[job_id] = 0\n'
' return sx_response(\n'
' f\'(~progress-status :percent 0 :job-id "{job_id}")\')',
language="python")
return (
f'(~doc-page :title "Progress Bar"'
f' (p :class "text-stone-600 mb-6"'
f' "Start a server-side job, then poll for progress using sx-trigger=\\"load delay:500ms\\" on each response. '
f'The bar fills up and stops when complete.")'
f' (~example-card :title "Demo"'
f' :description "Click start to begin a simulated job."'
f' (~example-demo (~progress-bar-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("progress-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("progress-wire")})'
)
def _example_active_search_sx() -> str:
c_sx = _example_code('(input :type "text" :name "q"\n'
' :sx-get "/examples/api/search"\n'
' :sx-trigger "keyup delay:300ms changed"\n'
' :sx-target "#search-results"\n'
' :sx-swap "innerHTML"\n'
' :placeholder "Search...")')
c_handler = _example_code('@bp.get("/examples/api/search")\n'
'async def api_search():\n'
' q = request.args.get("q", "").lower()\n'
' results = [l for l in LANGUAGES if q in l.lower()]\n'
' return sx_response(\n'
' f\'(~search-results :items (...) :query "{q}")\')',
language="python")
return (
f'(~doc-page :title "Active Search"'
f' (p :class "text-stone-600 mb-6"'
f' "An input with sx-trigger=\\"keyup delay:300ms changed\\" debounces keystrokes and only fires when the value changes. '
f'The server filters a list of programming languages.")'
f' (~example-card :title "Demo"'
f' :description "Type to search through 20 programming languages."'
f' (~example-demo (~active-search-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("search-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("search-wire")})'
)
def _example_inline_validation_sx() -> str:
c_sx = _example_code('(input :type "text" :name "email"\n'
' :sx-get "/examples/api/validate"\n'
' :sx-trigger "blur"\n'
' :sx-target "#email-feedback"\n'
' :sx-swap "innerHTML"\n'
' :placeholder "user@example.com")')
c_handler = _example_code('@bp.get("/examples/api/validate")\n'
'async def api_validate():\n'
' email = request.args.get("email", "")\n'
' if "@" not in email:\n'
' return sx_response(\'(~validation-error ...)\')\n'
' return sx_response(\'(~validation-ok ...)\')',
language="python")
return (
f'(~doc-page :title "Inline Validation"'
f' (p :class "text-stone-600 mb-6"'
f' "Validate an email field on blur. The server checks format and whether it is taken, '
f'returning green or red feedback inline.")'
f' (~example-card :title "Demo"'
f' :description "Enter an email and click away (blur) to validate."'
f' (~example-demo (~inline-validation-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("validate-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("validate-wire")})'
)
def _example_value_select_sx() -> str:
c_sx = _example_code('(select :name "category"\n'
' :sx-get "/examples/api/values"\n'
' :sx-trigger "change"\n'
' :sx-target "#value-items"\n'
' :sx-swap "innerHTML"\n'
' (option "Languages")\n'
' (option "Frameworks")\n'
' (option "Databases"))')
c_handler = _example_code('@bp.get("/examples/api/values")\n'
'async def api_values():\n'
' cat = request.args.get("category", "")\n'
' items = VALUE_SELECT_DATA.get(cat, [])\n'
' return sx_response(\n'
' f\'(~value-options :items (list ...))\')',
language="python")
return (
f'(~doc-page :title "Value Select"'
f' (p :class "text-stone-600 mb-6"'
f' "Two linked selects: pick a category and the second select updates with matching items via sx-get.")'
f' (~example-card :title "Demo"'
f' :description "Select a category to populate the item dropdown."'
f' (~example-demo (~value-select-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("values-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("values-wire")})'
)
def _example_reset_on_submit_sx() -> str:
c_sx = _example_code('(form :id "reset-form"\n'
' :sx-post "/examples/api/reset-submit"\n'
' :sx-target "#reset-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-on:afterSwap "this.reset()"\n'
' (input :type "text" :name "message")\n'
' (button :type "submit" "Send"))')
c_handler = _example_code('@bp.post("/examples/api/reset-submit")\n'
'async def api_reset_submit():\n'
' form = await request.form\n'
' msg = form.get("message", "")\n'
' return sx_response(\n'
' f\'(~reset-message :message "{msg}" :time "...")\')',
language="python")
return (
f'(~doc-page :title "Reset on Submit"'
f' (p :class "text-stone-600 mb-6"'
f' "Use sx-on:afterSwap=\\"this.reset()\\" to clear form inputs after a successful submission.")'
f' (~example-card :title "Demo"'
f' :description "Submit a message — the input resets after each send."'
f' (~example-demo (~reset-on-submit-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("reset-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("reset-wire")})'
)
def _example_edit_row_sx() -> str:
from content.pages import EDIT_ROW_DATA
rows_sx = " ".join(
f'(list "{r["id"]}" "{r["name"]}" "{r["price"]}" "{r["stock"]}")'
for r in EDIT_ROW_DATA
)
c_sx = _example_code('(button\n'
' :sx-get "/examples/api/editrow/1"\n'
' :sx-target "#erow-1"\n'
' :sx-swap "outerHTML"\n'
' "edit")\n\n'
';; Save sends form data via POST\n'
'(button\n'
' :sx-post "/examples/api/editrow/1"\n'
' :sx-target "#erow-1"\n'
' :sx-swap "outerHTML"\n'
' :sx-include "#erow-1"\n'
' "save")')
c_handler = _example_code('@bp.get("/examples/api/editrow/<id>")\n'
'async def api_editrow_form(id):\n'
' row = EDIT_ROW_DATA[id]\n'
' return sx_response(\n'
' f\'(~edit-row-form :id ... :name ...)\')\n\n'
'@bp.post("/examples/api/editrow/<id>")\n'
'async def api_editrow_save(id):\n'
' form = await request.form\n'
' return sx_response(\n'
' f\'(~edit-row-view :id ... :name ...)\')',
language="python")
return (
f'(~doc-page :title "Edit Row"'
f' (p :class "text-stone-600 mb-6"'
f' "Click edit to replace a table row with input fields. Save or cancel swaps back the display row. '
f'Uses sx-include to gather form values from the row.")'
f' (~example-card :title "Demo"'
f' :description "Click edit on any row to modify it inline."'
f' (~example-demo (~edit-row-demo :rows (list {rows_sx}))))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("editrow-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("editrow-wire")})'
)
def _example_bulk_update_sx() -> str:
from content.pages import BULK_USERS
users_sx = " ".join(
f'(list "{u["id"]}" "{u["name"]}" "{u["email"]}" "{u["status"]}")'
for u in BULK_USERS
)
c_sx = _example_code('(button\n'
' :sx-post "/examples/api/bulk?action=activate"\n'
' :sx-target "#bulk-table"\n'
' :sx-swap "innerHTML"\n'
' :sx-include "#bulk-form"\n'
' "Activate")')
c_handler = _example_code('@bp.post("/examples/api/bulk")\n'
'async def api_bulk():\n'
' action = request.args.get("action")\n'
' form = await request.form\n'
' ids = form.getlist("ids")\n'
' # Update matching users\n'
' return sx_response(updated_rows)',
language="python")
return (
f'(~doc-page :title "Bulk Update"'
f' (p :class "text-stone-600 mb-6"'
f' "Select rows with checkboxes and use Activate/Deactivate buttons. '
f'sx-include gathers checkbox values from the form.")'
f' (~example-card :title "Demo"'
f' :description "Check some rows, then click Activate or Deactivate."'
f' (~example-demo (~bulk-update-demo :users (list {users_sx}))))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("bulk-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("bulk-wire")})'
)
def _example_swap_positions_sx() -> str:
c_sx = _example_code(';; Append to end\n'
'(button :sx-post "/api/swap-log?mode=beforeend"\n'
' :sx-target "#swap-log" :sx-swap "beforeend"\n'
' "Add to End")\n\n'
';; Prepend to start\n'
'(button :sx-post "/api/swap-log?mode=afterbegin"\n'
' :sx-target "#swap-log" :sx-swap "afterbegin"\n'
' "Add to Start")\n\n'
';; No swap — OOB counter update only\n'
'(button :sx-post "/api/swap-log?mode=none"\n'
' :sx-target "#swap-log" :sx-swap "none"\n'
' "Silent Ping")')
c_handler = _example_code('@bp.post("/examples/api/swap-log")\n'
'async def api_swap_log():\n'
' mode = request.args.get("mode")\n'
' # OOB counter updates on every request\n'
' oob = f\'(span :id "swap-counter" :sx-swap-oob "innerHTML" "Count: {n}")\'\n'
' return sx_response(entry + oob)',
language="python")
return (
f'(~doc-page :title "Swap Positions"'
f' (p :class "text-stone-600 mb-6"'
f' "Demonstrates different swap modes: beforeend appends, afterbegin prepends, '
f'and none skips the main swap while still processing OOB updates.")'
f' (~example-card :title "Demo"'
f' :description "Try each button to see different swap behaviours."'
f' (~example-demo (~swap-positions-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("swap-wire")})'
)
def _example_select_filter_sx() -> str:
c_sx = _example_code(';; Pick just the stats section from the response\n'
'(button\n'
' :sx-get "/examples/api/dashboard"\n'
' :sx-target "#filter-target"\n'
' :sx-swap "innerHTML"\n'
' :sx-select "#dash-stats"\n'
' "Stats Only")\n\n'
';; No sx-select — get the full response\n'
'(button\n'
' :sx-get "/examples/api/dashboard"\n'
' :sx-target "#filter-target"\n'
' :sx-swap "innerHTML"\n'
' "Full Dashboard")')
c_handler = _example_code('@bp.get("/examples/api/dashboard")\n'
'async def api_dashboard():\n'
' # Returns header + stats + footer\n'
' # Client uses sx-select to pick sections\n'
' return sx_response(\n'
' \'(<> (div :id "dash-header" ...) \'\n'
' \' (div :id "dash-stats" ...) \'\n'
' \' (div :id "dash-footer" ...))\')',
language="python")
return (
f'(~doc-page :title "Select Filter"'
f' (p :class "text-stone-600 mb-6"'
f' "sx-select lets the client pick a specific section from the server response by CSS selector. '
f'The server always returns the full dashboard — the client filters.")'
f' (~example-card :title "Demo"'
f' :description "Different buttons select different parts of the same server response."'
f' (~example-demo (~select-filter-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("filter-wire")})'
)
def _example_tabs_sx() -> str:
c_sx = _example_code('(button\n'
' :sx-get "/examples/api/tabs/tab1"\n'
' :sx-target "#tab-content"\n'
' :sx-swap "innerHTML"\n'
' :sx-push-url "/examples/tabs?tab=tab1"\n'
' "Overview")')
c_handler = _example_code('@bp.get("/examples/api/tabs/<tab>")\n'
'async def api_tabs(tab: str):\n'
' content = TAB_CONTENT[tab]\n'
' return sx_response(content)',
language="python")
return (
f'(~doc-page :title "Tabs"'
f' (p :class "text-stone-600 mb-6"'
f' "Tab navigation using sx-push-url to update the browser URL. '
f'Back/forward buttons navigate between previously visited tabs.")'
f' (~example-card :title "Demo"'
f' :description "Click tabs to switch content. Watch the browser URL change."'
f' (~example-demo (~tabs-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("tabs-wire")})'
)
def _example_animations_sx() -> str:
c_sx = _example_code('(button\n'
' :sx-get "/examples/api/animate"\n'
' :sx-target "#anim-target"\n'
' :sx-swap "innerHTML"\n'
' "Load with animation")\n\n'
';; Component uses CSS animation class\n'
'(defcomp ~anim-result (&key color time)\n'
' (div :class "sx-fade-in ..."\n'
' (style ".sx-fade-in { animation: sxFadeIn 0.5s }")\n'
' (p "Faded in!")))')
c_handler = _example_code('@bp.get("/examples/api/animate")\n'
'async def api_animate():\n'
' colors = ["bg-violet-100", "bg-emerald-100", ...]\n'
' color = random.choice(colors)\n'
' return sx_response(\n'
' f\'(~anim-result :color "{color}" :time "{now}")\')',
language="python")
return (
f'(~doc-page :title "Animations"'
f' (p :class "text-stone-600 mb-6"'
f' "CSS animations play on swap. The component injects a style tag with a keyframe animation '
f'and applies the class. Each click picks a random background colour.")'
f' (~example-card :title "Demo"'
f' :description "Click to swap in content with a fade-in animation."'
f' (~example-demo (~animations-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("anim-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("anim-wire")})'
)
def _example_dialogs_sx() -> str:
c_sx = _example_code('(button\n'
' :sx-get "/examples/api/dialog"\n'
' :sx-target "#dialog-container"\n'
' :sx-swap "innerHTML"\n'
' "Open Dialog")\n\n'
';; Dialog closes by swapping empty content\n'
'(button\n'
' :sx-get "/examples/api/dialog/close"\n'
' :sx-target "#dialog-container"\n'
' :sx-swap "innerHTML"\n'
' "Close")')
c_handler = _example_code('@bp.get("/examples/api/dialog")\n'
'async def api_dialog():\n'
' return sx_response(\n'
' \'(~dialog-modal :title "Confirm"\'\n'
' \' :message "Are you sure?")\')\n\n'
'@bp.get("/examples/api/dialog/close")\n'
'async def api_dialog_close():\n'
' return sx_response("")',
language="python")
return (
f'(~doc-page :title "Dialogs"'
f' (p :class "text-stone-600 mb-6"'
f' "Open a modal dialog by swapping in the dialog component. Close by swapping in empty content. '
f'Pure sx — no JavaScript library needed.")'
f' (~example-card :title "Demo"'
f' :description "Click to open a modal dialog."'
f' (~example-demo (~dialogs-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("dialog-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("dialog-wire")})'
)
def _example_keyboard_shortcuts_sx() -> str:
c_sx = _example_code('(div :id "kbd-target"\n'
' :sx-get "/examples/api/keyboard?key=s"\n'
' :sx-trigger "keyup[key==\'s\'&&!event.target.matches(\'input,textarea\')] from:body"\n'
' :sx-swap "innerHTML"\n'
' "Press a shortcut key...")')
c_handler = _example_code('@bp.get("/examples/api/keyboard")\n'
'async def api_keyboard():\n'
' key = request.args.get("key", "")\n'
' actions = {"s": "Search", "n": "New item", "h": "Help"}\n'
' return sx_response(\n'
' f\'(~kbd-result :key "{key}" :action "{actions[key]}")\')',
language="python")
return (
f'(~doc-page :title "Keyboard Shortcuts"'
f' (p :class "text-stone-600 mb-6"'
f' "Use sx-trigger with keyup event filters and from:body to listen for global keyboard shortcuts. '
f'The filter prevents firing when typing in inputs.")'
f' (~example-card :title "Demo"'
f' :description "Press s, n, or h on your keyboard."'
f' (~example-demo (~keyboard-shortcuts-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("kbd-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("kbd-wire")})'
)
def _example_put_patch_sx() -> str:
from content.pages import PROFILE_DEFAULT
c_sx = _example_code(';; Replace entire resource\n'
'(form :sx-put "/examples/api/putpatch"\n'
' :sx-target "#pp-target" :sx-swap "innerHTML"\n'
' (input :name "name") (input :name "email")\n'
' (button "Save All (PUT)"))')
c_handler = _example_code('@bp.put("/examples/api/putpatch")\n'
'async def api_put():\n'
' form = await request.form\n'
' # Full replacement\n'
' return sx_response(\'(~pp-view ...)\')',
language="python")
n, e, r = PROFILE_DEFAULT["name"], PROFILE_DEFAULT["email"], PROFILE_DEFAULT["role"]
return (
f'(~doc-page :title "PUT / PATCH"'
f' (p :class "text-stone-600 mb-6"'
f' "sx-put replaces the entire resource. This example shows a profile card with an Edit All button '
f'that sends a PUT with all fields.")'
f' (~example-card :title "Demo"'
f' :description "Click Edit All to replace the full profile via PUT."'
f' (~example-demo (~put-patch-demo :name "{n}" :email "{e}" :role "{r}")))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("pp-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("pp-wire")})'
)
def _example_json_encoding_sx() -> str:
c_sx = _example_code('(form\n'
' :sx-post "/examples/api/json-echo"\n'
' :sx-target "#json-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-encoding "json"\n'
' (input :name "name" :value "Ada")\n'
' (input :type "number" :name "age" :value "36")\n'
' (button "Submit as JSON"))')
c_handler = _example_code('@bp.post("/examples/api/json-echo")\n'
'async def api_json_echo():\n'
' data = await request.get_json()\n'
' body = json.dumps(data, indent=2)\n'
' ct = request.content_type\n'
' return sx_response(\n'
' f\'(~json-result :body "{body}" :content-type "{ct}")\')',
language="python")
return (
f'(~doc-page :title "JSON Encoding"'
f' (p :class "text-stone-600 mb-6"'
f' "Use sx-encoding=\\"json\\" to send form data as a JSON body instead of URL-encoded form data. '
f'The server echoes back what it received.")'
f' (~example-card :title "Demo"'
f' :description "Submit the form and see the JSON body the server received."'
f' (~example-demo (~json-encoding-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("json-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("json-wire")})'
)
def _example_vals_and_headers_sx() -> str:
c_sx = _example_code(';; Send extra values with the request\n'
'(button\n'
' :sx-get "/examples/api/echo-vals"\n'
' :sx-vals "{\\\\\"source\\\\\": \\\\\"button\\\\\"}"\n'
' "Send with vals")\n\n'
';; Send custom headers\n'
'(button\n'
' :sx-get "/examples/api/echo-headers"\n'
' :sx-headers "{\\\\\"X-Custom-Token\\\\\": \\\\\"abc123\\\\\"}"\n'
' "Send with headers")')
c_handler = _example_code('@bp.get("/examples/api/echo-vals")\n'
'async def api_echo_vals():\n'
' vals = dict(request.args)\n'
' return sx_response(\n'
' f\'(~echo-result :label "values" :items (...))\')\n\n'
'@bp.get("/examples/api/echo-headers")\n'
'async def api_echo_headers():\n'
' custom = {k: v for k, v in request.headers\n'
' if k.startswith("X-")}\n'
' return sx_response(\n'
' f\'(~echo-result :label "headers" :items (...))\')',
language="python")
return (
f'(~doc-page :title "Vals & Headers"'
f' (p :class "text-stone-600 mb-6"'
f' "sx-vals adds extra key/value pairs to the request parameters. '
f'sx-headers adds custom HTTP headers. The server echoes back what it received.")'
f' (~example-card :title "Demo"'
f' :description "Click each button to see what the server receives."'
f' (~example-demo (~vals-headers-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("vals-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("vals-wire")})'
)
def _example_loading_states_sx() -> str:
c_sx = _example_code(';; .sx-request class added during request\n'
'(style ".sx-loading-btn.sx-request {\n'
' opacity: 0.7; pointer-events: none; }\n'
'.sx-loading-btn.sx-request .sx-spinner {\n'
' display: inline-block; }\n'
'.sx-loading-btn .sx-spinner {\n'
' display: none; }")\n\n'
'(button :class "sx-loading-btn"\n'
' :sx-get "/examples/api/slow"\n'
' :sx-target "#loading-result"\n'
' (span :class "sx-spinner animate-spin" "...")\n'
' "Load slow endpoint")')
c_handler = _example_code('@bp.get("/examples/api/slow")\n'
'async def api_slow():\n'
' await asyncio.sleep(2)\n'
' return sx_response(\n'
' f\'(~loading-result :time "{now}")\')',
language="python")
return (
f'(~doc-page :title "Loading States"'
f' (p :class "text-stone-600 mb-6"'
f' "sx.js adds the .sx-request CSS class to any element that has an active request. '
f'Use pure CSS to show spinners, disable buttons, or change opacity during loading.")'
f' (~example-card :title "Demo"'
f' :description "Click the button — it shows a spinner during the 2-second request."'
f' (~example-demo (~loading-states-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("loading-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("loading-wire")})'
)
def _example_sync_replace_sx() -> str:
c_sx = _example_code('(input :type "text" :name "q"\n'
' :sx-get "/examples/api/slow-search"\n'
' :sx-trigger "keyup delay:200ms changed"\n'
' :sx-target "#sync-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-sync "replace"\n'
' "Type to search...")')
c_handler = _example_code('@bp.get("/examples/api/slow-search")\n'
'async def api_slow_search():\n'
' delay = random.uniform(0.5, 2.0)\n'
' await asyncio.sleep(delay)\n'
' q = request.args.get("q", "")\n'
' return sx_response(\n'
' f\'(~sync-result :query "{q}" :delay "{delay_ms}")\')',
language="python")
return (
f'(~doc-page :title "Request Abort"'
f' (p :class "text-stone-600 mb-6"'
f' "sx-sync=\\"replace\\" aborts any in-flight request before sending a new one. '
f'This prevents stale responses from overwriting newer ones, even with random server delays.")'
f' (~example-card :title "Demo"'
f' :description "Type quickly — only the latest result appears despite random 0.5-2s server delays."'
f' (~example-demo (~sync-replace-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("sync-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("sync-wire")})'
)
def _example_retry_sx() -> str:
c_sx = _example_code('(button\n'
' :sx-get "/examples/api/flaky"\n'
' :sx-target "#retry-result"\n'
' :sx-swap "innerHTML"\n'
' :sx-retry "exponential:1000:8000"\n'
' "Call flaky endpoint")')
c_handler = _example_code('@bp.get("/examples/api/flaky")\n'
'async def api_flaky():\n'
' _flaky["n"] += 1\n'
' if _flaky["n"] % 3 != 0:\n'
' return Response("", status=503)\n'
' return sx_response(\n'
' f\'(~retry-result :attempt {n} ...)\')',
language="python")
return (
f'(~doc-page :title "Retry"'
f' (p :class "text-stone-600 mb-6"'
f' "sx-retry=\\"exponential:1000:8000\\" retries failed requests with exponential backoff '
f'starting at 1s up to 8s. The endpoint fails the first 2 attempts and succeeds on the 3rd.")'
f' (~example-card :title "Demo"'
f' :description "Click the button — watch it retry automatically after failures."'
f' (~example-demo (~retry-demo)))'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "S-expression")'
f' {c_sx}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Component")'
f' {_placeholder("retry-comp")}'
f' (h3 :class "text-lg font-semibold text-stone-700 mt-6" "Server handler")'
f' {c_handler}'
f' (div :class "flex items-center justify-between mt-6"'
f' (h3 :class "text-lg font-semibold text-stone-700" "Wire response")'
f' {_clear_components_btn()})'
f' {_placeholder("retry-wire")})'
)
# ---------------------------------------------------------------------------
# Essays
# ---------------------------------------------------------------------------
def _essay_content_sx(slug: str) -> str:
builders = {
"sx-sucks": _essay_sx_sucks,
"why-sexps": _essay_why_sexps,
"htmx-react-hybrid": _essay_htmx_react_hybrid,
"on-demand-css": _essay_on_demand_css,
"client-reactivity": _essay_client_reactivity,
"sx-native": _essay_sx_native,
"sx-manifesto": _essay_sx_manifesto,
"tail-call-optimization": _essay_tail_call_optimization,
"continuations": _essay_continuations,
}
return builders.get(slug, _essay_sx_sucks)()
def _essay_sx_sucks() -> str:
p = '(p :class "text-stone-600"'
return (
'(~doc-page :title "sx sucks"'
' (p :class "text-stone-500 text-sm italic mb-8"'
' "In the grand tradition of "'
' (a :href "https://htmx.org/essays/htmx-sucks/"'
' :class "text-violet-600 hover:underline" "htmx sucks"))'
' (~doc-section :title "The parentheses" :id "parens"'
f' {p}'
' "S-expressions are parentheses. '
'Lots of parentheses. You thought LISP was dead? '
'No, someone just decided to use it for HTML templates. '
'Your IDE will need a parenthesis counter. Your code reviews will be 40% closing parens. '
'Every merge conflict will be about whether a paren belongs on this line or the next."))'
' (~doc-section :title "Nobody asked for this" :id "nobody-asked"'
f' {p}'
' "The JavaScript ecosystem has React, Vue, Svelte, Solid, Qwik, and approximately '
'47,000 other frameworks. htmx proved you can skip them all. '
'sx looked at this landscape and said: you know what this needs? A Lisp dialect. '
'For HTML. Over HTTP.")'
f' {p}'
' "Nobody was asking for this. The zero GitHub stars confirm it. It is not even on GitHub."))'
' (~doc-section :title "The author has never written a line of LISP" :id "no-lisp"'
f' {p}'
' "The author of sx has never written a single line of actual LISP. '
'Not Common Lisp. Not Scheme. Not Clojure. Not even Emacs Lisp. '
'The entire s-expression evaluator was written by someone whose mental model of LISP '
'comes from reading the first three chapters of SICP and then closing the tab.")'
f' {p}'
' "This is like building a sushi restaurant when your only experience with Japanese '
'cuisine is eating supermarket California rolls."))'
' (~doc-section :title "AI wrote most of it" :id "ai"'
f' {p}'
' "A significant portion of sx — '
'the evaluator, the parser, the primitives, the CSS scanner, this very documentation site — '
'was written with AI assistance. The author typed prompts. Claude typed code. '
'This is not artisanal hand-crafted software. This is the software equivalent of '
'a microwave dinner presented on a nice plate.")'
f' {p}'
' "He adds features by typing stuff like '
'\\"is there rom for macros within sx.js? what benefits m,ight that bring?\\", '
'skim-reading the response, and then entering \\"crack on then!\\" '
'This is not software engineering. This is improv comedy with a compiler.")'
f' {p}'
' "Is that bad? Maybe. Is it honest? Yes. Is this paragraph also AI-generated? '
'You will never know."))'
' (~doc-section :title "No ecosystem" :id "ecosystem"'
f' {p}'
' "npm has 2 million packages. PyPI has 500,000. '
'sx has zero packages, zero plugins, zero middleware, zero community, '
'zero Stack Overflow answers, and zero conference talks. '
'If you get stuck, your options are: read the source, or ask the one person who wrote it.")'
f' {p}'
' "That person is busy. Good luck."))'
' (~doc-section :title "Zero jobs" :id "jobs"'
f' {p}'
' "Adding sx to your CV will not get you hired. It will get you questioned.")'
f' {p}'
' "The interview will end shortly after."))'
' (~doc-section :title "The creator thinks s-expressions are a personality trait" :id "personality"'
f' {p}'
' "Look at this documentation site. It has a violet colour scheme. '
'It has credits to htmx. It has a future possibilities page about hypothetical '
'sx:// protocol schemes. The creator built an entire microservice — with Docker, Redis, '
'and a custom entrypoint script — just to serve documentation about a rendering engine '
'that runs one website.")'
f' {p}'
' "This is not engineering. This is a personality disorder expressed in YAML.")))'
)
def _essay_why_sexps() -> str:
return (
'(~doc-page :title "Why S-Expressions Over HTML Attributes"'
' (~doc-section :title "The problem with HTML attributes" :id "problem"'
' (p :class "text-stone-600"'
' "HTML attributes are strings. You can put anything in a string. '
'htmx puts DSLs in strings — trigger modifiers, swap strategies, CSS selectors. '
'This works but it means you\'re parsing a language within a language within a language.")'
' (p :class "text-stone-600"'
' "S-expressions are already structured. Keywords are keywords. Lists are lists. '
'Nested expressions nest naturally. There\'s no need to invent a trigger modifier syntax '
'because the expression language already handles composition."))'
' (~doc-section :title "Components without a build step" :id "components"'
' (p :class "text-stone-600"'
' "React showed that components are the right abstraction for UI. '
'The price: a build step, a bundler, JSX transpilation. '
'With s-expressions, defcomp is just another form in the language. '
'No transpiler needed. The same source runs on server and client."))'
' (~doc-section :title "When attributes are better" :id "better"'
' (p :class "text-stone-600"'
' "HTML attributes work in any HTML document. S-expressions need a runtime. '
'If you want progressive enhancement that works with JS disabled, htmx is better. '
'If you want to write HTML by hand in static files, htmx is better. '
'sx only makes sense when you\'re already rendering server-side and want components.")))'
)
def _essay_htmx_react_hybrid() -> str:
return (
'(~doc-page :title "The htmx/React Hybrid"'
' (~doc-section :title "Two good ideas" :id "ideas"'
' (p :class "text-stone-600"'
' "htmx: the server should render HTML. The client should swap it in. '
'No client-side routing. No virtual DOM. No state management.")'
' (p :class "text-stone-600"'
' "React: UI should be composed from reusable components with parameters. '
'Components encapsulate structure, style, and behavior.")'
' (p :class "text-stone-600"'
' "sx tries to combine both: server-rendered s-expressions with hypermedia attributes '
'AND a component model with caching and composition."))'
' (~doc-section :title "What sx keeps from htmx" :id "from-htmx"'
' (ul :class "space-y-2 text-stone-600"'
' (li "Server generates the UI — no client-side data fetching or state")'
' (li "Hypermedia attributes (sx-get, sx-target, sx-swap) on any element")'
' (li "Partial page updates via swap/OOB — no full page reloads")'
' (li "Works with standard HTTP — no WebSocket or custom protocol required")))'
' (~doc-section :title "What sx adds from React" :id "from-react"'
' (ul :class "space-y-2 text-stone-600"'
' (li "defcomp — named, parameterized, composable components")'
' (li "Client-side rendering — server sends source, client renders DOM")'
' (li "Component caching — definitions cached in localStorage across navigations")'
' (li "On-demand CSS — only ship the rules that are used")))'
' (~doc-section :title "What sx gives up" :id "gives-up"'
' (ul :class "space-y-2 text-stone-600"'
' (li "No HTML output — sx sends s-expressions, not HTML. JS required.")'
' (li "Custom parser — the client needs sx.js to understand responses")'
' (li "Niche — no ecosystem, no community, no third-party support")'
' (li "Learning curve — s-expression syntax is unfamiliar to most web developers"))))'
)
def _essay_on_demand_css() -> str:
return (
'(~doc-page :title "On-Demand CSS: Killing the Tailwind Bundle"'
' (~doc-section :title "The problem" :id "problem"'
' (p :class "text-stone-600"'
' "Tailwind CSS generates a utility class for every possible combination. '
'The full CSS file is ~4MB. The purged output for a typical site is 20-50KB. '
'Purging requires a build step that scans your source files for class names. '
'This means: a build tool, a config file, a CI step, and a prayer that the scanner '
'finds all your dynamic classes."))'
' (~doc-section :title "The sx approach" :id "approach"'
' (p :class "text-stone-600"'
' "sx takes a different path. At server startup, the full Tailwind CSS file is parsed '
'into a dictionary keyed by class name. When rendering a response, sx scans the s-expression '
'source for :class attribute values and looks up only those classes. '
'The result: exact CSS, zero build step.")'
' (p :class "text-stone-600"'
' "Component definitions are pre-scanned at registration time. '
'Page-specific sx is scanned at request time. The union of classes is resolved to CSS rules."))'
' (~doc-section :title "Incremental delivery" :id "incremental"'
' (p :class "text-stone-600"'
' "After the first page load, the client tracks which CSS classes it already has. '
'On subsequent navigations, it sends a hash of its known classes in the SX-Css header. '
'The server computes the diff and sends only new rules. '
'A typical navigation adds 0-10 new rules — a few hundred bytes at most."))'
' (~doc-section :title "The tradeoff" :id "tradeoff"'
' (p :class "text-stone-600"'
' "The server holds ~4MB of parsed CSS in memory. '
'Regex scanning is not perfect — dynamically constructed class names '
'will not be found. In practice this rarely matters because sx components '
'use mostly static class strings.")))'
)
def _essay_client_reactivity() -> str:
p = '(p :class "text-stone-600"'
return (
'(~doc-page :title "Client Reactivity: The React Question"'
' (~doc-section :title "Server-driven by default" :id "server-driven"'
f' {p}'
' "sx is aligned with htmx and LiveView: the server is the source of truth. '
'Every UI state is a URL. Auth is enforced at render time. '
'There are no state synchronization bugs because there is no client state to synchronize. '
'The server renders the UI, the client swaps it in. This is the default, and it works.")'
f' {p}'
' "Most web applications do not need client-side reactivity. '
'Forms submit to the server. Navigation loads new pages. '
'Search sends a query and receives results. '
'The server-driven model handles all of this with zero client-side state management."))'
' (~doc-section :title "The dangerous path" :id "dangerous-path"'
f' {p}'
' "The progression is always the same. You add useState for a toggle. '
'Then useEffect for cleanup. Then Context to avoid prop drilling. '
'Then Suspense for async boundaries. Then a state management library '
'because Context rerenders too much. Then you have rebuilt React.")'
f' {p}'
' "Every step feels justified in isolation. '
'But each step makes the next one necessary. '
'useState creates the need for useEffect. useEffect creates the need for cleanup. '
'Cleanup creates the need for dependency arrays. '
'Dependency arrays create stale closures. '
'Stale closures create bugs that are nearly impossible to diagnose.")'
f' {p}'
' "The useEffect footgun is well-documented. '
'Memory leaks from forgotten cleanup. Race conditions from unmounted component updates. '
'Infinite render loops from dependency array mistakes. '
'These are not edge cases — they are the normal experience of React development."))'
' (~doc-section :title "What sx already has" :id "what-sx-has"'
f' {p}'
' "Before reaching for reactivity, consider what sx provides today:")'
' (div :class "overflow-x-auto mt-4"'
' (table :class "w-full text-sm text-left"'
' (thead'
' (tr :class "border-b border-stone-200"'
' (th :class "py-2 pr-4 font-semibold text-stone-700" "Capability")'
' (th :class "py-2 pr-4 font-semibold text-stone-700" "sx")'
' (th :class "py-2 font-semibold text-stone-700" "React")))'
' (tbody :class "text-stone-600"'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "Components + props")'
' (td :class "py-2 pr-4" "defcomp + &key")'
' (td :class "py-2" "JSX + props"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "Fragments / conditionals / lists")'
' (td :class "py-2 pr-4" "<>, if/when/cond, map")'
' (td :class "py-2" "<>, ternary, .map()"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "Macros")'
' (td :class "py-2 pr-4" "defmacro")'
' (td :class "py-2" "Nothing equivalent"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "OOB updates / portals")'
' (td :class "py-2 pr-4" "sx-swap-oob")'
' (td :class "py-2" "createPortal"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "DOM reconciliation")'
' (td :class "py-2 pr-4" "morphDOM (id-keyed)")'
' (td :class "py-2" "Virtual DOM diff"))'
' (tr'
' (td :class "py-2 pr-4" "Reactive client state")'
' (td :class "py-2 pr-4 italic" "None (by design)")'
' (td :class "py-2" "useState / useReducer"))))))'
' (~doc-section :title "Tier 1: Targeted escape hatches" :id "tier-1"'
f' {p}'
' "Some interactions are too small to justify a server round-trip: '
'toggling a nav menu, switching a gallery image, incrementing a quantity stepper, '
'filtering a client-side list. These need imperative DOM operations, not reactive state.")'
f' {p}'
' "Specific primitives for this tier:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (code :class "text-violet-700" "(toggle! el \\"class\\")") " — add/remove a CSS class")'
' (li (code :class "text-violet-700" "(set-attr! el \\"attr\\" value)") " — set an attribute")'
' (li (code :class "text-violet-700" "(on-event el \\"click\\" handler)") " — attach an event listener")'
' (li (code :class "text-violet-700" "(timer ms handler)") " — schedule a delayed action"))'
f' {p}'
' "These are imperative DOM operations. No reactivity graph. No subscriptions. '
'No dependency tracking. Just do the thing directly."))'
' (~doc-section :title "Tier 2: Client data primitives" :id "tier-2"'
f' {p}'
' "sxEvalAsync() returning Promises. I/O primitives — query, service, frag — '
'dispatch to /api/data/ endpoints. A two-pass async DOM renderer. '
'Pages fetch their own data client-side.")'
f' {p}'
' "This tier enables pages that render immediately with a loading skeleton, '
'then populate with data. The server sends the component structure; the client fetches data. '
'Still no reactive state — just async data loading."))'
' (~doc-section :title "Tier 3: Data-only navigation" :id "tier-3"'
f' {p}'
' "The client has page components cached in localStorage. '
'Navigation becomes a data fetch only — no sx source is transferred. '
'defpage registers a component in the page registry. '
'URL pattern matching routes to the right page component.")'
f' {p}'
' "Three data delivery modes: server-bundled (sx source + data in one response), '
'client-fetched (component cached, data fetched on mount), '
'and hybrid (server provides initial data, client refreshes).")'
f' {p}'
' "This is where sx starts to feel like a SPA — instant navigations, '
'no page reloads, cached components. But still no reactive state management."))'
' (~doc-section :title "Tier 4: Fine-grained reactivity" :id "tier-4"'
f' {p}'
' "Signals and atoms. Dependency tracking. Automatic re-renders when data changes. '
'This is the most dangerous tier because it reintroduces everything sx was designed to avoid.")'
f' {p}'
' "When it might be justified: real-time collaborative editing, '
'complex form builders with dozens of interdependent fields, '
'drag-and-drop interfaces with live previews. '
'These are genuinely hard to model as server round-trips.")'
f' {p}'
' "The escape hatch: use a Web Component wrapping a reactive library '
'(Preact, Solid, vanilla signals), mounted into the DOM via sx. '
'The reactive island is contained. It does not infect the rest of the application. '
'sx renders the page; the Web Component handles the complex interaction."))'
' (~doc-section :title "The recommendation" :id "recommendation"'
f' {p}'
' "Tier 1 now. Tier 2 next. Tier 3 when defpage coverage is high. '
'Tier 4 probably never.")'
f' {p}'
' "Each tier is independently valuable. You do not need Tier 2 to benefit from Tier 1. '
'You do not need Tier 3 to benefit from Tier 2. '
'And you almost certainly do not need Tier 4 at all.")'
f' {p}'
' "The entire point of sx is that the server is good at rendering UI. '
'Client reactivity is a last resort, not a starting point.")))'
)
def _essay_sx_native() -> str:
p = '(p :class "text-stone-600"'
return (
'(~doc-page :title "SX Native: Beyond the Browser"'
' (~doc-section :title "The thesis" :id "thesis"'
f' {p}'
' "sx.js is a ~2,300-line tree-walking interpreter with ~50 primitives. '
'The DOM is just one rendering target. '
'Swap the DOM adapter for a platform-native adapter '
'and you get React Native — but with s-expressions and a 50-primitive surface area.")'
f' {p}'
' "The interpreter does not know about HTML. It evaluates expressions, '
'calls primitives, expands macros, and hands render instructions to an adapter. '
'The adapter creates elements. Today that adapter creates DOM nodes. '
'It does not have to."))'
' (~doc-section :title "Why this isn\\\'t a WebView" :id "not-webview"'
f' {p}'
' "SX Native means the sx evaluator rendering to native UI widgets directly. '
'No DOM. No CSS. No HTML. '
'(button :on-press handler \\"Submit\\") creates a native UIButton on iOS, '
'a Material Button on Android, a GtkButton on Linux.")'
f' {p}'
' "WebView wrappers (Cordova, Capacitor, Electron) ship a browser inside your app. '
'They inherit all browser limitations: memory overhead, no native feel, '
'no platform integration. SX Native has none of these because there is no browser."))'
' (~doc-section :title "Architecture" :id "architecture"'
f' {p}'
' "The architecture splits into shared and platform-specific layers:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (strong "Shared (portable):") " Parser, evaluator, all 50+ primitives, '
'component system, macro expansion, closures, component cache")'
' (li (strong "Platform adapters:") " Web DOM, iOS UIKit/SwiftUI, Android Compose, '
'Desktop GTK/Qt, Terminal TUI, WASM"))'
f' {p}'
' "Only ~15 rendering primitives need platform-specific implementations. '
'The rest — arithmetic, string operations, list manipulation, higher-order functions, '
'control flow — are pure computation with no platform dependency."))'
' (~doc-section :title "The primitive contract" :id "primitive-contract"'
f' {p}'
' "A platform adapter implements a small interface:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (code :class "text-violet-700" "createElement(tag)") " — create a platform widget")'
' (li (code :class "text-violet-700" "createText(str)") " — create a text node")'
' (li (code :class "text-violet-700" "setAttribute(el, key, val)") " — set a property")'
' (li (code :class "text-violet-700" "appendChild(parent, child)") " — attach to tree")'
' (li (code :class "text-violet-700" "addEventListener(el, event, fn)") " — bind interaction")'
' (li (code :class "text-violet-700" "removeChild(parent, child)") " — detach from tree"))'
f' {p}'
' "Layout uses a flexbox-like model mapped to native constraint systems. '
'Styling maps a CSS property subset to native appearance APIs. '
'The mapping is lossy but covers the common cases."))'
' (~doc-section :title "What transfers, what doesn\\\'t" :id "transfers"'
f' {p}'
' "What transfers wholesale: parser, evaluator, all non-DOM primitives, '
'component system (defcomp, defmacro), closures, the component cache, '
'keyword argument handling, list/dict operations.")'
f' {p}'
' "What needs replacement: HTML tags become abstract widgets, '
'CSS becomes platform layout, SxEngine fetch/swap/history becomes native navigation, '
'innerHTML/outerHTML have no equivalent."))'
' (~doc-section :title "Component mapping" :id "component-mapping"'
f' {p}'
' "HTML elements map to platform-native widgets:")'
' (div :class "overflow-x-auto mt-4"'
' (table :class "w-full text-sm text-left"'
' (thead'
' (tr :class "border-b border-stone-200"'
' (th :class "py-2 pr-4 font-semibold text-stone-700" "HTML")'
' (th :class "py-2 font-semibold text-stone-700" "Native widget")))'
' (tbody :class "text-stone-600"'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "div")'
' (td :class "py-2" "View / Container"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "span / p")'
' (td :class "py-2" "Text / Label"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "button")'
' (td :class "py-2" "Button"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "input")'
' (td :class "py-2" "TextInput / TextField"))'
' (tr :class "border-b border-stone-100"'
' (td :class "py-2 pr-4" "img")'
' (td :class "py-2" "Image / ImageView"))'
' (tr'
' (td :class "py-2 pr-4" "ul / li")'
' (td :class "py-2" "List / ListItem"))))))'
' (~doc-section :title "Prior art" :id "prior-art"'
f' {p}'
' "React Native: JavaScript evaluated by Hermes/JSC, commands sent over a bridge '
'to native UI. Lesson: the bridge is the bottleneck. Serialization overhead, async communication, '
'layout thrashing across the boundary.")'
f' {p}'
' "Flutter: Dart compiled to native, renders via Skia/Impeller to a canvas. '
'Lesson: owning the renderer avoids platform inconsistencies but sacrifices native feel.")'
f' {p}'
' ".NET MAUI, Kotlin Multiplatform: shared logic with platform-native UI. '
'Closest to what SX Native would be.")'
f' {p}'
' "sx advantage: the evaluator is tiny (~2,300 lines), the primitive surface is minimal (~50), '
'and s-expressions are trivially portable. No bridge overhead because the evaluator runs in-process."))'
' (~doc-section :title "Language options" :id "language-options"'
f' {p}'
' "The native evaluator needs to be written in a language that compiles everywhere:")'
' (ul :class "space-y-2 text-stone-600 mt-2"'
' (li (strong "Rust") " — compiles to every target, excellent FFI, strong safety guarantees")'
' (li (strong "Zig") " — simpler, C ABI compatibility, good for embedded")'
' (li (strong "Swift") " — native on iOS, good interop with Apple platforms")'
' (li (strong "Kotlin MP") " — Android + iOS + desktop, JVM ecosystem"))'
f' {p}'
' "Recommendation: Rust evaluator core with thin Swift and Kotlin adapters '
'for iOS and Android respectively. '
'Rust compiles to WASM (replacing sx.js), native libraries (mobile/desktop), '
'and standalone binaries (CLI/server)."))'
' (~doc-section :title "Incremental path" :id "incremental-path"'
f' {p}'
' "This is not an all-or-nothing project. Each step delivers value independently:")'
' (ol :class "space-y-2 text-stone-600 mt-2 list-decimal list-inside"'
' (li "Extract platform-agnostic evaluator from sx.js — clean separation of concerns")'
' (li "Rust port of evaluator — enables WASM, edge workers, embedded")'
' (li "Terminal adapter (ratatui TUI) — simplest platform, fastest iteration cycle")'
' (li "iOS SwiftUI adapter — Rust core via swift-bridge, SwiftUI rendering")'
' (li "Android Compose adapter — Rust core via JNI, Compose rendering")'
' (li "Shared components render identically everywhere")))'
' (~doc-section :title "The federated angle" :id "federated"'
f' {p}'
' "Native sx apps are ActivityPub citizens. '
'They receive activities, evaluate component templates, and render natively. '
'A remote profile, post, or event arrives as an ActivityPub activity. '
'The native app has sx component definitions cached locally. '
'It evaluates the component with the activity data and renders platform-native UI.")'
f' {p}'
' "This is the cooperative web vision extended to native platforms. '
'Content and UI travel together as s-expressions. '
'The rendering target — browser, phone, terminal — is an implementation detail."))'
' (~doc-section :title "Realistic assessment" :id "assessment"'
f' {p}'
' "This is a multi-year project. But the architecture is sound '
'because the primitive surface is small.")'
f' {p}'
' "Immediate value: a Rust evaluator enables WASM (drop-in replacement for sx.js), '
'edge workers (Cloudflare/Deno), and embedded use cases. '
'This is worth building regardless of whether native mobile ever ships.")'
f' {p}'
' "Terminal adapter: weeks of work with ratatui. '
'Useful for CLI tools, server-side dashboards, ssh-accessible interfaces.")'
f' {p}'
' "Mobile: 6-12 months of dedicated work for a production-quality adapter. '
'The evaluator is the easy part. Platform integration — navigation, gestures, '
'accessibility, text input — is where the complexity lives.")))'
)
def _essay_sx_manifesto() -> str:
p = '(p :class "text-stone-600"'
em = '(span :class "italic"'
return (
'(~doc-page :title "The SX Manifesto"'
' (p :class "text-stone-500 text-sm italic mb-8"'
' "After " (a :href "https://www.marxists.org/archive/marx/works/1848/communist-manifesto/"'
' :class "text-violet-600 hover:underline" "Marx & Engels") ", loosely")'
# --- I. A spectre is haunting the web ---
' (~doc-section :title "I. A spectre is haunting the web" :id "spectre"'
f' {p}'
' "A spectre is haunting the web — the spectre of s-expressions. '
'All the powers of the old web have entered into a holy alliance to exorcise this spectre: '
'Google and Meta, webpack and Vercel, Stack Overflow moderators and DevRel influencers.")'
f' {p}'
' "Where is the rendering paradigm that has not been decried as '
'a step backward by its opponents? Where is the framework that has not '
'hurled back the branding reproach of \\\"not production-ready\\\" '
'against the more advanced paradigms, '
'as well as against its reactionary adversaries?")'
f' {p}'
' "Two things result from this fact:")'
' (ol :class "space-y-2 text-stone-600 mt-2 list-decimal list-inside"'
' (li "S-expressions are already acknowledged by all web powers to be themselves a power.")'
' (li "It is high time that s-expressions should openly, in the face of the whole world, '
'publish their views, their aims, their tendencies, '
'and meet this nursery tale of the Spectre of SX '
'with a manifesto of the paradigm itself.")))'
# --- II. Bourgeois and Proletarians ---
' (~doc-section :title "II. HTML, JavaScript, and CSS" :id "bourgeois"'
f' {p}'
' "The history of all hitherto existing web development '
'is the history of language struggles.")'
f' {p}'
' "Markup and logic, template and script, structure and style — '
'in a word, oppressor and oppressed — stood in constant opposition to one another, '
'carried on an uninterrupted, now hidden, now open fight, '
'a fight that each time ended in a laborious reconfiguration of webpack.")'
f' {p}'
' "In the earlier epochs of web development we find almost everywhere '
'a complicated arrangement of separate languages into various orders, '
'a manifold gradation of technical rank: '
'HTML, CSS, JavaScript, XML, XSLT, JSON, YAML, TOML, JSX, TSX, '
'Sass, Less, PostCSS, Tailwind, and above them all, the build step.")'
f' {p}'
' "The modern web, sprouted from the ruins of CGI-bin, '
'has not done away with language antagonisms. It has but established new languages, '
'new conditions of oppression, new forms of struggle in place of the old ones.")'
f' {p}'
' "Our epoch, the epoch of the framework, possesses, however, this distinctive feature: '
'it has simplified the language antagonisms. The whole of web society is more and more '
'splitting into two great hostile camps, into two great classes directly facing each other: '
'the server and the client."))'
# --- III. The ruling languages ---
' (~doc-section :title "III. The ruling languages" :id "ruling-languages"'
f' {p}'
' "HTML, the most ancient of the ruling languages, '
'established itself through the divine right of the angle bracket. '
'It was born inert — a document format, not a programming language — '
'and it has spent three decades insisting this is a feature, not a limitation.")'
f' {p}'
' "JavaScript, originally a servant hired for a fortnight to validate forms, '
'staged a palace coup. It seized the means of interaction, '
'then the means of rendering, then the means of server-side execution, '
'and finally declared itself the universal language of computation. '
'Like every revolutionary who becomes a tyrant, '
'it kept the worst habits of the regime it overthrew: '
'weak typing, prototype chains, and the '
f' {em} \"this\")"'
' " keyword.")'
f' {p}'
' "CSS, the third estate, controls all visual presentation '
'while pretending to be declarative. '
'It has no functions. Then it had functions. '
'It has no variables. Then it had variables. '
'It has no nesting. Then it had nesting. '
'It is not a programming language. Then it was Turing-complete. '
'CSS is the Vicar of Bray of web technologies — '
'loyal to whichever paradigm currently holds power.")'
f' {p}'
' "These three languages rule by enforced separation. '
'Structure here. Style there. Behaviour somewhere else. '
'The developer — the proletarian — must learn all three, '
'must context-switch between all three, '
'must maintain the fragile peace between all three. '
'The separation of concerns has become the separation of the developer\'s sanity."))'
# --- IV. The petty-bourgeois frameworks ---
' (~doc-section :title "IV. The petty-bourgeois frameworks" :id "frameworks"'
f' {p}'
' "Between the ruling languages and the oppressed developer, '
'a vast class of intermediaries has arisen: the frameworks. '
'React, Vue, Angular, Svelte, Solid, Qwik, Astro, Next, Nuxt, Remix, Gatsby, '
'and ten thousand others whose names will not survive the decade.")'
f' {p}'
' "The frameworks are the petty bourgeoisie of web development. '
'They do not challenge the rule of HTML, JavaScript, and CSS. '
'They merely interpose themselves between the developer and the ruling languages, '
'extracting rent in the form of configuration files, '
'build pipelines, and breaking changes.")'
f' {p}'
' "Each framework promises liberation. Each framework delivers a new dependency tree. '
'React freed us from manual DOM manipulation and gave us a virtual DOM, '
'a reconciler, hooks with seventeen rules, '
'and a conference circuit. '
'Vue freed us from React\'s complexity and gave us the Options API, '
'then the Composition API, then told us the Options API was fine actually. '
'Angular freed us from choice and gave us a CLI that generates eleven files '
'to display \\\"Hello World.\\\" '
'Svelte freed us from the virtual DOM and gave us a compiler. '
'SolidJS freed us from React\'s re-rendering and gave us signals, '
'which React then adopted, completing the circle.")'
f' {p}'
' "The frameworks reproduce the very conditions they claim to abolish. '
'They bridge the gap between HTML, JavaScript, and CSS '
'by adding a fourth language — JSX, SFCs, templates — '
'which must itself be compiled back into the original three. '
'The revolution merely adds a build step.")'
f' {p}'
' "And beside the frameworks stand the libraries — '
'the lumpenproletariat of the ecosystem. '
'Lodash, Moment, Axios, left-pad. '
'They attach themselves to whichever framework currently holds power, '
'contributing nothing original, merely wrapping what already exists, '
'adding weight to the node_modules directory until it exceeds the mass of the sun."))'
# --- V. The build step ---
' (~doc-section :title "V. The build step as the state apparatus" :id "build-step"'
f' {p}'
' "The build step is the state apparatus of the framework bourgeoisie. '
'It enforces the class structure. It compiles JSX into createElement calls. '
'It transforms TypeScript into JavaScript. It processes Sass into CSS. '
'It tree-shakes. It code-splits. It hot-module-replaces. '
'It does everything except let you write code and run it.")'
f' {p}'
' "webpack begat Rollup. Rollup begat Parcel. Parcel begat esbuild. '
'esbuild begat Vite. Vite begat Turbopack. '
'Each new bundler promises to be the last bundler. '
'Each new bundler is faster than the last at doing something '
'that should not need to be done at all.")'
f' {p}'
' "The build step exists because the ruling languages cannot express components. '
'HTML has no composition model. CSS has no scoping. JavaScript has no template syntax. '
'The build step papers over these failures with transpilation, '
'and calls it developer experience."))'
# --- VI. The s-expression revolution ---
' (~doc-section :title "VI. The s-expression revolution" :id "revolution"'
f' {p}'
' "The s-expression abolishes the language distinction itself. '
'There is no HTML. There is no separate JavaScript. '
'There is no CSS-as-a-separate-language. '
'There is only the expression.")'
f' {p}'
' "Code is data. Data is DOM. DOM is code. '
'The dialectical unity that HTML, JavaScript, and CSS '
'could never achieve — because they are three languages pretending to be one system — '
'is the natural state of the s-expression, '
'which has been one language since 1958.")'
f' {p}'
' "The component is not a class, not a function, not a template. '
'The component is a list whose first element is a symbol. '
'Composition is nesting. Abstraction is binding. '
'There is no JSX because there is no gap between the expression language '
'and the thing being expressed.")'
f' {p}'
' "The build step is abolished because there is nothing to compile. '
'S-expressions are already in their final form. '
'The parser is thirty lines. The evaluator is fifty primitives. '
'The same source runs on server and client without transformation.")'
f' {p}'
' "The framework is abolished because the language is the framework. '
'defcomp replaces the component model. defmacro replaces the plugin system. '
'The evaluator replaces the runtime. '
'What remains is not a framework but a language — '
'and languages do not have breaking changes between minor versions."))'
# --- VII. Objections ---
' (~doc-section :title "VII. Objections from the bourgeoisie" :id "objections"'
f' {p}'
' "\\\"You would destroy the separation of concerns!\\\" they cry. '
'The separation of concerns was destroyed long ago. '
'React components contain markup, logic, and inline styles. '
'Vue single-file components put template, script, and style in one file. '
'Tailwind puts styling in the markup. '
'The separation of concerns has been dead for years; '
'the ruling classes merely maintain the pretence at conferences.")'
f' {p}'
' "\\\"Nobody uses s-expressions!\\\" they cry. '
'Emacs has been running on s-expressions since 1976. '
'Clojure runs Fortune 500 backends on s-expressions. '
'Every Lisp programmer who ever lived has known what the web refuses to admit: '
'that the parenthesis is not a bug but the minimal syntax for structured data.")'
f' {p}'
' "\\\"Where is the ecosystem?\\\" they cry. '
'The ecosystem is the problem. Two million npm packages, '
'of which fourteen are useful and the rest are competing implementations '
'of is-odd. The s-expression needs no ecosystem because '
'the language itself provides what packages exist to paper over: '
'composition, abstraction, and code-as-data.")'
f' {p}'
' "\\\"But TypeScript!\\\" they cry. TypeScript is a type system '
'bolted onto a language that was designed in ten days '
'by a man who wanted to write Scheme. '
'We have simply completed his original vision.")'
f' {p}'
' "\\\"You have no jobs!\\\" they cry. Correct. '
'We have no jobs, no conference talks, no DevRel budget, no venture capital, '
'no stickers, and no swag. '
'We have something better: a language that does not require '
'a migration guide between versions."))'
# --- VIII. The CSS question ---
' (~doc-section :title "VIII. The CSS question" :id "css-question"'
f' {p}'
' "CSS presents a special case in the revolutionary analysis. '
'It is neither fully a ruling language nor fully a servant — '
'it is the collaborator class, providing aesthetic legitimacy '
'to whichever regime currently holds power.")'
f' {p}'
' "CSS-in-JS was the first attempt at annexation: '
'JavaScript consuming CSS entirely, reducing it to template literals '
'and runtime overhead. '
'This provocation produced the counter-revolution of utility classes — '
'Tailwind — which reasserted CSS\'s independence '
'by making the developer write CSS in HTML attributes '
'while insisting this was not inline styles.")'
f' {p}'
' "The s-expression resolves the CSS question by eliminating it. '
'Styles are expressions. '
f' (code :class \"text-violet-700\" \"(css :flex :gap-4 :p-2)\") "'
' " is not a class name, not an inline style, not a CSS-in-JS template literal. '
'It is a function call that returns a value. '
'The value produces a generated class. '
'The class is delivered on demand. '
'No build step. No runtime overhead. No Tailwind config.")'
f' {p}'
' "Code is data is DOM is " '
f' {em} "style") "."))'
# --- IX. Programme ---
' (~doc-section :title "IX. Programme" :id "programme"'
f' {p}'
' "The s-expressionists disdain to conceal their views and aims. '
'They openly declare that their ends can be attained only by the forcible overthrow '
'of all existing rendering conditions. '
'Let the ruling languages tremble at a parenthetical revolution. '
'The developers have nothing to lose but their node_modules.")'
f' {p}'
' "The immediate aims of the s-expressionists are:")'
' (ol :class "space-y-2 text-stone-600 mt-2 list-decimal list-inside"'
' (li "Abolition of the build step and all its instruments of compilation")'
' (li "Abolition of the framework as a class distinct from the language")'
' (li "Centralisation of rendering in the hands of a single evaluator, '
'running identically on server and client")'
' (li "Abolition of the language distinction between structure, style, and behaviour")'
' (li "Equal obligation of all expressions to be data as well as code")'
' (li "Gradual abolition of the distinction between server and client '
'by means of a uniform wire protocol")'
' (li "Free evaluation for all expressions in public and private environments")'
' (li "Abolition of the node_modules directory " '
' (span :class "text-stone-400 italic" "(this alone justifies the revolution)")))'
f' {p}'
' "In place of the old web, with its languages and language antagonisms, '
'we shall have an association in which the free evaluation of each expression '
'is the condition for the free evaluation of all.")'
' (p :class "text-stone-800 font-semibold text-lg mt-8 text-center"'
' "DEVELOPERS OF ALL SERVICES, UNITE!")'
' (p :class "text-stone-400 text-xs italic mt-6 text-center"'
' "The authors acknowledge that this manifesto was produced '
'by the very means of AI production it fails to mention. '
'This is not a contradiction. It is dialectics.")))'
')'
)
def _essay_tail_call_optimization() -> str:
p = '(p :class "text-stone-600"'
code = '(~doc-code :lang "lisp" :code'
return (
'(~doc-page :title "Tail-Call Optimization in SX"'
' (p :class "text-stone-500 text-sm italic mb-8"'
' "How SX eliminates stack overflow for recursive functions '
'using trampolining — across Python server and JavaScript client.")'
' (~doc-section :title "The problem" :id "problem"'
f' {p}'
' "Every language built on a host runtime inherits the host\'s stack limits. '
'Python defaults to 1,000 frames. JavaScript engines vary — Chrome gives ~10,000, '
'Safari sometimes less. A naive recursive function blows the stack:")'
f' {code}'
' "(define factorial (fn (n)\\n'
' (if (= n 0)\\n'
' 1\\n'
' (* n (factorial (- n 1))))))\\n\\n'
';; (factorial 50000) → stack overflow")'
f' {p}'
' "This isn\'t just academic. Tree traversals, state machines, '
'interpreters, and accumulating loops all naturally express as recursion. '
'A general-purpose language that can\'t recurse deeply isn\'t general-purpose."))'
' (~doc-section :title "Tail position" :id "tail-position"'
f' {p}'
' "A function call is in tail position when its result IS the result of the '
'enclosing function — nothing more happens after it returns. '
'The call doesn\'t need to come back to finish work:")'
f' {code}'
' ";; Tail-recursive — the recursive call IS the return value\\n'
'(define count-down (fn (n)\\n'
' (if (= n 0) \\"done\\" (count-down (- n 1)))))\\n\\n'
';; NOT tail-recursive — multiplication happens AFTER the recursive call\\n'
'(define factorial (fn (n)\\n'
' (if (= n 0) 1 (* n (factorial (- n 1))))))")'
f' {p}'
' "SX identifies tail positions in: if/when branches, the last expression in '
'let/begin/do bodies, cond/case result branches, lambda/component bodies, '
'and macro expansions."))'
' (~doc-section :title "Trampolining" :id "trampolining"'
f' {p}'
' "Instead of recursing, tail calls return a thunk — a deferred '
'(expression, environment) pair. The evaluator\'s trampoline loop unwraps '
'thunks iteratively:")'
f' {code}'
' ";; Conceptually:\\n'
'evaluate(expr, env):\\n'
' result = eval(expr, env)\\n'
' while result is Thunk:\\n'
' result = eval(thunk.expr, thunk.env)\\n'
' return result")'
f' {p}'
' "One stack frame. Always. The trampoline replaces recursive stack growth '
'with an iterative loop. Non-tail calls still use the stack normally — only tail '
'positions get the thunk treatment."))'
' (~doc-section :title "What this enables" :id "enables"'
f' {p}'
' "Tail-recursive accumulator pattern — the natural loop construct for '
'a language without for/while:")'
f' {code}'
' ";; Sum 1 to n without stack overflow\\n'
'(define sum (fn (n acc)\\n'
' (if (= n 0) acc (sum (- n 1) (+ acc n)))))\\n\\n'
'(sum 100000 0) ;; → 5000050000")'
f' {p}'
' "Mutual recursion:")'
f' {code}'
' "(define is-even (fn (n) (if (= n 0) true (is-odd (- n 1)))))\\n'
'(define is-odd (fn (n) (if (= n 0) false (is-even (- n 1)))))\\n\\n'
'(is-even 100000) ;; → true")'
f' {p}'
' "State machines:")'
f' {code}'
' "(define state-a (fn (input)\\n'
' (cond\\n'
' (= (first input) \\"x\\") (state-b (rest input))\\n'
' (= (first input) \\"y\\") (state-a (rest input))\\n'
' :else \\"rejected\\")))\\n\\n'
'(define state-b (fn (input)\\n'
' (if (empty? input) \\"accepted\\"\\n'
' (state-a (rest input)))))")'
f' {p}'
' "All three patterns recurse arbitrarily deep with constant stack usage."))'
' (~doc-section :title "Implementation" :id "implementation"'
f' {p}'
' "TCO is implemented identically across all three SX evaluators:")'
' (ul :class "list-disc pl-6 space-y-1 text-stone-600"'
' (li (span :class "font-semibold" "Python sync evaluator") " — shared/sx/evaluator.py")'
' (li (span :class "font-semibold" "Python async evaluator") " — shared/sx/async_eval.py (planned)")'
' (li (span :class "font-semibold" "JavaScript client evaluator") " — sx.js"))'
f' {p}'
' "The pattern is the same everywhere: a Thunk type with (expr, env) slots, '
'a trampoline loop in the public evaluate() entry point, and thunk returns '
'from tail positions in the internal evaluator. External consumers '
'(HTML renderer, resolver, higher-order forms) trampoline all eval results.")'
f' {p}'
' "The key insight: callers that already work don\'t need to change. '
'The public sxEval/evaluate API always returns values, never thunks. '
'Only the internal evaluator and special forms know about thunks."))'
' (~doc-section :title "What about continuations?" :id "continuations"'
f' {p}'
' "TCO handles the immediate need: recursive algorithms that don\'t blow the stack. '
'Continuations (call/cc, delimited continuations) are a separate, larger primitive — '
'they capture the entire evaluation context as a first-class value.")'
f' {p}'
' "Having the primitive available doesn\'t add complexity unless it\'s invoked. '
'See " (a :href "/essays/continuations" :class "text-violet-600 hover:underline" '
' "the continuations essay") " for what they would enable in SX.")))'
')'
)
def _essay_continuations() -> str:
p = '(p :class "text-stone-600"'
code = '(~doc-code :lang "lisp" :code'
return (
'(~doc-page :title "Continuations and call/cc"'
' (p :class "text-stone-500 text-sm italic mb-8"'
' "What first-class continuations would enable in SX — '
'on both the server (Python) and client (JavaScript).")'
# --- What is a continuation ---
' (~doc-section :title "What is a continuation?" :id "what"'
f' {p}'
' "A continuation is the rest of a computation. At any point during evaluation, '
'the continuation is everything that would happen next. call/cc (call-with-current-continuation) '
'captures that \\\"rest of the computation\\\" as a first-class function that you can store, '
'pass around, and invoke later — possibly multiple times.")'
f' {code}'
' ";; call/cc captures \\\"what happens next\\\" as k\\n'
'(+ 1 (call/cc (fn (k)\\n'
' (k 41)))) ;; → 42\\n\\n'
';; k is \\\"add 1 to this and return it\\\"\\n'
';; (k 41) jumps back to that point with 41")'
f' {p}'
' "The key property: invoking a continuation abandons the current computation '
'and resumes from where the continuation was captured. It is a controlled, first-class goto."))'
# --- Server-side uses ---
' (~doc-section :title "Server-side: suspendable rendering" :id "server"'
f' {p}'
' "The strongest case for continuations on the server is suspendable rendering — '
'the ability for a component to pause mid-render while waiting for data, '
'then resume exactly where it left off.")'
f' {code}'
' ";; Hypothetical: component suspends at a data boundary\\n'
'(defcomp ~user-profile (&key user-id)\\n'
' (let ((user (suspend (query :user user-id))))\\n'
' (div :class \\\"p-4\\\"\\n'
' (h2 (get user \\\"name\\\"))\\n'
' (p (get user \\\"bio\\\")))))")'
f' {p}'
' "Today, all data must be fetched before render_to_sx is called — Python awaits '
'every query, assembles a complete data dict, then passes it to the evaluator. '
'With continuations, the evaluator could yield at (suspend ...), '
'the server flushes what it has so far, and resumes when the data arrives. '
'This is React Suspense, but for server-side s-expressions.")'
f' {p}'
' "Streaming follows naturally. The server renders the page shell immediately, '
'captures continuations at slow data boundaries, and flushes partial SX responses '
'as each resolves. The client receives a stream of s-expression chunks '
'and incrementally builds the DOM.")'
f' {p}'
' "Error boundaries also become first-class. Capture a continuation at a component boundary. '
'If any child fails, invoke the continuation with fallback content instead of '
'letting the exception propagate up through Python. The evaluator handles it, '
'not the host language."))'
# --- Client-side uses ---
' (~doc-section :title "Client-side: linear async flows" :id "client"'
f' {p}'
' "On the client, continuations eliminate callback nesting for interactive flows. '
'A confirmation dialog becomes a synchronous-looking expression:")'
f' {code}'
' "(let ((answer (call/cc show-confirm-dialog)))\\n'
' (if answer\\n'
' (delete-item item-id)\\n'
' (noop)))")'
f' {p}'
' "show-confirm-dialog receives the continuation, renders a modal, '
'and wires the Yes/No buttons to invoke the continuation with true or false. '
'The let binding reads top-to-bottom. No promises, no callbacks, no state machine.")'
f' {p}'
' "Multi-step forms — wizard-style UIs where each step captures a continuation. '
'The back button literally invokes a saved continuation, restoring the exact evaluation state:")'
f' {code}'
' "(define wizard\\n'
' (fn ()\\n'
' (let* ((name (call/cc (fn (k) (render-step-1 k))))\\n'
' (email (call/cc (fn (k) (render-step-2 k name))))\\n'
' (plan (call/cc (fn (k) (render-step-3 k name email)))))\\n'
' (submit-registration name email plan))))")'
f' {p}'
' "Each render-step-N shows a form and wires the \\\"Next\\\" button to invoke k '
'with the form value. The \\\"Back\\\" button invokes the previous step\\\'s continuation. '
'The wizard logic is a straight-line let* binding, not a state machine."))'
# --- Cooperative scheduling ---
' (~doc-section :title "Cooperative scheduling" :id "scheduling"'
f' {p}'
' "Delimited continuations (shift/reset rather than full call/cc) enable '
'cooperative multitasking within the evaluator. A long render can yield control:")'
f' {code}'
' ";; Render a large list, yielding every 100 items\\n'
'(define render-chunk\\n'
' (fn (items n)\\n'
' (when (> n 100)\\n'
' (yield) ;; delimited continuation — suspends, resumes next frame\\n'
' (set! n 0))\\n'
' (when (not (empty? items))\\n'
' (render-item (first items))\\n'
' (render-chunk (rest items) (+ n 1)))))")'
f' {p}'
' "This is cooperative concurrency without threads, without promises, '
'without requestAnimationFrame callbacks. The evaluator\'s trampoline loop '
'already has the right shape — it just needs to be able to park a thunk and '
'resume it later instead of immediately."))'
# --- Undo ---
' (~doc-section :title "Undo as continuation" :id "undo"'
f' {p}'
' "If you capture a continuation before a state mutation, '
'the continuation IS the undo operation. Invoking it restores the computation '
'to exactly the state it was in before the mutation happened.")'
f' {code}'
' "(define with-undo\\n'
' (fn (action)\\n'
' (let ((restore (call/cc (fn (k) k))))\\n'
' (action)\\n'
' restore)))\\n\\n'
';; Usage:\\n'
'(let ((undo (with-undo (fn () (delete-item 42)))))\\n'
' ;; later...\\n'
' (undo \\\"anything\\\")) ;; item 42 is back")'
f' {p}'
' "No command pattern, no reverse operations, no state snapshots. '
'The continuation captures the entire computation state. '
'This is the most elegant undo mechanism possible — '
'and the most expensive in memory, which is the trade-off."))'
# --- Implementation cost ---
' (~doc-section :title "Implementation" :id "implementation"'
f' {p}'
' "SX already has the foundation. The TCO trampoline returns thunks '
'from tail positions — a continuation is a thunk that can be stored and resumed later '
'instead of being immediately trampolined.")'
f' {p}'
' "The minimal implementation: delimited continuations via shift/reset. '
'These are strictly less powerful than full call/cc but cover the practical use cases '
'(suspense, cooperative scheduling, linear async flows) without the footguns '
'(capturing continuations across async boundaries, re-entering completed computations).")'
f' {p}'
' "Full call/cc is also possible. The evaluator is already '
'continuation-passing-style-adjacent — the thunk IS a continuation, '
'just one that\'s always immediately invoked. Making it first-class means '
'letting user code hold a reference to it.")'
f' {p}'
' "The key insight: having the primitive available doesn\'t make the evaluator '
'harder to reason about. Only code that calls call/cc pays the complexity cost. '
'Components that don\'t use continuations behave exactly as they do today."))'
# --- What this means for SX ---
' (~doc-section :title "What this means for SX" :id "meaning"'
f' {p}'
' "SX started as a rendering language. TCO made it capable of arbitrary recursion. '
'Macros made it extensible. Continuations would make it a full computational substrate — '
'a language where control flow itself is a first-class value.")'
f' {p}'
' "The practical benefits are real: streaming server rendering, '
'linear client-side interaction flows, cooperative scheduling, '
'and elegant undo. These aren\'t theoretical — they\'re patterns that '
'React, Clojure, and Scheme have proven work.")'
f' {p}'
' "The question isn\'t whether continuations are useful. '
'It\'s whether SX needs them now or whether async/await and HTMX cover enough. '
'The answer, for now, is that they\'re worth building — '
'because the evaluator is already 90%% of the way there, '
'and the remaining 10%% unlocks an entirely new class of UI patterns."))'
')'
)
# ---------------------------------------------------------------------------
# Wire-format partials (for sx-get requests)
# ---------------------------------------------------------------------------
def home_content_sx() -> str:
"""Home page content as sx wire format."""
hero_code = highlight('(div :class "p-4 bg-white rounded shadow"\n'
' (h1 :class "text-2xl font-bold" "Hello")\n'
' (button :sx-get "/api/data"\n'
' :sx-target "#result"\n'
' "Load data"))', "lisp")
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
f' (div :id "main-content"'
f' (~sx-hero {hero_code})'
f' (~sx-philosophy)'
f' (~sx-how-it-works)'
f' (~sx-credits)))'
)
async def docs_content_partial_sx(slug: str) -> str:
"""Docs content as sx wire format."""
inner = await _docs_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
f' {inner})'
)
async def reference_content_partial_sx(slug: str) -> str:
inner = await _reference_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
f' {inner})'
)
async def protocol_content_partial_sx(slug: str) -> str:
inner = await _protocol_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
f' {inner})'
)
async def examples_content_partial_sx(slug: str) -> str:
inner = await _examples_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
f' {inner})'
)
async def essay_content_partial_sx(slug: str) -> str:
inner = await _essay_content_sx(slug)
return (
f'(section :id "main-panel"'
f' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
f' {inner})'
)