Some checks failed
Build and Deploy / build-and-deploy (push) Has been cancelled
Add sx-preserve/sx-ignore (morph skip), sx-indicator (loading element), sx-validate (form validation), sx-boost (progressive enhancement), sx-preload (hover prefetch with 30s cache), and sx-optimistic (instant UI preview with rollback). Move all from HTMX_MISSING_ATTRS to SX_UNIQUE_ATTRS with full ATTR_DETAILS docs and reference.sx demos. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
3030 lines
156 KiB
Python
3030 lines
156 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" "(<x>)")'),
|
|
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
|
|
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)
|
|
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 all '
|
|
f'available attributes and their status.")'
|
|
f' (div :class "space-y-8"'
|
|
f' {req}'
|
|
f' {beh}'
|
|
f' {uniq}))'
|
|
)
|
|
|
|
|
|
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.")'
|
|
f' {p}'
|
|
' "In fact, continuations can be easier to reason about than the hacks '
|
|
'people build to avoid them. Without call/cc, you get callback pyramids, '
|
|
'state machines with explicit transition tables, command pattern undo stacks, '
|
|
'Promise chains, manual CPS transforms, and framework-specific hooks '
|
|
'like React\'s useEffect/useSuspense/useTransition. Each is a partial, '
|
|
'ad-hoc reinvention of continuations — with its own rules, edge cases, and leaky abstractions.")'
|
|
f' {p}'
|
|
' "A wizard form built with continuations is a straight-line let* binding. '
|
|
'The same wizard built without them is a state machine with a current-step variable, '
|
|
'a data accumulator, forward/backward transition logic, and a render function '
|
|
'that switches on step number. The continuation version has fewer moving parts. '
|
|
'It is more declarative. It is easier to read.")'
|
|
f' {p}'
|
|
' "The complexity doesn\'t disappear when you remove continuations from a language. '
|
|
'It moves into user code, where it\'s harder to get right and harder to compose."))'
|
|
|
|
# --- 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 evaluator is already 90%% of the way there. '
|
|
'The remaining 10%% unlocks an entirely new class of UI patterns — '
|
|
'and eliminates an entire class of workarounds."))'
|
|
')'
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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})'
|
|
)
|