"""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, root_header_sx, full_page_sx, header_child_sx, ) # 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) # --------------------------------------------------------------------------- # 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) -> 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", icon="fa fa-code", nav=SxExpr(nav) if nav else None, child_id="sx-header-child", ) 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 _header_stack_sx(ctx: dict, section_nav: str | None = None) -> str: """Full header stack: root header + sx menu row.""" hdr = root_header_sx(ctx) inner = _sx_header_sx(section_nav) child = header_child_sx(inner) return "(<> " + hdr + " " + child + ")" def _section_header_stack_sx(ctx: dict, main_nav: str, sub_nav: str, sub_label: str, sub_href: str) -> str: """Header stack with main nav + sub-section nav row.""" hdr = root_header_sx(ctx) sx_row = _sx_header_sx(main_nav) sub_row = sx_call("menu-row-sx", id="sx-sub-row", level=2, colour="violet", link_href=sub_href, link_label=sub_label, nav=SxExpr(sub_nav), ) inner = "(<> " + sx_row + " " + header_child_sx(sub_row, id="sx-header-child") + ")" child = header_child_sx(inner) return "(<> " + hdr + " " + child + ")" # --------------------------------------------------------------------------- # 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)}))))' ) # --------------------------------------------------------------------------- # Page renderers — async functions returning full HTML # --------------------------------------------------------------------------- async def render_home_page_sx(ctx: dict) -> str: """Full page: home.""" main_nav = _main_nav_sx() hdr = _header_stack_sx(ctx, main_nav) content = ( '(div :id "main-content"' ' (~sx-hero)' ' (~sx-philosophy)' ' (~sx-how-it-works)' ' (~sx-credits))' ) return full_page_sx(ctx, header_rows=hdr, content=content) async def render_docs_page_sx(ctx: dict, slug: str) -> str: """Full page: docs section.""" from content.pages import DOCS_NAV current = next((label for label, href in DOCS_NAV if href.endswith(slug)), None) main_nav = _main_nav_sx("Docs") sub_nav = _docs_nav_sx(current) hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Docs", "/docs/introduction") content = _docs_content_sx(slug) return full_page_sx(ctx, header_rows=hdr, content=content) 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: return ( '(~doc-page :title "Getting Started"' ' (~doc-section :title "Minimal example" :id "minimal"' ' (p :class "text-stone-600"' ' "An sx response is s-expression source code with content type text/sx:")' ' (~doc-code :language "lisp" :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.\\"))")' ' (p :class "text-stone-600"' ' "Add sx-get to any element to make it fetch and render sx:"))' ' (~doc-section :title "Hypermedia attributes" :id "attrs"' ' (p :class "text-stone-600"' ' "Like htmx, sx adds attributes to HTML elements to trigger HTTP requests:")' ' (~doc-code :language "lisp" :code' ' "(button\\n' ' :sx-get \\"/api/data\\"\\n' ' :sx-target \\"#result\\"\\n' ' :sx-swap \\"innerHTML\\"\\n' ' \\"Load data\\")")' ' (p :class "text-stone-600"' ' "sx-get, sx-post, sx-put, sx-delete, sx-patch — all work the same way. ' 'The response is parsed as sx and rendered into the target element.")))' ) def _docs_components_sx() -> str: return ( '(~doc-page :title "Components"' ' (~doc-section :title "defcomp" :id "defcomp"' ' (p :class "text-stone-600"' ' "Components are defined with defcomp. They take keyword parameters and optional children:")' ' (~doc-code :language "lisp" :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)))")' ' (p :class "text-stone-600"' ' "Use components with the ~ prefix:") ' ' (~doc-code :language "lisp" :code' ' "(~card :title \\"My Card\\" :subtitle \\"A description\\"\\n' ' (p \\"First child\\")\\n' ' (p \\"Second child\\"))"))' ' (~doc-section :title "Component caching" :id "caching"' ' (p :class "text-stone-600"' ' "Component definitions are sent in a