Fix SX client navigation: path-derived names, provide clash, component expansion

- inject_path_name: strip _islands/ convention dirs from path-derived names
- page-functions.sx: fix geography (→ ~geography) and isomorphism (→ ~etc/plan/isomorphic)
- request-handler.sx: rewrite sx-eval-page to call page functions explicitly
  via env-get+apply, avoiding provide special form intercepting (provide) calls
- sx_server.ml: set expand-components? on AJAX aser paths so server-side
  components expand for the browser (islands stay unexpanded for hydration)
- Rename 19 component references in geography/spreads, geography/provide,
  geography/scopes to use path-qualified names matching inject_path_name output

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-18 10:19:00 +00:00
parent 9e0de8831f
commit ac65666f6f
7 changed files with 400 additions and 264 deletions

View File

@@ -1240,6 +1240,9 @@ let inject_path_name expr path base_dir =
let stem = if Filename.check_suffix rel ".sx" let stem = if Filename.check_suffix rel ".sx"
then String.sub rel 0 (String.length rel - 3) then String.sub rel 0 (String.length rel - 3)
else rel in else rel in
(* Strip _islands/ convention directories from the path *)
let stem = let parts = String.split_on_char '/' stem in
String.concat "/" (List.filter (fun p -> p <> "_islands") parts) in
(* index files are known by their directory *) (* index files are known by their directory *)
let name = if Filename.basename stem = "index" let name = if Filename.basename stem = "index"
then let d = Filename.dirname stem in then let d = Filename.dirname stem in
@@ -2165,10 +2168,14 @@ let http_render_page env path headers =
let inner_layout = get_app_str "inner-layout" "~layouts/doc" in let inner_layout = get_app_str "inner-layout" "~layouts/doc" in
let wrapped = List [Symbol inner_layout; Keyword "path"; String nav_path; page_ast] in let wrapped = List [Symbol inner_layout; Keyword "path"; String nav_path; page_ast] in
if is_ajax then begin if is_ajax then begin
(* AJAX: return SX wire format (aser output) with text/sx content type *) (* AJAX: return SX wire format (aser output) with text/sx content type.
Expand server-side components so the browser doesn't need their definitions.
Islands stay as (~ ...) calls — the browser hydrates those. *)
ignore (env_bind env "expand-components?" (NativeFn ("expand-components?", fun _args -> Bool true)));
let body_result = let body_result =
let call = List [Symbol "aser"; List [Symbol "quote"; wrapped]; Env env] in let call = List [Symbol "aser"; List [Symbol "quote"; wrapped]; Env env] in
eval_with_io_render call env in eval_with_io_render call env in
Hashtbl.remove env.bindings (Sx_types.intern "expand-components?");
let body_str = match body_result with let body_str = match body_result with
| String s | SxExpr s -> s | _ -> serialize_value body_result in | String s | SxExpr s -> s | _ -> serialize_value body_result in
let t1 = Unix.gettimeofday () in let t1 = Unix.gettimeofday () in
@@ -4078,6 +4085,7 @@ let http_mode port =
if is_ajax then begin if is_ajax then begin
(* AJAX streaming: evaluate shell + data + content synchronously, (* AJAX streaming: evaluate shell + data + content synchronously,
return fully-resolved SX wire format (no chunked transfer). *) return fully-resolved SX wire format (no chunked transfer). *)
ignore (env_bind env "expand-components?" (NativeFn ("expand-components?", fun _args -> Bool true)));
let response = try let response = try
let page_def = match env_get env ("page:" ^ sname) with Dict d -> d | _ -> raise Not_found in let page_def = match env_get env ("page:" ^ sname) with Dict d -> d | _ -> raise Not_found in
let shell_ast = match Hashtbl.find_opt page_def "shell" with Some v -> v | None -> Nil in let shell_ast = match Hashtbl.find_opt page_def "shell" with Some v -> v | None -> Nil in

View File

@@ -1,186 +1,322 @@
;; ---- Page content ---- ;; ---- Page content ----
(defcomp () (defcomp
(~docs/page :title "Provide / Context / Emit!" ()
(~docs/page
(p (~tw :tokens "text-stone-500 text-sm italic mb-8") :title "Provide / Context / Emit!"
"Sugar for " (code "scope") " with a value. " (code "provide") " creates a named scope " (p
"with a value and an accumulator. " (code "context") " reads the value downward. " (~tw :tokens "text-stone-500 text-sm italic mb-8")
(code "emit!") " appends to the accumulator upward. " (code "emitted") " retrieves what was emitted. " "Sugar for "
(code "scope")
" with a value. "
(code "provide")
" creates a named scope "
"with a value and an accumulator. "
(code "context")
" reads the value downward. "
(code "emit!")
" appends to the accumulator upward. "
(code "emitted")
" retrieves what was emitted. "
"See " "See "
(a :href "/sx/(geography.(scopes))" (~tw :tokens "text-violet-600 hover:underline") "scopes") (a
:href "/sx/(geography.(scopes))"
(~tw :tokens "text-violet-600 hover:underline")
"scopes")
" for the unified primitive.") " for the unified primitive.")
(~docs/section
;; ===================================================================== :title "Four primitives"
;; I. The four primitives :id "primitives"
;; ===================================================================== (~docs/subsection
:title "provide (special form)"
(~docs/section :title "Four primitives" :id "primitives" (p
(code "provide")
(~docs/subsection :title "provide (special form)" " creates a named scope with a value and an empty accumulator. "
(p (code "provide") " creates a named scope with a value and an empty accumulator. "
"The body expressions execute with the scope active. When the body completes, " "The body expressions execute with the scope active. When the body completes, "
"the scope is popped.") "the scope is popped.")
(~docs/code :src (highlight "(provide name value\n body...)\n\n;; Example: theme context\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"Title\") ;; can read (context \"theme\")\n (p \"Body\")) ;; scope active for all children" "lisp")) (~docs/code
(p (code "provide") " is a special form, not a function — the body is evaluated " :src (highlight
"(provide name value\n body...)\n\n;; Example: theme context\n(provide \"theme\" {:primary \"violet\"}\n (h1 \"Title\") ;; can read (context \"theme\")\n (p \"Body\")) ;; scope active for all children"
"lisp"))
(p
(code "provide")
" is a special form, not a function — the body is evaluated "
"inside the scope, not before it.")) "inside the scope, not before it."))
(~docs/subsection
(~docs/subsection :title "context" :title "context"
(p "Reads the value from the nearest enclosing " (code "provide") " with the given name. " (p
"Reads the value from the nearest enclosing "
(code "provide")
" with the given name. "
"Errors if no provider and no default given.") "Errors if no provider and no default given.")
(~docs/code :src (highlight "(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (get (context \"theme\") :primary)) ;; → \"violet\"\n\n;; With default (no error when missing):\n(context \"theme\" {:primary \"stone\"}) ;; → {:primary \"stone\"}" "lisp"))) (~docs/code
:src (highlight
(~docs/subsection :title "emit!" "(provide \"theme\" {:primary \"violet\" :font \"serif\"}\n (get (context \"theme\") :primary)) ;; → \"violet\"\n\n;; With default (no error when missing):\n(context \"theme\" {:primary \"stone\"}) ;; → {:primary \"stone\"}"
(p "Appends a value to the nearest enclosing provider's accumulator. " "lisp")))
(~docs/subsection
:title "emit!"
(p
"Appends a value to the nearest enclosing provider's accumulator. "
"Tolerant: returns nil silently when no provider exists.") "Tolerant: returns nil silently when no provider exists.")
(~docs/code :src (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"analytics.js\")\n (emit! \"scripts\" \"charts.js\")\n ;; accumulator now has both scripts\n )\n\n;; Outside any provider — silently does nothing:\n(emit! \"scripts\" \"orphan.js\") ;; → nil, no error" "lisp")) (~docs/code
(p "Tolerance is critical. Spreads emit into " (code "\"element-attrs\"") :src (highlight
" — but a spread might be evaluated in a fragment, a " (code "begin") " block, or a " "(provide \"scripts\" nil\n (emit! \"scripts\" \"analytics.js\")\n (emit! \"scripts\" \"charts.js\")\n ;; accumulator now has both scripts\n )\n\n;; Outside any provider — silently does nothing:\n(emit! \"scripts\" \"orphan.js\") ;; → nil, no error"
(code "map") " call where no element provider exists. " "lisp"))
"Tolerant " (code "emit!") " means these cases silently vanish instead of crashing.")) (p
"Tolerance is critical. Spreads emit into "
(~docs/subsection :title "emitted" (code "\"element-attrs\"")
(p "Returns the list of values emitted into the nearest provider with the given name. " " — but a spread might be evaluated in a fragment, a "
(code "begin")
" block, or a "
(code "map")
" call where no element provider exists. "
"Tolerant "
(code "emit!")
" means these cases silently vanish instead of crashing."))
(~docs/subsection
:title "emitted"
(p
"Returns the list of values emitted into the nearest provider with the given name. "
"Empty list if no provider.") "Empty list if no provider.")
(~docs/code :src (highlight "(provide \"scripts\" nil\n (emit! \"scripts\" \"a.js\")\n (emit! \"scripts\" \"b.js\")\n (emitted \"scripts\")) ;; → (\"a.js\" \"b.js\")" "lisp")))) (~docs/code
:src (highlight
;; ===================================================================== "(provide \"scripts\" nil\n (emit! \"scripts\" \"a.js\")\n (emit! \"scripts\" \"b.js\")\n (emitted \"scripts\")) ;; → (\"a.js\" \"b.js\")"
;; II. Two directions, one mechanism "lisp"))))
;; ===================================================================== (~docs/section
:title "Two directions, one mechanism"
(~docs/section :title "Two directions, one mechanism" :id "directions" :id "directions"
(p (code "provide") " serves both downward and upward communication through a single scope.") (p
(code "provide")
" serves both downward and upward communication through a single scope.")
(~docs/table (~docs/table
:headers (list "Direction" "Read with" "Write with" "Example") :headers (list "Direction" "Read with" "Write with" "Example")
:rows (list :rows (list
(list "Downward (scope → child)" "context" "provide value" "Theme, config, locale") (list
(list "Upward (child → scope)" "emitted" "emit!" "Script collection, spread attrs"))) "Downward (scope → child)"
"context"
(~geography/provide-demo-example "provide value"
:demo (~geography/demo-provide-basic) "Theme, config, locale")
:code (highlight ";; Downward: theme context\n(provide \"theme\"\n {:primary \"violet\" :accent \"rose\"}\n (h1 :style (str \"color:\"\n (get (context \"theme\") :primary))\n \"Themed heading\")\n (p \"inherits theme context\"))" "lisp")) (list
"Upward (child → scope)"
(~geography/provide-demo-example "emitted"
:demo (~geography/demo-emit-collect) "emit!"
:code (highlight ";; Upward: script accumulation\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div\n (emit! \"scripts\" \"charts.js\")\n \"chart\"))\n ;; Collect at the boundary:\n (for-each (fn (s)\n (script :src s))\n (emitted \"scripts\")))" "lisp"))) "Script collection, spread attrs")))
(~geography/provide/provide-demo-example
;; ===================================================================== :demo (~geography/provide/demo-provide-basic)
;; III. How spreads use it :code (highlight
;; ===================================================================== ";; Downward: theme context\n(provide \"theme\"\n {:primary \"violet\" :accent \"rose\"}\n (h1 :style (str \"color:\"\n (get (context \"theme\") :primary))\n \"Themed heading\")\n (p \"inherits theme context\"))"
"lisp"))
(~docs/section :title "How spreads use provide/emit!" :id "spreads" (~geography/provide/provide-demo-example
(p "Every element rendering function wraps its children in a provider scope " :demo (~geography/provide/demo-emit-collect)
"named " (code "\"element-attrs\"") ". When the adapter encounters a spread child, " :code (highlight
";; Upward: script accumulation\n(provide \"scripts\" nil\n (div\n (emit! \"scripts\" \"analytics.js\")\n (div\n (emit! \"scripts\" \"charts.js\")\n \"chart\"))\n ;; Collect at the boundary:\n (for-each (fn (s)\n (script :src s))\n (emitted \"scripts\")))"
"lisp")))
(~docs/section
:title "How spreads use provide/emit!"
:id "spreads"
(p
"Every element rendering function wraps its children in a provider scope "
"named "
(code "\"element-attrs\"")
". When the adapter encounters a spread child, "
"it emits the spread's attrs into this scope. After all children render, the " "it emits the spread's attrs into this scope. After all children render, the "
"element collects and merges the emitted attrs.") "element collects and merges the emitted attrs.")
(~geography/provide/provide-demo-example
(~geography/provide-demo-example :demo (~geography/provide/demo-spread-mechanism)
:demo (~geography/demo-spread-mechanism) :code (highlight
:code (highlight ";; Spread = emit! into element-attrs\n(div (make-spread {:class \"card\"})\n \"hello\")\n\n;; Internally:\n;; 1. div opens provider:\n;; (provide-push! \"element-attrs\" nil)\n;; 2. spread child emits:\n;; (emit! \"element-attrs\"\n;; {:class \"card\"})\n;; 3. div collects + merges:\n;; (emitted \"element-attrs\")\n;; → ({:class \"card\"})\n;; 4. (provide-pop! \"element-attrs\")\n;; Result: <div class=\"card\">hello</div>" "lisp")) ";; Spread = emit! into element-attrs\n(div (make-spread {:class \"card\"})\n \"hello\")\n\n;; Internally:\n;; 1. div opens provider:\n;; (provide-push! \"element-attrs\" nil)\n;; 2. spread child emits:\n;; (emit! \"element-attrs\"\n;; {:class \"card\"})\n;; 3. div collects + merges:\n;; (emitted \"element-attrs\")\n;; → ({:class \"card\"})\n;; 4. (provide-pop! \"element-attrs\")\n;; Result: <div class=\"card\">hello</div>"
"lisp"))
(~docs/subsection :title "Why this matters" (~docs/subsection
(p "Before the refactor, every intermediate form in the render pipeline — " :title "Why this matters"
"fragments, " (code "let") ", " (code "begin") ", " (code "map") ", " (p
(code "for-each") ", " (code "when") ", " (code "cond") ", component children — " "Before the refactor, every intermediate form in the render pipeline — "
"needed an explicit " (code "(filter (fn (r) (not (spread? r))) ...)") " to strip " "fragments, "
(code "let")
", "
(code "begin")
", "
(code "map")
", "
(code "for-each")
", "
(code "when")
", "
(code "cond")
", component children — "
"needed an explicit "
(code "(filter (fn (r) (not (spread? r))) ...)")
" to strip "
"spread values from rendered output. Over 25 such filters existed across the four adapters.") "spread values from rendered output. Over 25 such filters existed across the four adapters.")
(p "With provide/emit!, all of these disappear. Spreads emit into the nearest element's " (p
"With provide/emit!, all of these disappear. Spreads emit into the nearest element's "
"scope regardless of how many layers of control flow they pass through. Non-element " "scope regardless of how many layers of control flow they pass through. Non-element "
"contexts have no provider, so " (code "emit!") " is a silent no-op."))) "contexts have no provider, so "
(code "emit!")
;; ===================================================================== " is a silent no-op.")))
;; IV. Nested scoping (~docs/section
;; ===================================================================== :title "Nested scoping"
:id "nesting"
(~docs/section :title "Nested scoping" :id "nesting" (p
(p "Providers stack. Each " (code "provide") " pushes onto a per-name stack; " "Providers stack. Each "
(code "provide")
" pushes onto a per-name stack; "
"the closest one wins. This gives lexical-style scoping at render time.") "the closest one wins. This gives lexical-style scoping at render time.")
(~geography/provide/provide-demo-example
(~geography/provide-demo-example :demo (~geography/provide/demo-nested-provide)
:demo (~geography/demo-nested-provide) :code (highlight
:code (highlight "(provide \"level\" \"outer\"\n (context \"level\") ;; → \"outer\"\n (provide \"level\" \"inner\"\n (context \"level\")) ;; → \"inner\"\n (context \"level\")) ;; → \"outer\" again" "lisp")) "(provide \"level\" \"outer\"\n (context \"level\") ;; → \"outer\"\n (provide \"level\" \"inner\"\n (context \"level\")) ;; → \"inner\"\n (context \"level\")) ;; → \"outer\" again"
"lisp"))
(p "For " (code "emit!") ", this means emissions go to the " (em "nearest") " provider. " (p
"For "
(code "emit!")
", this means emissions go to the "
(em "nearest")
" provider. "
"A spread inside a nested element emits to that element, not an ancestor.") "A spread inside a nested element emits to that element, not an ancestor.")
(~docs/code :src (highlight ";; Nested elements = nested providers\n(div ;; provider A\n (span ;; provider B\n (make-spread {:class \"inner\"})) ;; emits to B\n (make-spread {:class \"outer\"})) ;; emits to A\n;; → <div class=\"outer\"><span class=\"inner\"></span></div>" "lisp"))) (~docs/code
:src (highlight
;; ===================================================================== ";; Nested elements = nested providers\n(div ;; provider A\n (span ;; provider B\n (make-spread {:class \"inner\"})) ;; emits to B\n (make-spread {:class \"outer\"})) ;; emits to A\n;; → <div class=\"outer\"><span class=\"inner\"></span></div>"
;; V. Across all adapters "lisp")))
;; ===================================================================== (~docs/section
:title "Across all adapters"
(~docs/section :title "Across all adapters" :id "adapters" :id "adapters"
(p "The provide/emit! mechanism works identically across all four rendering adapters. " (p
"The provide/emit! mechanism works identically across all four rendering adapters. "
"The element rendering pattern is the same; only the output format differs.") "The element rendering pattern is the same; only the output format differs.")
(~docs/table (~docs/table
:headers (list "Adapter" "Element render" "Spread dispatch") :headers (list "Adapter" "Element render" "Spread dispatch")
:rows (list :rows (list
(list "HTML (server)" "provide-push! → render children → merge emitted → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → \"\"") (list
(list "Async (server)" "Same pattern, with await on child rendering" "Same dispatch") "HTML (server)"
(list "SX wire (aser)" "provide-push! → serialize children → merge emitted as :key attrs → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → nil") "provide-push! → render children → merge emitted → provide-pop!"
(list "DOM (browser)" "provide-push! → reduce children → merge emitted onto DOM element → provide-pop!" "emit! + keep value for reactive-spread detection"))) "(emit! \"element-attrs\" (spread-attrs expr)) → \"\"")
(list
(~docs/subsection :title "DOM adapter: reactive-spread preserved" "Async (server)"
(p "In the DOM adapter, spread children inside islands are still checked individually " "Same pattern, with await on child rendering"
"for signal dependencies. " (code "reactive-spread") " tracks signal deps and " "Same dispatch")
(list
"SX wire (aser)"
"provide-push! → serialize children → merge emitted as :key attrs → provide-pop!"
"(emit! \"element-attrs\" (spread-attrs expr)) → nil")
(list
"DOM (browser)"
"provide-push! → reduce children → merge emitted onto DOM element → provide-pop!"
"emit! + keep value for reactive-spread detection")))
(~docs/subsection
:title "DOM adapter: reactive-spread preserved"
(p
"In the DOM adapter, spread children inside islands are still checked individually "
"for signal dependencies. "
(code "reactive-spread")
" tracks signal deps and "
"surgically updates attributes when signals change. The static path uses provide/emit!; " "surgically updates attributes when signals change. The static path uses provide/emit!; "
"the reactive path wraps it in an effect.") "the reactive path wraps it in an effect.")
(p "See the " (p
(a :href "/sx/(geography.(spreads))" (~tw :tokens "text-violet-600 hover:underline") "spreads article") "See the "
(a
:href "/sx/(geography.(spreads))"
(~tw :tokens "text-violet-600 hover:underline")
"spreads article")
" for reactive-spread details."))) " for reactive-spread details.")))
(~docs/section
;; ===================================================================== :title "Comparison with collect! / collected"
;; VI. Comparison with collect! :id "comparison"
;; =====================================================================
(~docs/section :title "Comparison with collect! / collected" :id "comparison"
(~docs/table (~docs/table
:headers (list "" "provide / emit!" "collect! / collected") :headers (list "" "provide / emit!" "collect! / collected")
:rows (list :rows (list
(list "Scope" "Lexical (nearest enclosing provide)" "Global (render-wide)") (list
(list "Deduplication" "None — every emit! appends" "Automatic (same value skipped)") "Scope"
(list "Multiple scopes" "Yes — nested provides shadow" "No — single global bucket per name") "Lexical (nearest enclosing provide)"
"Global (render-wide)")
(list
"Deduplication"
"None — every emit! appends"
"Automatic (same value skipped)")
(list
"Multiple scopes"
"Yes — nested provides shadow"
"No — single global bucket per name")
(list "Downward data" "Yes (context)" "No") (list "Downward data" "Yes (context)" "No")
(list "Used by" "Spreads (element-attrs)" "CSSX rule accumulation"))) (list "Used by" "Spreads (element-attrs)" "CSSX rule accumulation")))
(p
(p (code "collect!") " remains the right tool for CSS rule accumulation — deduplication " (code "collect!")
" remains the right tool for CSS rule accumulation — deduplication "
"matters there, and rules need to reach the layout root regardless of nesting depth. " "matters there, and rules need to reach the layout root regardless of nesting depth. "
(code "emit!") " is right for spread attrs — no dedup needed, and each element only " (code "emit!")
" is right for spread attrs — no dedup needed, and each element only "
"wants attrs from its direct children.")) "wants attrs from its direct children."))
(~docs/section
;; ===================================================================== :title "Platform implementation"
;; VII. Platform implementation :id "platform"
;; ===================================================================== (p
(code "provide")
(~docs/section :title "Platform implementation" :id "platform" " is sugar for "
(p (code "provide") " is sugar for " (code "scope") ". At the platform level, " (code "scope")
(code "provide-push!") " and " (code "provide-pop!") " are aliases for " ". At the platform level, "
(code "scope-push!") " and " (code "scope-pop!") ". All operations work on a unified " (code "provide-push!")
(code "_scope_stacks") " data structure.") " and "
(code "provide-pop!")
" are aliases for "
(code "scope-push!")
" and "
(code "scope-pop!")
". All operations work on a unified "
(code "_scope_stacks")
" data structure.")
(~docs/table (~docs/table
:headers (list "Platform primitive" "Purpose") :headers (list "Platform primitive" "Purpose")
:rows (list :rows (list
(list "scope-push!(name, value)" "Push a new scope with value and empty accumulator") (list
"scope-push!(name, value)"
"Push a new scope with value and empty accumulator")
(list "scope-pop!(name)" "Pop the most recent scope") (list "scope-pop!(name)" "Pop the most recent scope")
(list "context(name, ...default)" "Read value from nearest scope (error if missing and no default)") (list
(list "emit!(name, value)" "Append to nearest scope's accumulator (tolerant: no-op if missing)") "context(name, ...default)"
"Read value from nearest scope (error if missing and no default)")
(list
"emit!(name, value)"
"Append to nearest scope's accumulator (tolerant: no-op if missing)")
(list "emitted(name)" "Return accumulated values from nearest scope"))) (list "emitted(name)" "Return accumulated values from nearest scope")))
(p
(p (code "provide") " is a special form in " (code "provide")
(a :href "/sx/(language.(spec.(explore.evaluator)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "eval.sx") " is a special form in "
" — it calls " (code "scope-push!") ", evaluates the body, " (a
"then calls " (code "scope-pop!") ". See " :href "/sx/(language.(spec.(explore.evaluator)))"
(a :href "/sx/(geography.(scopes))" (~tw :tokens "text-violet-600 hover:underline") "scopes") (~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
"eval.sx")
" — it calls "
(code "scope-push!")
", evaluates the body, "
"then calls "
(code "scope-pop!")
". See "
(a
:href "/sx/(geography.(scopes))"
(~tw :tokens "text-violet-600 hover:underline")
"scopes")
" for the full unified platform.") " for the full unified platform.")
(~docs/note (~docs/note
(p (strong "Spec explorer: ") "See the provide/emit! primitives in " (p
(a :href "/sx/(language.(spec.(explore.boundary)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "boundary.sx explorer") (strong "Spec explorer: ")
". The " (code "provide") " special form is in " "See the provide/emit! primitives in "
(a :href "/sx/(language.(spec.(explore.evaluator)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "eval.sx explorer") (a
:href "/sx/(language.(spec.(explore.boundary)))"
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
"boundary.sx explorer")
". The "
(code "provide")
" special form is in "
(a
:href "/sx/(language.(spec.(explore.evaluator)))"
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
"eval.sx explorer")
". Element rendering with provide/emit! is visible in " ". Element rendering with provide/emit! is visible in "
(a :href "/sx/(language.(spec.(explore.adapter-html)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-html") (a
:href "/sx/(language.(spec.(explore.adapter-html)))"
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
"adapter-html")
" and " " and "
(a :href "/sx/(language.(spec.(explore.adapter-async)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-async") (a
:href "/sx/(language.(spec.(explore.adapter-async)))"
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
"adapter-async")
"."))))) ".")))))

View File

@@ -34,8 +34,8 @@
"to the accumulator, and " "to the accumulator, and "
(code "emitted") (code "emitted")
" reads what was accumulated.") " reads what was accumulated.")
(~geography/scopes-demo-example (~geography/scopes/scopes-demo-example
:demo (~geography/demo-scope-basic) :demo (~geography/scopes/demo-scope-basic)
:src (highlight :src (highlight
"(scope \"theme\" :value \"violet\"\n (context \"theme\")) ;; → \"violet\"\n\n;; Nested scopes shadow:\n(scope \"x\" :value \"outer\"\n (scope \"x\" :value \"inner\"\n (context \"x\")) ;; → \"inner\"\n (context \"x\")) ;; → \"outer\"" "(scope \"theme\" :value \"violet\"\n (context \"theme\")) ;; → \"violet\"\n\n;; Nested scopes shadow:\n(scope \"x\" :value \"outer\"\n (scope \"x\" :value \"inner\"\n (context \"x\")) ;; → \"inner\"\n (context \"x\")) ;; → \"outer\""
"lisp"))) "lisp")))
@@ -80,8 +80,8 @@
" is the most interesting sugar. When called, if no scope exists " " is the most interesting sugar. When called, if no scope exists "
"for that name, it lazily creates a root scope with deduplication enabled. " "for that name, it lazily creates a root scope with deduplication enabled. "
"Then it emits into it.") "Then it emits into it.")
(~geography/scopes-demo-example (~geography/scopes/scopes-demo-example
:demo (~geography/demo-scope-dedup) :demo (~geography/scopes/demo-scope-dedup)
:src (highlight :src (highlight
";; collect! creates a lazy root scope:\n(collect! \"css\" \".card { pad: 1rem }\")\n(collect! \"css\" \".card { pad: 1rem }\") ;; deduped!\n(collect! \"css\" \".btn { color: blue }\")\n(collected \"css\") ;; → 2 rules\n\n;; Equivalent to:\n(scope \"css\" ;; with dedup\n (emit! \"css\" ...)\n (emitted \"css\"))" ";; collect! creates a lazy root scope:\n(collect! \"css\" \".card { pad: 1rem }\")\n(collect! \"css\" \".card { pad: 1rem }\") ;; deduped!\n(collect! \"css\" \".btn { color: blue }\")\n(collected \"css\") ;; → 2 rules\n\n;; Equivalent to:\n(scope \"css\" ;; with dedup\n (emit! \"css\" ...)\n (emitted \"css\"))"
"lisp")) "lisp"))
@@ -111,8 +111,8 @@
(~docs/section (~docs/section
:title "Upward data flow" :title "Upward data flow"
:id "upward" :id "upward"
(~geography/scopes-demo-example (~geography/scopes/scopes-demo-example
:demo (~geography/demo-scope-emit) :demo (~geography/scopes/demo-scope-emit)
:src (highlight :src (highlight
"(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")" "(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")"
"lisp")) "lisp"))

View File

@@ -3,11 +3,15 @@
(div (div
(~tw :tokens "space-y-3") (~tw :tokens "space-y-3")
(div (div
(~geography/demo-callout :type "info") (~geography/spreads/demo-callout :type "info")
(p (~tw :tokens "text-sm") "Info — styled by its child.")) (p (~tw :tokens "text-sm") "Info — styled by its child."))
(div (div
(~geography/demo-callout :type "warning") (~geography/spreads/demo-callout :type "warning")
(p (~tw :tokens "text-sm") "Warning — same component, different type.")) (p
(~tw :tokens "text-sm")
"Warning — same component, different type."))
(div (div
(~geography/demo-callout :type "success") (~geography/spreads/demo-callout :type "success")
(p (~tw :tokens "text-sm") "Success — child tells parent how to look.")))) (p
(~tw :tokens "text-sm")
"Success — child tells parent how to look."))))

View File

@@ -41,8 +41,8 @@
" extracts the dict. " " extracts the dict. "
"But the actual delivery from child to parent goes through provide/emit!, not through " "But the actual delivery from child to parent goes through provide/emit!, not through "
"return-value inspection.") "return-value inspection.")
(~geography/demo-example (~geography/spreads/demo-example
:demo (~geography/demo-spread-basic) :demo (~geography/spreads/demo-spread-basic)
:code (highlight :code (highlight
"(defcomp ~callout (&key type)\n (make-spread\n (cond\n (= type \"info\")\n {\"style\" \"border-left:4px solid\n #3b82f6; background:#eff6ff\"}\n (= type \"warning\")\n {\"style\" \"border-left:4px solid\n #f59e0b; background:#fffbeb\"}\n (= type \"success\")\n {\"style\" \"border-left:4px solid\n #10b981; background:#ecfdf5\"})))\n\n;; Child injects attrs onto parent:\n(div (~callout :type \"info\")\n \"This div gets the callout style.\")" "(defcomp ~callout (&key type)\n (make-spread\n (cond\n (= type \"info\")\n {\"style\" \"border-left:4px solid\n #3b82f6; background:#eff6ff\"}\n (= type \"warning\")\n {\"style\" \"border-left:4px solid\n #f59e0b; background:#fffbeb\"}\n (= type \"success\")\n {\"style\" \"border-left:4px solid\n #10b981; background:#ecfdf5\"})))\n\n;; Child injects attrs onto parent:\n(div (~callout :type \"info\")\n \"This div gets the callout style.\")"
"lisp")) "lisp"))
@@ -97,8 +97,8 @@
(code "reactive-spread") (code "reactive-spread")
" tracks signal dependencies and surgically " " tracks signal dependencies and surgically "
"updates the parent element's attributes when signals change.") "updates the parent element's attributes when signals change.")
(~geography/demo-example (~geography/spreads/demo-example
:demo (~geography/demo-reactive-spread) :demo (~geography/spreads/demo-reactive-spread)
:code (highlight :code (highlight
"(defisland ~themed-card ()\n (let ((theme (signal \"violet\")))\n (div (~cssx/tw :tokens\n (str \"bg-\" (deref theme)\n \"-100 p-4\"))\n (button\n :on-click\n (fn (e) (reset! theme \"rose\"))\n \"change theme\"))))" "(defisland ~themed-card ()\n (let ((theme (signal \"violet\")))\n (div (~cssx/tw :tokens\n (str \"bg-\" (deref theme)\n \"-100 p-4\"))\n (button\n :on-click\n (fn (e) (reset! theme \"rose\"))\n \"change theme\"))))"
"lisp")) "lisp"))
@@ -179,8 +179,8 @@
:title "CSSX: the first application" :title "CSSX: the first application"
:id "cssx" :id "cssx"
(p (code "~cssx/tw") " is the primary consumer of spreads:") (p (code "~cssx/tw") " is the primary consumer of spreads:")
(~geography/demo-example (~geography/spreads/demo-example
:demo (~geography/demo-cssx-tw) :demo (~geography/spreads/demo-cssx-tw)
:code (highlight :code (highlight
"(defcomp ~cssx/tw (tokens)\n (let ((token-list\n (filter (fn (t) (not (= t \"\")))\n (split (or tokens \"\") \" \")))\n (results\n (map cssx-process-token\n token-list))\n (classes (map (fn (r)\n (get r \"cls\")) results))\n (rules (map (fn (r)\n (get r \"rule\")) results)))\n (for-each (fn (rule)\n (collect! \"cssx\" rule)) rules)\n (make-spread\n {\"class\" (join \" \" classes)})))" "(defcomp ~cssx/tw (tokens)\n (let ((token-list\n (filter (fn (t) (not (= t \"\")))\n (split (or tokens \"\") \" \")))\n (results\n (map cssx-process-token\n token-list))\n (classes (map (fn (r)\n (get r \"cls\")) results))\n (rules (map (fn (r)\n (get r \"rule\")) results)))\n (for-each (fn (rule)\n (collect! \"cssx\" rule)) rules)\n (make-spread\n {\"class\" (join \" \" classes)})))"
"lisp")) "lisp"))
@@ -217,8 +217,8 @@
(code "~cssx/tw") (code "~cssx/tw")
" returns a spread, and spreads are values, " " returns a spread, and spreads are values, "
"you can bind them to names:") "you can bind them to names:")
(~geography/demo-example (~geography/spreads/demo-example
:demo (~geography/demo-semantic-vars) :demo (~geography/spreads/demo-semantic-vars)
:code (highlight :code (highlight
"(let ((card (~cssx/tw :tokens\n \"bg-stone-50 rounded-lg p-4\n shadow-sm border\"))\n (heading (~cssx/tw :tokens\n \"text-violet-700 text-lg\n font-bold\"))\n (body (~cssx/tw :tokens\n \"text-stone-600 text-sm\")))\n ;; Reuse everywhere:\n (div card\n (h4 heading \"First Card\")\n (p body \"Same variables.\"))\n (div card\n (h4 heading \"Second Card\")\n (p body \"Consistent look.\")))" "(let ((card (~cssx/tw :tokens\n \"bg-stone-50 rounded-lg p-4\n shadow-sm border\"))\n (heading (~cssx/tw :tokens\n \"text-violet-700 text-lg\n font-bold\"))\n (body (~cssx/tw :tokens\n \"text-stone-600 text-sm\")))\n ;; Reuse everywhere:\n (div card\n (h4 heading \"First Card\")\n (p body \"Same variables.\"))\n (div card\n (h4 heading \"Second Card\")\n (p body \"Consistent look.\")))"
"lisp")) "lisp"))

View File

@@ -24,9 +24,7 @@
(define (define
geography geography
(fn (fn (content) (if (nil? content) (quote (~geography)) content)))
(content)
(if (nil? content) (quote (~geography/index-content)) content)))
(define (define
applications applications
@@ -104,7 +102,7 @@
(slug) (slug)
(if (if
(nil? slug) (nil? slug)
(quote (~geography/isomorphism)) (quote (~etc/plan/isomorphic))
(case (case
slug slug
"bundle-analyzer" "bundle-analyzer"

View File

@@ -1,13 +1,9 @@
(define-library (web request-handler) (define-library
(export (web request-handler)
sx-url-to-expr (export sx-url-to-expr sx-auto-quote sx-eval-page sx-handle-request)
sx-auto-quote
sx-eval-page
sx-handle-request)
(begin (begin
(define (define
sx-url-to-expr sx-url-to-expr
(fn (fn
@@ -24,7 +20,6 @@
(starts-with? path "/") (starts-with? path "/")
(join " " (split (slice path 1 (len path)) ".")) (join " " (split (slice path 1 (len path)) "."))
:else path)))) :else path))))
(define (define
sx-auto-quote sx-auto-quote
(fn (fn
@@ -35,7 +30,6 @@
(list? expr) (list? expr)
(map (fn (e) (sx-auto-quote e env)) expr) (map (fn (e) (sx-auto-quote e env)) expr)
:else expr))) :else expr)))
(define (define
sx-eval-page sx-eval-page
(fn (fn
@@ -48,12 +42,11 @@
(when (when
(not (empty? exprs)) (not (empty? exprs))
(let (let
((expr (if (= (len exprs) 1) (first exprs) exprs)) ((expr (first exprs)) (quoted (sx-auto-quote expr env)))
(quoted (sx-auto-quote expr env)) (letrec
(callable (if (symbol? quoted) (list quoted) quoted))) ((call-page (fn (e) (cond (symbol? e) (let ((f (env-get env (symbol-name e)))) (f nil)) (list? e) (let ((head (first e)) (args (rest e))) (if (symbol? head) (let ((f (env-get env (symbol-name head))) (evaled-args (map call-page args))) (apply f evaled-args)) (eval-expr e env))) :else (eval-expr e env)))))
(eval-expr callable env))))) (call-page quoted))))))
(fn (err) nil)))) (fn (err) nil))))
(define (define
sx-handle-request sx-handle-request
(fn (fn
@@ -75,10 +68,7 @@
(= (slice path 0 prefix-len) prefix)) (= (slice path 0 prefix-len) prefix))
path path
(str (slice prefix 0 (- prefix-len 1)) path)))) (str (slice prefix 0 (- prefix-len 1)) path))))
{:page-ast page-ast :nav-path nav-path :is-ajax is-ajax}))))) {:page-ast page-ast :nav-path nav-path :is-ajax is-ajax}))))))) ;; end define-library
)) ;; end define-library
;; Re-export to global namespace for backward compatibility ;; Re-export to global namespace for backward compatibility
(import (web request-handler)) (import (web request-handler))