"""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