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 from __future__ import annotations
import contextvars
import inspect import inspect
from typing import Any from typing import Any
from .types import Component, Keyword, Lambda, Macro, NIL, StyleValue, Symbol 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 .evaluator import _expand_macro, EvalError
from .primitives import _PRIMITIVES from .primitives import _PRIMITIVES
from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io from .primitives_io import IO_PRIMITIVES, RequestContext, execute_io
@@ -1058,6 +1066,24 @@ async def async_eval_slot_to_sx(
""" """
if ctx is None: if ctx is None:
ctx = RequestContext() 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 expr is a component call, expand it through _aser
if isinstance(expr, list) and expr: if isinstance(expr, list) and expr:
head = expr[0] head = expr[0]
@@ -1159,13 +1185,14 @@ async def _aser(expr: Any, env: dict[str, Any], ctx: RequestContext) -> Any:
if name.startswith("html:"): if name.startswith("html:"):
return await _aser_call(name[5:], expr[1:], env, ctx) 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("~"): if name.startswith("~"):
val = env.get(name) val = env.get(name)
if isinstance(val, Macro): if isinstance(val, Macro):
expanded = _expand_macro(val, expr[1:], env) expanded = _expand_macro(val, expr[1:], env)
return await _aser(expanded, env, ctx) 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_component(val, expr[1:], env, ctx)
return await _aser_call(name, 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 "SX Native" :href "/essays/sx-native")
(dict :label "The SX Manifesto" :href "/essays/sx-manifesto") (dict :label "The SX Manifesto" :href "/essays/sx-manifesto")
(dict :label "Tail-Call Optimization" :href "/essays/tail-call-optimization") (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. ;; Find the current nav label for a slug by matching href suffix.
;; Returns the label string or nil if no match. ;; Returns the label string or nil if no match.

View File

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

View File

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

View File

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