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:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
(~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"))
|
:src (highlight
|
||||||
(p (code "provide") " is a special form, not a function — the body is evaluated "
|
"(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"
|
||||||
"inside the scope, not before it."))
|
"lisp"))
|
||||||
|
(p
|
||||||
(~docs/subsection :title "context"
|
(code "provide")
|
||||||
(p "Reads the value from the nearest enclosing " (code "provide") " with the given name. "
|
" is a special form, not a function — the body is evaluated "
|
||||||
"Errors if no provider and no default given.")
|
"inside the scope, not before it."))
|
||||||
(~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 "context"
|
||||||
(~docs/subsection :title "emit!"
|
(p
|
||||||
(p "Appends a value to the nearest enclosing provider's accumulator. "
|
"Reads the value from the nearest enclosing "
|
||||||
"Tolerant: returns nil silently when no provider exists.")
|
(code "provide")
|
||||||
(~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"))
|
" with the given name. "
|
||||||
(p "Tolerance is critical. Spreads emit into " (code "\"element-attrs\"")
|
"Errors if no provider and no default given.")
|
||||||
" — but a spread might be evaluated in a fragment, a " (code "begin") " block, or a "
|
(~docs/code
|
||||||
(code "map") " call where no element provider exists. "
|
:src (highlight
|
||||||
"Tolerant " (code "emit!") " means these cases silently vanish instead of crashing."))
|
"(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 "emitted"
|
(~docs/subsection
|
||||||
(p "Returns the list of values emitted into the nearest provider with the given name. "
|
:title "emit!"
|
||||||
"Empty list if no provider.")
|
(p
|
||||||
(~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"))))
|
"Appends a value to the nearest enclosing provider's accumulator. "
|
||||||
|
"Tolerant: returns nil silently when no provider exists.")
|
||||||
;; =====================================================================
|
(~docs/code
|
||||||
;; II. Two directions, one mechanism
|
: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/section :title "Two directions, one mechanism" :id "directions"
|
(p
|
||||||
(p (code "provide") " serves both downward and upward communication through a single scope.")
|
"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
|
(~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
|
||||||
"it emits the spread's attrs into this scope. After all children render, the "
|
";; 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\")))"
|
||||||
"element collects and merges the emitted attrs.")
|
"lisp")))
|
||||||
|
(~docs/section
|
||||||
(~geography/provide-demo-example
|
:title "How spreads use provide/emit!"
|
||||||
:demo (~geography/demo-spread-mechanism)
|
:id "spreads"
|
||||||
: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"))
|
(p
|
||||||
|
"Every element rendering function wraps its children in a provider scope "
|
||||||
(~docs/subsection :title "Why this matters"
|
"named "
|
||||||
(p "Before the refactor, every intermediate form in the render pipeline — "
|
(code "\"element-attrs\"")
|
||||||
"fragments, " (code "let") ", " (code "begin") ", " (code "map") ", "
|
". When the adapter encounters a spread child, "
|
||||||
(code "for-each") ", " (code "when") ", " (code "cond") ", component children — "
|
"it emits the spread's attrs into this scope. After all children render, the "
|
||||||
"needed an explicit " (code "(filter (fn (r) (not (spread? r))) ...)") " to strip "
|
"element collects and merges the emitted attrs.")
|
||||||
"spread values from rendered output. Over 25 such filters existed across the four adapters.")
|
(~geography/provide/provide-demo-example
|
||||||
(p "With provide/emit!, all of these disappear. Spreads emit into the nearest element's "
|
:demo (~geography/provide/demo-spread-mechanism)
|
||||||
"scope regardless of how many layers of control flow they pass through. Non-element "
|
:code (highlight
|
||||||
"contexts have no provider, so " (code "emit!") " is a silent no-op.")))
|
";; 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
|
||||||
;; IV. Nested scoping
|
:title "Why this matters"
|
||||||
;; =====================================================================
|
(p
|
||||||
|
"Before the refactor, every intermediate form in the render pipeline — "
|
||||||
(~docs/section :title "Nested scoping" :id "nesting"
|
"fragments, "
|
||||||
(p "Providers stack. Each " (code "provide") " pushes onto a per-name stack; "
|
(code "let")
|
||||||
"the closest one wins. This gives lexical-style scoping at render time.")
|
", "
|
||||||
|
(code "begin")
|
||||||
(~geography/provide-demo-example
|
", "
|
||||||
:demo (~geography/demo-nested-provide)
|
(code "map")
|
||||||
:code (highlight "(provide \"level\" \"outer\"\n (context \"level\") ;; → \"outer\"\n (provide \"level\" \"inner\"\n (context \"level\")) ;; → \"inner\"\n (context \"level\")) ;; → \"outer\" again" "lisp"))
|
", "
|
||||||
|
(code "for-each")
|
||||||
(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.")
|
(code "when")
|
||||||
(~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")))
|
", "
|
||||||
|
(code "cond")
|
||||||
;; =====================================================================
|
", component children — "
|
||||||
;; V. Across all adapters
|
"needed an explicit "
|
||||||
;; =====================================================================
|
(code "(filter (fn (r) (not (spread? r))) ...)")
|
||||||
|
" to strip "
|
||||||
(~docs/section :title "Across all adapters" :id "adapters"
|
"spread values from rendered output. Over 25 such filters existed across the four adapters.")
|
||||||
(p "The provide/emit! mechanism works identically across all four rendering adapters. "
|
(p
|
||||||
"The element rendering pattern is the same; only the output format differs.")
|
"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;; → <div class=\"outer\"><span class=\"inner\"></span></div>"
|
||||||
|
"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
|
(~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")
|
||||||
"surgically updates attributes when signals change. The static path uses provide/emit!; "
|
(list
|
||||||
"the reactive path wraps it in an effect.")
|
"SX wire (aser)"
|
||||||
(p "See the "
|
"provide-push! → serialize children → merge emitted as :key attrs → provide-pop!"
|
||||||
(a :href "/sx/(geography.(spreads))" (~tw :tokens "text-violet-600 hover:underline") "spreads article")
|
"(emit! \"element-attrs\" (spread-attrs expr)) → nil")
|
||||||
" for reactive-spread details.")))
|
(list
|
||||||
|
"DOM (browser)"
|
||||||
;; =====================================================================
|
"provide-push! → reduce children → merge emitted onto DOM element → provide-pop!"
|
||||||
;; VI. Comparison with collect!
|
"emit! + keep value for reactive-spread detection")))
|
||||||
;; =====================================================================
|
(~docs/subsection
|
||||||
|
:title "DOM adapter: reactive-spread preserved"
|
||||||
(~docs/section :title "Comparison with collect! / collected" :id "comparison"
|
(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
|
(~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!")
|
||||||
"matters there, and rules need to reach the layout root regardless of nesting depth. "
|
" remains the right tool for CSS rule accumulation — deduplication "
|
||||||
(code "emit!") " is right for spread attrs — no dedup needed, and each element only "
|
"matters there, and rules need to reach the layout root regardless of nesting depth. "
|
||||||
"wants attrs from its direct children."))
|
(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"
|
||||||
(~docs/section :title "Platform implementation" :id "platform"
|
(p
|
||||||
(p (code "provide") " is sugar for " (code "scope") ". At the platform level, "
|
(code "provide")
|
||||||
(code "provide-push!") " and " (code "provide-pop!") " are aliases for "
|
" is sugar for "
|
||||||
(code "scope-push!") " and " (code "scope-pop!") ". All operations work on a unified "
|
(code "scope")
|
||||||
(code "_scope_stacks") " data structure.")
|
". 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
|
(~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")
|
||||||
" for the full unified platform.")
|
"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
|
(~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
|
||||||
". Element rendering with provide/emit! is visible in "
|
:href "/sx/(language.(spec.(explore.boundary)))"
|
||||||
(a :href "/sx/(language.(spec.(explore.adapter-html)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-html")
|
(~tw :tokens "font-mono text-violet-600 hover:underline text-sm")
|
||||||
" and "
|
"boundary.sx explorer")
|
||||||
(a :href "/sx/(language.(spec.(explore.adapter-async)))" (~tw :tokens "font-mono text-violet-600 hover:underline text-sm") "adapter-async")
|
". 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")
|
||||||
|
".")))))
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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."))))
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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"
|
||||||
|
|||||||
@@ -1,84 +1,74 @@
|
|||||||
|
|
||||||
|
|
||||||
(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
|
|
||||||
(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
|
|
||||||
(fn
|
(fn
|
||||||
()
|
(path)
|
||||||
(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
|
|
||||||
(let
|
(let
|
||||||
((prefix (cek-try (fn () (or (dict-get __app-config :path-prefix) "/sx/")) (fn (e) "/sx/")))
|
((prefix (cek-try (fn () (or (dict-get __app-config :path-prefix) "/sx/")) (fn (e) "/sx/")))
|
||||||
(prefix-len (len prefix))
|
(prefix-len (len prefix))
|
||||||
(nav-path
|
(prefix-bare (slice prefix 0 (- prefix-len 1))))
|
||||||
(if
|
(cond
|
||||||
(and
|
(or (= path "/") (= path prefix) (= path prefix-bare))
|
||||||
(>= (len path) prefix-len)
|
"home"
|
||||||
(= (slice path 0 prefix-len) prefix))
|
(starts-with? path prefix)
|
||||||
path
|
(join " " (split (slice path prefix-len (len path)) "."))
|
||||||
(str (slice prefix 0 (- prefix-len 1)) path))))
|
(starts-with? path "/")
|
||||||
{:page-ast page-ast :nav-path nav-path :is-ajax is-ajax})))))
|
(join " " (split (slice path 1 (len path)) "."))
|
||||||
|
:else path))))
|
||||||
|
(define
|
||||||
)) ;; end define-library
|
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
|
;; Re-export to global namespace for backward compatibility
|
||||||
(import (web request-handler))
|
(import (web request-handler))
|
||||||
|
|||||||
Reference in New Issue
Block a user