|
|
|
|
@@ -5,9 +5,11 @@ 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,
|
|
|
|
|
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__)
|
|
|
|
|
@@ -15,6 +17,23 @@ 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
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
@@ -31,13 +50,14 @@ def _nav_items_sx(items: list[tuple[str, str]], current: str | None = None) -> s
|
|
|
|
|
return "(<> " + " ".join(parts) + ")"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _sx_header_sx(nav: str | None = None) -> str:
|
|
|
|
|
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,
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -74,24 +94,118 @@ def _main_nav_sx(current_section: str | None = None) -> str:
|
|
|
|
|
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 + ")"
|
|
|
|
|
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) -> 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)
|
|
|
|
|
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),
|
|
|
|
|
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))'
|
|
|
|
|
)
|
|
|
|
|
inner = "(<> " + sx_row + " " + header_child_sx(sub_row, id="sx-header-child") + ")"
|
|
|
|
|
child = header_child_sx(inner)
|
|
|
|
|
return "(<> " + hdr + " " + child + ")"
|
|
|
|
|
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 "")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
@@ -169,14 +283,19 @@ 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 = (
|
|
|
|
|
'(div :id "main-content"'
|
|
|
|
|
' (~sx-hero)'
|
|
|
|
|
' (~sx-philosophy)'
|
|
|
|
|
' (~sx-how-it-works)'
|
|
|
|
|
' (~sx-credits))'
|
|
|
|
|
f'(div :id "main-content"'
|
|
|
|
|
f' (~sx-hero {hero_code})'
|
|
|
|
|
f' (~sx-philosophy)'
|
|
|
|
|
f' (~sx-how-it-works)'
|
|
|
|
|
f' (~sx-credits))'
|
|
|
|
|
)
|
|
|
|
|
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
|
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def render_docs_page_sx(ctx: dict, slug: str) -> str:
|
|
|
|
|
@@ -185,9 +304,10 @@ async def render_docs_page_sx(ctx: dict, slug: str) -> str:
|
|
|
|
|
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")
|
|
|
|
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Docs", "/docs/introduction",
|
|
|
|
|
selected=current or "")
|
|
|
|
|
content = _docs_content_sx(slug)
|
|
|
|
|
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
|
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _docs_content_sx(slug: str) -> str:
|
|
|
|
|
@@ -235,98 +355,92 @@ def _docs_introduction_sx() -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
'(~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.")))'
|
|
|
|
|
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 (
|
|
|
|
|
'(~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 <script type=\\"text/sx\\" data-components> block. '
|
|
|
|
|
'The client caches them in localStorage keyed by a content hash. '
|
|
|
|
|
'On subsequent page loads, the client sends an SX-Components header listing what it has. '
|
|
|
|
|
'The server only sends definitions the client is missing.")'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "This means the first page load sends all component definitions (~5-15KB). '
|
|
|
|
|
'Subsequent navigations send zero component bytes — just the page content.")) '
|
|
|
|
|
' (~doc-section :title "Parameters" :id "params"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "&key declares keyword parameters. &rest children captures remaining positional arguments. '
|
|
|
|
|
'Missing parameters evaluate to nil. Components always receive all declared parameters — '
|
|
|
|
|
'use (when param ...) or (if param ... ...) to handle optional values.")))'
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Evaluator"'
|
|
|
|
|
' (~doc-section :title "Special forms" :id "special"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "Special forms have lazy evaluation — arguments are not evaluated before the form runs:")'
|
|
|
|
|
' (~doc-code :language "lisp" :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))"))'
|
|
|
|
|
' (~doc-section :title "Higher-order forms" :id "higher"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "These operate on collections with function arguments:")'
|
|
|
|
|
' (~doc-code :language "lisp" :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")))'
|
|
|
|
|
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}))'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -343,78 +457,78 @@ def _docs_primitives_sx() -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "On-Demand CSS"'
|
|
|
|
|
' (~doc-section :title "How it works" :id "how"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "sx scans every response for CSS class names used in :class attributes. '
|
|
|
|
|
'It looks up only those classes in a pre-parsed Tailwind CSS registry and ships '
|
|
|
|
|
'just the rules that are needed. No build step. No purging. No unused CSS.")'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "On the first page load, the full set of used classes is embedded in a <style> block. '
|
|
|
|
|
'A hash of the class set is stored. On subsequent navigations, the client sends the hash '
|
|
|
|
|
'in the SX-Css header. The server computes the diff and sends only new rules via '
|
|
|
|
|
'SX-Css-Add and a <style data-sx-css> block."))'
|
|
|
|
|
' (~doc-section :title "The protocol" :id "protocol"'
|
|
|
|
|
' (~doc-code :language "bash" :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>"))'
|
|
|
|
|
' (~doc-section :title "Advantages" :id "advantages"'
|
|
|
|
|
' (ul :class "space-y-2 text-stone-600"'
|
|
|
|
|
' (li "Zero build step — no Tailwind CLI, no PostCSS, no purging")'
|
|
|
|
|
' (li "Exact CSS — never ships a rule that isn\'t used on the page")'
|
|
|
|
|
' (li "Incremental — subsequent navigations only ship new rules")'
|
|
|
|
|
' (li "Component-aware — pre-scans component definitions at registration time")))'
|
|
|
|
|
' (~doc-section :title "Disadvantages" :id "disadvantages"'
|
|
|
|
|
' (ul :class "space-y-2 text-stone-600"'
|
|
|
|
|
' (li "Requires the full Tailwind CSS file loaded in memory at startup (~4MB parsed)")'
|
|
|
|
|
' (li "Regex-based class scanning — can miss dynamically constructed class names")'
|
|
|
|
|
' (li "No @apply support — classes must be used directly")'
|
|
|
|
|
' (li "Tied to Tailwind\'s utility class naming conventions"))))'
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Server Rendering"'
|
|
|
|
|
' (~doc-section :title "Python API" :id "python"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "The server-side sx library provides several entry points for rendering:")'
|
|
|
|
|
' (~doc-code :language "python" :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)"))'
|
|
|
|
|
' (~doc-section :title "sx_call" :id "sx-call"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "sx_call converts Python kwargs to an s-expression component call. '
|
|
|
|
|
'Snake_case becomes kebab-case. SxExpr values are inlined without quoting. '
|
|
|
|
|
'None becomes nil. Bools become true/false."))'
|
|
|
|
|
' (~doc-section :title "sx_response" :id "sx-response"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "sx_response returns a Quart Response with content type text/sx. '
|
|
|
|
|
'It prepends missing component definitions, scans for CSS classes, '
|
|
|
|
|
'and sets SX-Css-Hash and SX-Css-Add headers."))'
|
|
|
|
|
' (~doc-section :title "sx_page" :id "sx-page"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "sx_page returns a minimal HTML document that boots the page from sx source. '
|
|
|
|
|
'The browser loads component definitions and page sx from inline <script> tags, '
|
|
|
|
|
'then sx.js renders everything client-side. CSS rules are pre-scanned and injected.")))'
|
|
|
|
|
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.")))'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -429,9 +543,10 @@ async def render_reference_page_sx(ctx: dict, slug: str) -> str:
|
|
|
|
|
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/")
|
|
|
|
|
hdr = _section_header_stack_sx(ctx, main_nav, sub_nav, "Reference", "/reference/",
|
|
|
|
|
selected=current or "")
|
|
|
|
|
content = _reference_content_sx(slug)
|
|
|
|
|
return full_page_sx(ctx, header_rows=hdr, content=content)
|
|
|
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _reference_content_sx(slug: str) -> str:
|
|
|
|
|
@@ -526,9 +641,10 @@ async def render_protocol_page_sx(ctx: dict, slug: str) -> str:
|
|
|
|
|
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")
|
|
|
|
|
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_sx(ctx, header_rows=hdr, content=content)
|
|
|
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _protocol_content_sx(slug: str) -> str:
|
|
|
|
|
@@ -544,18 +660,18 @@ def _protocol_content_sx(slug: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Wire Format"'
|
|
|
|
|
' (~doc-section :title "The text/sx content type" :id "content-type"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "sx responses use content type text/sx. The body is s-expression source code. '
|
|
|
|
|
'The client parses and evaluates it, then renders the result into the DOM.")'
|
|
|
|
|
' (~doc-code :language "bash" :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\\"))"))'
|
|
|
|
|
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.")'
|
|
|
|
|
@@ -578,16 +694,16 @@ def _protocol_wire_format_sx() -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _protocol_fragments_sx() -> str:
|
|
|
|
|
c1 = _code('(frag "blog" "link-card" :slug "hello")')
|
|
|
|
|
return (
|
|
|
|
|
'(~doc-page :title "Cross-Service Fragments"'
|
|
|
|
|
' (~doc-section :title "Fragment protocol" :id "protocol"'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "Rose Ash runs as independent microservices. Each service can expose HTML or sx fragments '
|
|
|
|
|
'that other services compose into their pages. Fragment endpoints return text/sx or text/html.")'
|
|
|
|
|
' (p :class "text-stone-600"'
|
|
|
|
|
' "The frag resolver is an I/O primitive in the render tree:") '
|
|
|
|
|
' (~doc-code :language "lisp" :code'
|
|
|
|
|
' "(frag \\"blog\\" \\"link-card\\" :slug \\"hello\\")"))'
|
|
|
|
|
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 '
|
|
|
|
|
@@ -700,9 +816,10 @@ async def render_examples_page_sx(ctx: dict, slug: str) -> str:
|
|
|
|
|
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")
|
|
|
|
|
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_sx(ctx, header_rows=hdr, content=content)
|
|
|
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _examples_content_sx(slug: str) -> str:
|
|
|
|
|
@@ -718,60 +835,66 @@ def _examples_content_sx(slug: str) -> str:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Click to Load"'
|
|
|
|
|
' (p :class "text-stone-600 mb-6"'
|
|
|
|
|
' "The simplest sx interaction: click a button, fetch content from the server, swap it in.")'
|
|
|
|
|
' (~example-card :title "Demo"'
|
|
|
|
|
' :description "Click the button to load server-rendered content."'
|
|
|
|
|
' (~example-demo (~click-to-load-demo)))'
|
|
|
|
|
' (~example-source :code'
|
|
|
|
|
' "(button\\n'
|
|
|
|
|
' :sx-get \\"/examples/api/click\\"\\n'
|
|
|
|
|
' :sx-target \\"#click-result\\"\\n'
|
|
|
|
|
' :sx-swap \\"innerHTML\\"\\n'
|
|
|
|
|
' \\"Load content\\")"))'
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Form Submission"'
|
|
|
|
|
' (p :class "text-stone-600 mb-6"'
|
|
|
|
|
' "Forms with sx-post submit via AJAX and swap the response into a target.")'
|
|
|
|
|
' (~example-card :title "Demo"'
|
|
|
|
|
' :description "Enter a name and submit."'
|
|
|
|
|
' (~example-demo (~form-demo)))'
|
|
|
|
|
' (~example-source :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\\"))"))'
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Polling"'
|
|
|
|
|
' (p :class "text-stone-600 mb-6"'
|
|
|
|
|
' "Use sx-trigger with \\"every\\\" to poll the server at regular intervals.")'
|
|
|
|
|
' (~example-card :title "Demo"'
|
|
|
|
|
' :description "This div polls the server every 2 seconds."'
|
|
|
|
|
' (~example-demo (~polling-demo)))'
|
|
|
|
|
' (~example-source :code'
|
|
|
|
|
' "(div\\n'
|
|
|
|
|
' :sx-get \\"/examples/api/poll\\"\\n'
|
|
|
|
|
' :sx-trigger \\"load, every 2s\\"\\n'
|
|
|
|
|
' :sx-swap \\"innerHTML\\"\\n'
|
|
|
|
|
' \\"Loading...\\")"))'
|
|
|
|
|
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"'
|
|
|
|
|
@@ -779,53 +902,47 @@ def _example_delete_row_sx() -> str:
|
|
|
|
|
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' (~example-source :code'
|
|
|
|
|
f' "(button\\n'
|
|
|
|
|
f' :sx-delete \\"/api/delete/1\\"\\n'
|
|
|
|
|
f' :sx-target \\"#row-1\\"\\n'
|
|
|
|
|
f' :sx-swap \\"outerHTML\\"\\n'
|
|
|
|
|
f' :sx-confirm \\"Delete this item?\\"\\n'
|
|
|
|
|
f' \\"delete\\")"))'
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Inline Edit"'
|
|
|
|
|
' (p :class "text-stone-600 mb-6"'
|
|
|
|
|
' "Click edit to swap a display view for an edit form. Save swaps back.")'
|
|
|
|
|
' (~example-card :title "Demo"'
|
|
|
|
|
' :description "Click edit, modify the text, save or cancel."'
|
|
|
|
|
' (~example-demo (~inline-edit-demo)))'
|
|
|
|
|
' (~example-source :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\\"))"))'
|
|
|
|
|
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 (
|
|
|
|
|
'(~doc-page :title "Out-of-Band Swaps"'
|
|
|
|
|
' (p :class "text-stone-600 mb-6"'
|
|
|
|
|
' "sx-swap-oob lets a single response update multiple elements anywhere in the DOM.")'
|
|
|
|
|
' (~example-card :title "Demo"'
|
|
|
|
|
' :description "One request updates both Box A (via sx-target) and Box B (via sx-swap-oob)."'
|
|
|
|
|
' (~example-demo (~oob-demo)))'
|
|
|
|
|
' (~example-source :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!\\"))))"))'
|
|
|
|
|
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})'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -839,9 +956,10 @@ async def render_essay_page_sx(ctx: dict, slug: str) -> str:
|
|
|
|
|
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")
|
|
|
|
|
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_sx(ctx, header_rows=hdr, content=content)
|
|
|
|
|
return _full_page(ctx, header_rows=hdr, content=content)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _essay_content_sx(slug: str) -> str:
|
|
|
|
|
@@ -878,7 +996,7 @@ def _essay_sx_sucks() -> str:
|
|
|
|
|
'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."))'
|
|
|
|
|
' "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}'
|
|
|
|
|
@@ -898,6 +1016,11 @@ def _essay_sx_sucks() -> str:
|
|
|
|
|
'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."))'
|
|
|
|
|
|
|
|
|
|
@@ -1029,14 +1152,19 @@ def _essay_on_demand_css() -> str:
|
|
|
|
|
|
|
|
|
|
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 (
|
|
|
|
|
'(section :id "main-panel"'
|
|
|
|
|
' :class "flex-1 md:h-full md:min-h-0 overflow-y-auto overscroll-contain js-grid-viewport"'
|
|
|
|
|
' (div :id "main-content"'
|
|
|
|
|
' (~sx-hero)'
|
|
|
|
|
' (~sx-philosophy)'
|
|
|
|
|
' (~sx-how-it-works)'
|
|
|
|
|
' (~sx-credits)))'
|
|
|
|
|
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)))'
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|