All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m10s
- OOB nav updates: AJAX navigation now swaps both menu bar levels (main nav highlighting + sub-nav with current page) using the same oob_header_sx/oob_page_sx pattern as blog/market/events - Enable OAuth for sx and test apps (removed from _NO_OAUTH, added sx to ALLOWED_CLIENTS, added app_urls for sx/test/orders) - Fetch real cross-service fragments (cart-mini, auth-menu, nav-tree) instead of hardcoding empty values - Add :selected param to ~menu-row-sx for white text current-page label - Fix duplicate element IDs: use menu-row-sx child_id/child mechanism instead of manual header_child_sx wrappers - Fix home page copy: "Server-rendered DOM over the wire (no HTML)" Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1215 lines
57 KiB
Python
1215 lines
57 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 (
|
|
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 <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}))'
|
|
)
|
|
|
|
|
|
def _docs_primitives_sx() -> str:
|
|
prims = _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 render_reference_page_sx(ctx: dict, slug: str) -> str:
|
|
"""Full page: reference section."""
|
|
from content.pages import REFERENCE_NAV
|
|
current = next((label for label, href in REFERENCE_NAV
|
|
if href.rstrip("/").endswith(slug or "reference")), "Attributes")
|
|
main_nav = _main_nav_sx("Reference")
|
|
sub_nav = _reference_nav_sx(current)
|
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Reference", "/reference/",
|
|
selected=current or "")
|
|
content = _reference_content_sx(slug)
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
def _reference_content_sx(slug: str) -> str:
|
|
builders = {
|
|
"": _reference_attrs_sx,
|
|
"attributes": _reference_attrs_sx,
|
|
"headers": _reference_headers_sx,
|
|
"events": _reference_events_sx,
|
|
"js-api": _reference_js_api_sx,
|
|
}
|
|
return builders.get(slug or "", _reference_attrs_sx)()
|
|
|
|
|
|
def _reference_attrs_sx() -> str:
|
|
from content.pages import REQUEST_ATTRS, BEHAVIOR_ATTRS, SX_UNIQUE_ATTRS, HTMX_MISSING_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 what exists, '
|
|
f'what\'s unique to sx, and what\'s not yet implemented.")'
|
|
f' (div :class "space-y-8"'
|
|
f' {_attr_table_sx("Request Attributes", REQUEST_ATTRS)}'
|
|
f' {_attr_table_sx("Behavior Attributes", BEHAVIOR_ATTRS)}'
|
|
f' {_attr_table_sx("Unique to sx", SX_UNIQUE_ATTRS)}'
|
|
f' {_attr_table_sx("htmx features not yet in sx", HTMX_MISSING_ATTRS)}))'
|
|
)
|
|
|
|
|
|
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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_protocol_page_sx(ctx: dict, slug: str) -> str:
|
|
"""Full page: protocols section."""
|
|
from content.pages import PROTOCOLS_NAV
|
|
current = next((label for label, href in PROTOCOLS_NAV if href.endswith(slug)), None)
|
|
main_nav = _main_nav_sx("Protocols")
|
|
sub_nav = _protocols_nav_sx(current)
|
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Protocols", "/protocols/wire-format",
|
|
selected=current or "")
|
|
content = _protocol_content_sx(slug)
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
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
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_examples_page_sx(ctx: dict, slug: str) -> str:
|
|
"""Full page: examples section."""
|
|
from content.pages import EXAMPLES_NAV
|
|
current = next((label for label, href in EXAMPLES_NAV if href.endswith(slug)), None)
|
|
main_nav = _main_nav_sx("Examples")
|
|
sub_nav = _examples_nav_sx(current)
|
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Examples", "/examples/click-to-load",
|
|
selected=current or "")
|
|
content = _examples_content_sx(slug)
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
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,
|
|
}
|
|
return builders.get(slug, _example_click_to_load_sx)()
|
|
|
|
|
|
def _example_click_to_load_sx() -> str:
|
|
c1 = _example_code('(button\n'
|
|
' :sx-get "/examples/api/click"\n'
|
|
' :sx-target "#click-result"\n'
|
|
' :sx-swap "innerHTML"\n'
|
|
' "Load content")')
|
|
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' {c1})'
|
|
)
|
|
|
|
|
|
def _example_form_submission_sx() -> str:
|
|
c1 = _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"))')
|
|
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' {c1})'
|
|
)
|
|
|
|
|
|
def _example_polling_sx() -> str:
|
|
c1 = _example_code('(div\n'
|
|
' :sx-get "/examples/api/poll"\n'
|
|
' :sx-trigger "load, every 2s"\n'
|
|
' :sx-swap "innerHTML"\n'
|
|
' "Loading...")')
|
|
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' {c1})'
|
|
)
|
|
|
|
|
|
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)
|
|
c1 = _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")')
|
|
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' {c1})'
|
|
)
|
|
|
|
|
|
def _example_inline_edit_sx() -> str:
|
|
c1 = _example_code(';; View mode\n'
|
|
'(button :sx-get "/api/edit?value=text"\n'
|
|
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
|
' "edit")\n\n'
|
|
';; Edit mode (returned by server)\n'
|
|
'(form :sx-post "/api/edit"\n'
|
|
' :sx-target "#edit-target" :sx-swap "innerHTML"\n'
|
|
' (input :type "text" :name "value")\n'
|
|
' (button :type "submit" "save"))')
|
|
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' {c1})'
|
|
)
|
|
|
|
|
|
def _example_oob_swaps_sx() -> str:
|
|
c1 = _example_code(';; Response body updates the target (Box A)\n'
|
|
';; OOB element updates Box B by ID\n\n'
|
|
'(<>\n'
|
|
' (div :class "text-center"\n'
|
|
' (p "Box A updated!")))\n'
|
|
' (div :id "oob-box-b" :sx-swap-oob "innerHTML"\n'
|
|
' (p "Box B updated via OOB!")))')
|
|
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' {c1})'
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# Essays
|
|
# ---------------------------------------------------------------------------
|
|
|
|
async def render_essay_page_sx(ctx: dict, slug: str) -> str:
|
|
"""Full page: essays section."""
|
|
from content.pages import ESSAYS_NAV
|
|
current = next((label for label, href in ESSAYS_NAV if href.endswith(slug)), None)
|
|
main_nav = _main_nav_sx("Essays")
|
|
sub_nav = _essays_nav_sx(current)
|
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Essays", "/essays/sx-sucks",
|
|
selected=current or "")
|
|
content = _essay_content_sx(slug)
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
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,
|
|
}
|
|
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.")))'
|
|
)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# 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)))'
|
|
)
|
|
|
|
|
|
def docs_content_partial_sx(slug: str) -> str:
|
|
"""Docs content as sx wire format."""
|
|
inner = _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})'
|
|
)
|
|
|
|
|
|
def reference_content_partial_sx(slug: str) -> str:
|
|
inner = _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})'
|
|
)
|
|
|
|
|
|
def protocol_content_partial_sx(slug: str) -> str:
|
|
inner = _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})'
|
|
)
|
|
|
|
|
|
def examples_content_partial_sx(slug: str) -> str:
|
|
inner = _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})'
|
|
)
|
|
|
|
|
|
def essay_content_partial_sx(slug: str) -> str:
|
|
inner = _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})'
|
|
)
|