"""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, root_header_sx, full_page_sx, oob_header_sx, oob_page_sx, ) 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 _full_page(ctx: dict, **kwargs) -> str: """full_page_sx wrapper.""" return full_page_sx(ctx, **kwargs) def _code(code: str, language: str = "lisp") -> str: """Build a ~doc-code component with highlighted content.""" highlighted = highlight(code, language) return f'(~doc-code {highlighted})' def _example_code(code: str) -> str: """Build an ~example-source component with highlighted content.""" highlighted = highlight(code, "lisp") return f'(~example-source {highlighted})' # --------------------------------------------------------------------------- # 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", icon="fa fa-code", 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 _header_stack_sx(ctx: dict, section_nav: str | None = None) -> str: """Full header stack: root header + sx menu row.""" hdr = root_header_sx(ctx) sx_row = _sx_header_sx(section_nav) return "(<> " + hdr + " " + sx_row + ")" 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), ) def _section_header_stack_sx(ctx: dict, main_nav: str, sub_nav: str, sub_label: str, sub_href: str, selected: str = "") -> str: """Header stack with main nav + sub-section nav row.""" hdr = root_header_sx(ctx) sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected) sx_row = _sx_header_sx(main_nav, child=sub_row) return "(<> " + hdr + " " + sx_row + ")" # --------------------------------------------------------------------------- # OOB helpers — rebuild header rows for AJAX navigation # --------------------------------------------------------------------------- async def _section_oob_sx(section: str, sub_label: str, sub_href: str, sub_nav: str, content: str, selected: str = "") -> str: """Generic OOB response: rebuild both header rows + content.""" from shared.sx.page import get_template_context ctx = await get_template_context() root_hdr = root_header_sx(ctx) main_nav = _main_nav_sx(section) sub_row = _sub_row_sx(sub_label, sub_href, sub_nav, selected) sx_row = _sx_header_sx(main_nav, child=sub_row) rows = "(<> " + root_hdr + " " + sx_row + ")" header_oob = oob_header_sx("root-header-child", "sx-header-child", rows) return oob_page_sx(oobs=header_oob, content=content) async def home_oob_sx() -> str: """OOB response for home page navigation.""" from shared.sx.page import get_template_context ctx = await get_template_context() root_hdr = root_header_sx(ctx) main_nav = _main_nav_sx() sx_row = _sx_header_sx(main_nav) rows = "(<> " + root_hdr + " " + sx_row + ")" header_oob = oob_header_sx("root-header-child", "sx-header-child", rows) 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") content = ( f'(div :id "main-content"' f' (~sx-hero {hero_code})' f' (~sx-philosophy)' f' (~sx-how-it-works)' f' (~sx-credits))' ) return oob_page_sx(oobs=header_oob, content=content) async def docs_oob_sx(slug: str) -> str: """OOB response for docs section navigation.""" from content.pages import DOCS_NAV current = next((label for label, href in DOCS_NAV if href.endswith(slug)), None) sub_nav = _docs_nav_sx(current) return await _section_oob_sx("Docs", "Docs", "/docs/introduction", sub_nav, _docs_content_sx(slug), selected=current or "") async def reference_oob_sx(slug: str) -> str: """OOB response for reference section navigation.""" from content.pages import REFERENCE_NAV current = next((label for label, href in REFERENCE_NAV if href.rstrip("/").endswith(slug or "reference")), "Attributes") sub_nav = _reference_nav_sx(current) return await _section_oob_sx("Reference", "Reference", "/reference/", sub_nav, _reference_content_sx(slug), selected=current or "") async def protocol_oob_sx(slug: str) -> str: """OOB response for protocols section navigation.""" from content.pages import PROTOCOLS_NAV current = next((label for label, href in PROTOCOLS_NAV if href.endswith(slug)), None) sub_nav = _protocols_nav_sx(current) return await _section_oob_sx("Protocols", "Protocols", "/protocols/wire-format", sub_nav, _protocol_content_sx(slug), selected=current or "") async def examples_oob_sx(slug: str) -> str: """OOB response for examples section navigation.""" from content.pages import EXAMPLES_NAV current = next((label for label, href in EXAMPLES_NAV if href.endswith(slug)), None) sub_nav = _examples_nav_sx(current) return await _section_oob_sx("Examples", "Examples", "/examples/click-to-load", sub_nav, _examples_content_sx(slug), selected=current or "") async def essay_oob_sx(slug: str) -> str: """OOB response for essays section navigation.""" from content.pages import ESSAYS_NAV current = next((label for label, href in ESSAYS_NAV if href.endswith(slug)), None) sub_nav = _essays_nav_sx(current) return await _section_oob_sx("Essays", "Essays", "/essays/sx-sucks", sub_nav, _essay_content_sx(slug), selected=current or "") # --------------------------------------------------------------------------- # 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) 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") content = ( f'(div :id "main-content"' f' (~sx-hero {hero_code})' f' (~sx-philosophy)' f' (~sx-how-it-works)' f' (~sx-credits))' ) return _full_page(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", selected=current or "") content = _docs_content_sx(slug) return _full_page(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: 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