Gate server-side component expansion with contextvar, fix nth arg order, add GEB essay and manifesto links

- Add _expand_components contextvar so _aser only expands components
  during page slot evaluation (fixes highlight on examples, avoids
  breaking fragment responses)
- Fix nth arg order (nth coll n) in docs.sx, examples.sx (delete-row,
  edit-row, bulk-update)
- Add "Godel, Escher, Bach and SX" essay with Wikipedia links
- Update SX Manifesto: new authors, Wikipedia links throughout,
  remove Marx/Engels link

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 11:03:50 +00:00
parent 4a515f1a0d
commit 6fa843016b
6 changed files with 43 additions and 11 deletions

View File

@@ -41,10 +41,18 @@ Usage::
from __future__ import annotations
import contextvars
import inspect
from typing import Any
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol
# When True, _aser expands known components server-side instead of serializing
# them for client rendering. Set during page slot evaluation so Python-only
# helpers (e.g. highlight) in component bodies execute on the server.
_expand_components: contextvars.ContextVar[bool] = contextvars.ContextVar(
"_expand_components", default=False
)
from .evaluator import _expand_macro, EvalError
from .primitives import _PRIMITIVES
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
@@ -1058,6 +1066,24 @@ async def async_eval_slot_to_sx(
"""
if ctx is None:
ctx = RequestContext()
# Enable server-side component expansion for this slot evaluation.
# This lets _aser expand known components (so Python-only helpers
# like highlight execute server-side) instead of serializing them
# for client rendering.
token = _expand_components.set(True)
try:
return await _eval_slot_inner(expr, env, ctx)
finally:
_expand_components.reset(token)
async def _eval_slot_inner(
expr: Any,
env: dict[str, Any],
ctx: RequestContext,
) -> str:
"""Inner implementation — runs with _expand_components=True."""
# If expr is a component call, expand it through _aser
if isinstance(expr, list) and expr:
head = expr[0]
@@ -1159,13 +1185,14 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
if name.startswith("html:"):
return await _aser_call(name[5:], expr[1:], env, ctx)
# Component call — expand macros, expand known components, serialize unknown
# Component call — expand macros, expand known components (in slot
# eval context only), serialize unknown
if name.startswith("~"):
val = env.get(name)
if isinstance(val, Macro):
expanded = _expand_macro(val, expr[1:], env)
return await _aser(expanded, env, ctx)
if isinstance(val, Component):
if isinstance(val, Component) and _expand_components.get():
return await _aser_component(val, expr[1:], env, ctx)
return await _aser_call(name, expr[1:], env, ctx)

File diff suppressed because one or more lines are too long

View File

@@ -63,7 +63,8 @@
(dict :label "SX Native" :href "/essays/sx-native")
(dict :label "The SX Manifesto" :href "/essays/sx-manifesto")
(dict :label "Tail-Call Optimization" :href "/essays/tail-call-optimization")
(dict :label "Continuations" :href "/essays/continuations")))
(dict :label "Continuations" :href "/essays/continuations")
(dict :label "Godel, Escher, Bach" :href "/essays/godel-escher-bach")))
;; Find the current nav label for a slug by matching href suffix.
;; Returns the label string or nil if no match.

View File

@@ -61,15 +61,15 @@
(defcomp ~doc-nav (&key items current)
(nav :class "flex flex-wrap gap-2 mb-8"
(map (fn (item)
(a :href (nth 1 item)
:sx-get (nth 1 item)
(a :href (nth item 1)
:sx-get (nth item 1)
:sx-target "#main-panel"
:sx-select "#main-panel"
:sx-swap "outerHTML"
:sx-push-url "true"
:class (str "px-3 py-1.5 rounded text-sm font-medium no-underline "
(if (= (nth 0 item) current)
(if (= (nth item 0) current)
"bg-violet-100 text-violet-800"
"bg-stone-100 text-stone-600 hover:bg-stone-200"))
(nth 0 item)))
(nth item 0)))
items)))

View File

@@ -91,7 +91,7 @@
(th :class "px-3 py-2 font-medium text-stone-600 w-20" "")))
(tbody :id "delete-rows"
(map (fn (item)
(~delete-row :id (nth 0 item) :name (nth 1 item)))
(~delete-row :id (nth item 0) :name (nth item 1)))
items)))))
(defcomp ~delete-row (&key id name)
@@ -344,7 +344,7 @@
(th :class "px-3 py-2 font-medium text-stone-600 w-24" "")))
(tbody :id "edit-rows"
(map (fn (row)
(~edit-row-view :id (nth 0 row) :name (nth 1 row) :price (nth 2 row) :stock (nth 3 row)))
(~edit-row-view :id (nth row 0) :name (nth row 1) :price (nth row 2) :stock (nth row 3)))
rows)))))
(defcomp ~edit-row-view (&key id name price stock)
@@ -415,7 +415,7 @@
(th :class "px-3 py-2 font-medium text-stone-600" "Status")))
(tbody :id "bulk-table"
(map (fn (u)
(~bulk-row :id (nth 0 u) :name (nth 1 u) :email (nth 2 u) :status (nth 3 u)))
(~bulk-row :id (nth u 0) :name (nth u 1) :email (nth u 2) :status (nth u 3)))
users))))))
(defcomp ~bulk-row (&key id name email status)

View File

@@ -239,4 +239,5 @@
"sx-manifesto" (~essay-sx-manifesto)
"tail-call-optimization" (~essay-tail-call-optimization)
"continuations" (~essay-continuations)
"godel-escher-bach" (~essay-godel-escher-bach)
:else (~essay-sx-sucks)))