;; --------------------------------------------------------------------------- ;; Provide / Context / Emit! — render-time dynamic scope ;; --------------------------------------------------------------------------- ;; ---- Demo components ---- (defcomp ~geography/demo-provide-basic () (div (~tw :tokens "space-y-2") (provide "theme" {:primary "violet" :accent "rose"} (div (~tw :tokens "rounded-lg p-3 bg-violet-50 border border-violet-200") (p (~tw :tokens "text-sm text-violet-800 font-semibold") "Inside provider: theme.primary = violet") (p (~tw :tokens "text-xs text-stone-500") "Child reads context value without prop threading."))) (div (~tw :tokens "rounded-lg p-3 bg-stone-50 border border-stone-200") (p (~tw :tokens "text-sm text-stone-600") "Outside provider: no theme context.")))) (defcomp ~geography/demo-emit-collect () (div (~tw :tokens "space-y-2") (provide "scripts" nil (div (~tw :tokens "rounded-lg p-3 bg-stone-50 border border-stone-200") (p (~tw :tokens "text-sm text-stone-700") (emit! "scripts" "analytics.js") (emit! "scripts" "charts.js") "Page content renders here. Scripts emitted silently.")) (div (~tw :tokens "rounded-lg p-3 bg-violet-50 border border-violet-200") (p (~tw :tokens "text-sm text-violet-800 font-semibold") "Emitted scripts:") (ul (~tw :tokens "text-xs text-stone-600 list-disc pl-5") (map (fn (s) (li (code s))) (emitted "scripts"))))))) (defcomp ~geography/demo-spread-mechanism () (div (~tw :tokens "space-y-2") (div (make-spread (~tw :tokens "rounded-lg p-3 bg-rose-50 border border-rose-200")) (p (~tw :tokens "text-sm text-rose-800 font-semibold") "Spread child styled this div") (p (~tw :tokens "text-xs text-stone-500") "The spread emitted into the element-attrs provider.")) (let ((card (make-spread (~tw :tokens "rounded-lg p-3 bg-amber-50 border border-amber-200")))) (div card (p (~tw :tokens "text-sm text-amber-800 font-semibold") "Stored spread, same mechanism") (p (~tw :tokens "text-xs text-stone-500") "Bound to a let variable, applied when rendered as child."))))) (defcomp ~geography/demo-nested-provide () (div (~tw :tokens "space-y-2") (provide "level" "outer" (div (~tw :tokens "rounded-lg p-3 bg-stone-50 border border-stone-200") (p (~tw :tokens "text-sm text-stone-700") (str "Level: " (context "level"))) (provide "level" "inner" (div (~tw :tokens "rounded-lg p-3 bg-violet-50 border border-violet-200 ml-4") (p (~tw :tokens "text-sm text-violet-700") (str "Level: " (context "level"))))) (p (~tw :tokens "text-sm text-stone-500 mt-1") (str "Back to: " (context "level"))))))) ;; ---- Layout helper (reuse from spreads article) ---- (defcomp ~geography/provide-demo-example (&key demo code) (div (~tw :tokens "grid grid-cols-1 lg:grid-cols-2 gap-4 my-6 items-start") (div (~tw :tokens "border border-dashed border-stone-300 rounded-lg p-4 bg-stone-50 min-h-[80px]") demo) (div (~tw :tokens "not-prose bg-stone-100 rounded-lg p-4 overflow-x-auto") (pre (~tw :tokens "text-sm leading-relaxed whitespace-pre-wrap break-words") (code code))))) ;; ---- Page content ---- (defcomp ~geography/provide-content () (~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") " 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/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: