diff --git a/hosts/ocaml/bin/sx_server.ml b/hosts/ocaml/bin/sx_server.ml index 9466cff5..be82ee26 100644 --- a/hosts/ocaml/bin/sx_server.ml +++ b/hosts/ocaml/bin/sx_server.ml @@ -1240,6 +1240,9 @@ let inject_path_name expr path base_dir = let stem = if Filename.check_suffix rel ".sx" then String.sub rel 0 (String.length rel - 3) 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 *) let name = if Filename.basename stem = "index" 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 wrapped = List [Symbol inner_layout; Keyword "path"; String nav_path; page_ast] in 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 call = List [Symbol "aser"; List [Symbol "quote"; wrapped]; Env 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 | String s | SxExpr s -> s | _ -> serialize_value body_result in let t1 = Unix.gettimeofday () in @@ -4078,6 +4085,7 @@ let http_mode port = if is_ajax then begin (* AJAX streaming: evaluate shell + data + content synchronously, 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 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 diff --git a/sx/sx/geography/provide/index.sx b/sx/sx/geography/provide/index.sx index bfbfc701..1b6629f0 100644 --- a/sx/sx/geography/provide/index.sx +++ b/sx/sx/geography/provide/index.sx @@ -1,186 +1,322 @@ ;; ---- Page content ---- -(defcomp () - (~docs/page :title "Provide / Context / Emit!" - - (p (~tw :tokens "text-stone-500 text-sm italic mb-8") - "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. " +(defcomp + () + (~docs/page + :title "Provide / Context / Emit!" + (p + (~tw :tokens "text-stone-500 text-sm italic mb-8") + "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 " - (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.") - - ;; ===================================================================== - ;; I. The four primitives - ;; ===================================================================== - - (~docs/section :title "Four primitives" :id "primitives" - - (~docs/subsection :title "provide (special form)" - (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 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")) - (p (code "provide") " is a special form, not a function — the body is evaluated " - "inside the scope, not before it.")) - - (~docs/subsection :title "context" - (p "Reads the value from the nearest enclosing " (code "provide") " with the given name. " - "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/subsection :title "emit!" - (p "Appends a value to the nearest enclosing provider's accumulator. " - "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")) - (p "Tolerance is critical. Spreads emit into " (code "\"element-attrs\"") - " — 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.") - (~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")))) - - ;; ===================================================================== - ;; II. Two directions, one mechanism - ;; ===================================================================== - - (~docs/section :title "Two directions, one mechanism" :id "directions" - (p (code "provide") " serves both downward and upward communication through a single scope.") - + (~docs/section + :title "Four primitives" + :id "primitives" + (~docs/subsection + :title "provide (special form)" + (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 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")) + (p + (code "provide") + " is a special form, not a function — the body is evaluated " + "inside the scope, not before it.")) + (~docs/subsection + :title "context" + (p + "Reads the value from the nearest enclosing " + (code "provide") + " with the given name. " + "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/subsection + :title "emit!" + (p + "Appends a value to the nearest enclosing provider's accumulator. " + "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")) + (p + "Tolerance is critical. Spreads emit into " + (code "\"element-attrs\"") + " — 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.") + (~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/section + :title "Two directions, one mechanism" + :id "directions" + (p + (code "provide") + " serves both downward and upward communication through a single scope.") (~docs/table :headers (list "Direction" "Read with" "Write with" "Example") :rows (list - (list "Downward (scope → child)" "context" "provide value" "Theme, config, locale") - (list "Upward (child → scope)" "emitted" "emit!" "Script collection, spread attrs"))) - - (~geography/provide-demo-example - :demo (~geography/demo-provide-basic) - :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")) - - (~geography/provide-demo-example - :demo (~geography/demo-emit-collect) - :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"))) - - ;; ===================================================================== - ;; III. How spreads use it - ;; ===================================================================== - - (~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 " - "element collects and merges the emitted attrs.") - - (~geography/provide-demo-example - :demo (~geography/demo-spread-mechanism) - :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:
hello
" "lisp")) - - (~docs/subsection :title "Why this matters" - (p "Before the refactor, every intermediate form in the render pipeline — " - "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.") - (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 " - "contexts have no provider, so " (code "emit!") " is a silent no-op."))) - - ;; ===================================================================== - ;; IV. Nested scoping - ;; ===================================================================== - - (~docs/section :title "Nested scoping" :id "nesting" - (p "Providers stack. Each " (code "provide") " pushes onto a per-name stack; " - "the closest one wins. This gives lexical-style scoping at render time.") - - (~geography/provide-demo-example - :demo (~geography/demo-nested-provide) - :code (highlight "(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. " - "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;; →
" "lisp"))) - - ;; ===================================================================== - ;; V. Across all adapters - ;; ===================================================================== - - (~docs/section :title "Across all adapters" :id "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.") - + (list + "Downward (scope → child)" + "context" + "provide value" + "Theme, config, locale") + (list + "Upward (child → scope)" + "emitted" + "emit!" + "Script collection, spread attrs"))) + (~geography/provide/provide-demo-example + :demo (~geography/provide/demo-provide-basic) + :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")) + (~geography/provide/provide-demo-example + :demo (~geography/provide/demo-emit-collect) + :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 " + "element collects and merges the emitted attrs.") + (~geography/provide/provide-demo-example + :demo (~geography/provide/demo-spread-mechanism) + :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:
hello
" + "lisp")) + (~docs/subsection + :title "Why this matters" + (p + "Before the refactor, every intermediate form in the render pipeline — " + "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.") + (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 " + "contexts have no provider, so " + (code "emit!") + " is a silent no-op."))) + (~docs/section + :title "Nested scoping" + :id "nesting" + (p + "Providers stack. Each " + (code "provide") + " pushes onto a per-name stack; " + "the closest one wins. This gives lexical-style scoping at render time.") + (~geography/provide/provide-demo-example + :demo (~geography/provide/demo-nested-provide) + :code (highlight + "(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. " + "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;; →
" + "lisp"))) + (~docs/section + :title "Across all adapters" + :id "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.") (~docs/table :headers (list "Adapter" "Element render" "Spread dispatch") :rows (list - (list "HTML (server)" "provide-push! → render children → merge emitted → provide-pop!" "(emit! \"element-attrs\" (spread-attrs expr)) → \"\"") - (list "Async (server)" "Same pattern, with await on child rendering" "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!; " - "the reactive path wraps it in an effect.") - (p "See the " - (a :href "/sx/(geography.(spreads))" (~tw :tokens "text-violet-600 hover:underline") "spreads article") - " for reactive-spread details."))) - - ;; ===================================================================== - ;; VI. Comparison with collect! - ;; ===================================================================== - - (~docs/section :title "Comparison with collect! / collected" :id "comparison" + (list + "HTML (server)" + "provide-push! → render children → merge emitted → provide-pop!" + "(emit! \"element-attrs\" (spread-attrs expr)) → \"\"") + (list + "Async (server)" + "Same pattern, with await on child rendering" + "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!; " + "the reactive path wraps it in an effect.") + (p + "See the " + (a + :href "/sx/(geography.(spreads))" + (~tw :tokens "text-violet-600 hover:underline") + "spreads article") + " for reactive-spread details."))) + (~docs/section + :title "Comparison with collect! / collected" + :id "comparison" (~docs/table :headers (list "" "provide / emit!" "collect! / collected") :rows (list - (list "Scope" "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 + "Scope" + "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 "Used by" "Spreads (element-attrs)" "CSSX rule accumulation"))) - - (p (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. " - (code "emit!") " is right for spread attrs — no dedup needed, and each element only " - "wants attrs from its direct children.")) - - ;; ===================================================================== - ;; VII. Platform implementation - ;; ===================================================================== - - (~docs/section :title "Platform implementation" :id "platform" - (p (code "provide") " is sugar for " (code "scope") ". At the platform level, " - (code "provide-push!") " 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.") - + (p + (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. " + (code "emit!") + " is right for spread attrs — no dedup needed, and each element only " + "wants attrs from its direct children.")) + (~docs/section + :title "Platform implementation" + :id "platform" + (p + (code "provide") + " is sugar for " + (code "scope") + ". At the platform level, " + (code "provide-push!") + " 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 :headers (list "Platform primitive" "Purpose") :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 "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 + "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"))) - - (p (code "provide") " is a special form in " - (a :href "/sx/(language.(spec.(explore.evaluator)))" (~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.") - + (p + (code "provide") + " is a special form in " + (a + :href "/sx/(language.(spec.(explore.evaluator)))" + (~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.") (~docs/note - (p (strong "Spec explorer: ") "See the provide/emit! primitives in " - (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 " - (a :href "/sx/(language.(spec.(explore.adapter-html)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-html") - " and " - (a :href "/sx/(language.(spec.(explore.adapter-async)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-async") - "."))))) + (p + (strong "Spec explorer: ") + "See the provide/emit! primitives in " + (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 " + (a + :href "/sx/(language.(spec.(explore.adapter-html)))" + (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") + "adapter-html") + " and " + (a + :href "/sx/(language.(spec.(explore.adapter-async)))" + (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") + "adapter-async") + "."))))) diff --git a/sx/sx/geography/scopes/index.sx b/sx/sx/geography/scopes/index.sx index 8cb11797..608633ae 100644 --- a/sx/sx/geography/scopes/index.sx +++ b/sx/sx/geography/scopes/index.sx @@ -34,8 +34,8 @@ "to the accumulator, and " (code "emitted") " reads what was accumulated.") - (~geography/scopes-demo-example - :demo (~geography/demo-scope-basic) + (~geography/scopes/scopes-demo-example + :demo (~geography/scopes/demo-scope-basic) :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\"" "lisp"))) @@ -80,8 +80,8 @@ " is the most interesting sugar. When called, if no scope exists " "for that name, it lazily creates a root scope with deduplication enabled. " "Then it emits into it.") - (~geography/scopes-demo-example - :demo (~geography/demo-scope-dedup) + (~geography/scopes/scopes-demo-example + :demo (~geography/scopes/demo-scope-dedup) :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\"))" "lisp")) @@ -111,8 +111,8 @@ (~docs/section :title "Upward data flow" :id "upward" - (~geography/scopes-demo-example - :demo (~geography/demo-scope-emit) + (~geography/scopes/scopes-demo-example + :demo (~geography/scopes/demo-scope-emit) :src (highlight "(scope \"deps\"\n (emit! \"deps\" \"lodash\")\n (emit! \"deps\" \"react\")\n (emitted \"deps\")) ;; → (\"lodash\" \"react\")" "lisp")) diff --git a/sx/sx/geography/spreads/_islands/demo-spread-basic.sx b/sx/sx/geography/spreads/_islands/demo-spread-basic.sx index d55d193f..58769e13 100644 --- a/sx/sx/geography/spreads/_islands/demo-spread-basic.sx +++ b/sx/sx/geography/spreads/_islands/demo-spread-basic.sx @@ -3,11 +3,15 @@ (div (~tw :tokens "space-y-3") (div - (~geography/demo-callout :type "info") + (~geography/spreads/demo-callout :type "info") (p (~tw :tokens "text-sm") "Info — styled by its child.")) (div - (~geography/demo-callout :type "warning") - (p (~tw :tokens "text-sm") "Warning — same component, different type.")) + (~geography/spreads/demo-callout :type "warning") + (p + (~tw :tokens "text-sm") + "Warning — same component, different type.")) (div - (~geography/demo-callout :type "success") - (p (~tw :tokens "text-sm") "Success — child tells parent how to look.")))) + (~geography/spreads/demo-callout :type "success") + (p + (~tw :tokens "text-sm") + "Success — child tells parent how to look.")))) diff --git a/sx/sx/geography/spreads/index.sx b/sx/sx/geography/spreads/index.sx index 990a4f2e..290f8abd 100644 --- a/sx/sx/geography/spreads/index.sx +++ b/sx/sx/geography/spreads/index.sx @@ -41,8 +41,8 @@ " extracts the dict. " "But the actual delivery from child to parent goes through provide/emit!, not through " "return-value inspection.") - (~geography/demo-example - :demo (~geography/demo-spread-basic) + (~geography/spreads/demo-example + :demo (~geography/spreads/demo-spread-basic) :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.\")" "lisp")) @@ -97,8 +97,8 @@ (code "reactive-spread") " tracks signal dependencies and surgically " "updates the parent element's attributes when signals change.") - (~geography/demo-example - :demo (~geography/demo-reactive-spread) + (~geography/spreads/demo-example + :demo (~geography/spreads/demo-reactive-spread) :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\"))))" "lisp")) @@ -179,8 +179,8 @@ :title "CSSX: the first application" :id "cssx" (p (code "~cssx/tw") " is the primary consumer of spreads:") - (~geography/demo-example - :demo (~geography/demo-cssx-tw) + (~geography/spreads/demo-example + :demo (~geography/spreads/demo-cssx-tw) :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)})))" "lisp")) @@ -217,8 +217,8 @@ (code "~cssx/tw") " returns a spread, and spreads are values, " "you can bind them to names:") - (~geography/demo-example - :demo (~geography/demo-semantic-vars) + (~geography/spreads/demo-example + :demo (~geography/spreads/demo-semantic-vars) :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.\")))" "lisp")) diff --git a/sx/sx/page-functions.sx b/sx/sx/page-functions.sx index b1efcfb6..2da9c610 100644 --- a/sx/sx/page-functions.sx +++ b/sx/sx/page-functions.sx @@ -24,9 +24,7 @@ (define geography - (fn - (content) - (if (nil? content) (quote (~geography/index-content)) content))) + (fn (content) (if (nil? content) (quote (~geography)) content))) (define applications @@ -104,7 +102,7 @@ (slug) (if (nil? slug) - (quote (~geography/isomorphism)) + (quote (~etc/plan/isomorphic)) (case slug "bundle-analyzer" diff --git a/web/request-handler.sx b/web/request-handler.sx index d8d4141f..f2cb489b 100644 --- a/web/request-handler.sx +++ b/web/request-handler.sx @@ -1,84 +1,74 @@ -(define-library (web request-handler) - (export - sx-url-to-expr - sx-auto-quote - sx-eval-page - sx-handle-request) +(define-library + (web request-handler) + (export sx-url-to-expr sx-auto-quote sx-eval-page sx-handle-request) (begin - -(define - sx-url-to-expr - (fn - (path) - (let - ((prefix (cek-try (fn () (or (dict-get __app-config :path-prefix) "/sx/")) (fn (e) "/sx/"))) - (prefix-len (len prefix)) - (prefix-bare (slice prefix 0 (- prefix-len 1)))) - (cond - (or (= path "/") (= path prefix) (= path prefix-bare)) - "home" - (starts-with? path prefix) - (join " " (split (slice path prefix-len (len path)) ".")) - (starts-with? path "/") - (join " " (split (slice path 1 (len path)) ".")) - :else path)))) - -(define - sx-auto-quote - (fn - (expr env) - (cond - (and (symbol? expr) (not (env-has? env (symbol-name expr)))) - (symbol-name expr) - (list? expr) - (map (fn (e) (sx-auto-quote e env)) expr) - :else expr))) - -(define - sx-eval-page - (fn - (path-expr env) - (cek-try + (define + sx-url-to-expr (fn - () - (let - ((exprs (sx-parse path-expr))) - (when - (not (empty? exprs)) - (let - ((expr (if (= (len exprs) 1) (first exprs) exprs)) - (quoted (sx-auto-quote expr env)) - (callable (if (symbol? quoted) (list quoted) quoted))) - (eval-expr callable env))))) - (fn (err) nil)))) - -(define - sx-handle-request - (fn - (path headers env shell-vars) - (let - ((is-ajax (or (has-key? headers "sx-request") (has-key? headers "hx-request"))) - (path-expr (sx-url-to-expr path)) - (page-ast (sx-eval-page path-expr env))) - (if - (nil? page-ast) - nil + (path) (let ((prefix (cek-try (fn () (or (dict-get __app-config :path-prefix) "/sx/")) (fn (e) "/sx/"))) (prefix-len (len prefix)) - (nav-path - (if - (and - (>= (len path) prefix-len) - (= (slice path 0 prefix-len) prefix)) - path - (str (slice prefix 0 (- prefix-len 1)) path)))) - {:page-ast page-ast :nav-path nav-path :is-ajax is-ajax}))))) - - -)) ;; end define-library + (prefix-bare (slice prefix 0 (- prefix-len 1)))) + (cond + (or (= path "/") (= path prefix) (= path prefix-bare)) + "home" + (starts-with? path prefix) + (join " " (split (slice path prefix-len (len path)) ".")) + (starts-with? path "/") + (join " " (split (slice path 1 (len path)) ".")) + :else path)))) + (define + sx-auto-quote + (fn + (expr env) + (cond + (and (symbol? expr) (not (env-has? env (symbol-name expr)))) + (symbol-name expr) + (list? expr) + (map (fn (e) (sx-auto-quote e env)) expr) + :else expr))) + (define + sx-eval-page + (fn + (path-expr env) + (cek-try + (fn + () + (let + ((exprs (sx-parse path-expr))) + (when + (not (empty? exprs)) + (let + ((expr (first exprs)) (quoted (sx-auto-quote expr env))) + (letrec + ((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))))) + (call-page quoted)))))) + (fn (err) nil)))) + (define + sx-handle-request + (fn + (path headers env shell-vars) + (let + ((is-ajax (or (has-key? headers "sx-request") (has-key? headers "hx-request"))) + (path-expr (sx-url-to-expr path)) + (page-ast (sx-eval-page path-expr env))) + (if + (nil? page-ast) + nil + (let + ((prefix (cek-try (fn () (or (dict-get __app-config :path-prefix) "/sx/")) (fn (e) "/sx/"))) + (prefix-len (len prefix)) + (nav-path + (if + (and + (>= (len path) prefix-len) + (= (slice path 0 prefix-len) prefix)) + path + (str (slice prefix 0 (- prefix-len 1)) path)))) + {:page-ast page-ast :nav-path nav-path :is-ajax is-ajax}))))))) ;; end define-library ;; Re-export to global namespace for backward compatibility (import (web request-handler))