"""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 (
sx_call, 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'')
parts.append('')
# 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
# ---------------------------------------------------------------------------
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(sx_call("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) + ")"
def _sx_header_sx(nav: str | None = None, *, child: str | None = None) -> str:
"""Build the sx docs menu-row."""
return sx_call("menu-row-sx",
id="sx-row", level=1, colour="violet",
link_href="/", link_label="sx",
link_label_content=SxExpr('(span :class "font-mono" "(>) sx")'),
nav=SxExpr(nav) if nav else None,
child_id="sx-header-child",
child=SxExpr(child) if child else None,
)
def _docs_nav_sx(current: str | None = None) -> str:
from content.pages import DOCS_NAV
return _nav_items_sx(DOCS_NAV, current)
def _reference_nav_sx(current: str | None = None) -> str:
from content.pages import REFERENCE_NAV
return _nav_items_sx(REFERENCE_NAV, current)
def _protocols_nav_sx(current: str | None = None) -> str:
from content.pages import PROTOCOLS_NAV
return _nav_items_sx(PROTOCOLS_NAV, current)
def _examples_nav_sx(current: str | None = None) -> str:
from content.pages import EXAMPLES_NAV
return _nav_items_sx(EXAMPLES_NAV, current)
def _essays_nav_sx(current: str | None = None) -> str:
from content.pages import ESSAYS_NAV
return _nav_items_sx(ESSAYS_NAV, current)
def _main_nav_sx(current_section: str | None = None) -> str:
from content.pages import MAIN_NAV
return _nav_items_sx(MAIN_NAV, current_section)
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 sx_call("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
# ---------------------------------------------------------------------------
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 sx_call("doc-nav", items=SxExpr(f"(list {items_sx})"), current=current)
def _attr_table_sx(title: str, attrs: list[tuple[str, str, bool]]) -> str:
"""Build an attribute reference table."""
rows = []
for attr, desc, exists in attrs:
rows.append(sx_call("doc-attr-row", attr=attr, description=desc,
exists="true" if exists else None))
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)}))))'
)
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(sx_call("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)}))))'
)
def _docs_content_sx(slug: str) -> str:
"""Route to the right docs content builder."""
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)
return builder()
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 is not trying to replace JavaScript. It\'s trying to replace the pattern of '
'shipping a JS framework + build step + client-side router + state management library '
'just to render some server data into HTML."))'
' (~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 Lisp implementation — no macros, no continuations, no tail-call optimization")'
' (li "Not a replacement for JavaScript — it handles rendering, not arbitrary DOM manipulation")'
' (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