Files
rose-ash/lib/host/compose.sx
giles cdbb5bb4ba
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 35s
host: composition-objects render-fold — seq/par/alt/each + recursion + context (keystone)
The cards-as-OBJECTS model (plans/composition-objects.md): an object's :body is a tiny UI
language over content-addressed object refs; the render-fold is its interpreter. Four
combinators — seq (sequence) / row,grid (layout/par) / alt+when (conditional/or) / each
(iteration/loop) — plus field/text/card leaves, ref (transclude), and tmpl (recursion).

The two fundamentals designed IN: (1) recursion via self-referential named templates
(tmpl) + each over (children) + a depth guard — renders trees (verified: a nested type
hierarchy -> [Types[Article][Card[Image][Callout]]]); (2) the context is an extensible
ENVIRONMENT —  reads it,  extends it (:item, :depth) — so behaviour (Slice 9)
and reactivity (signals) plug in via the context with no new combinators.

and/or/choice fall out of one axis ( on forks) x the container strategy (render-all
vs render-first), so Alt isn't a new node — it's 'first'. The unifying property, proven:
the object's CID is its DEFINITION (query/template/every when-variant); render is the
EXECUTION (which items/branch/context). One object renders two ways by context (anon ->
'Please log in', authed -> 'Members area'). Render-fold and the Slice-9 behaviour interpreter
are the same shape — interpreters over content-addressed objects.

lib/host/compose.sx is self-contained (no blog deps); verified via sx_eval (every combinator
+ a recursive tree + a full composed doc across two contexts). Roadmap: wire :body into
host/blog-render, each-source=graph-query, live context, Lexical->card-objects import, block
editor.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 17:11:17 +00:00

99 lines
4.8 KiB
Plaintext

;; lib/host/compose.sx — the composition / object render-fold (plans/composition-objects.md).
;;
;; An object's :body is a composition node — a tiny UI language over object refs. The
;; render-fold below is its interpreter. Four combinators (seq/row/alt/each) + leaves
;; (field/text/card) + ref + recursion (tmpl). The context is an EXTENSIBLE ENVIRONMENT:
;; `when` reads it, `each` extends it (:item, :depth). Same predicate set as the type
;; guards. The object's CID is its DEFINITION; render is the EXECUTION (per context+data).
;; Self-contained (no blog deps) so the model can be proven in isolation.
;; ── predicates for `when` (over the context environment) ────────────
(define host/comp--pred?
(fn (pred ctx)
(let ((op (str (first pred))))
(cond
((= op "has") (not (nil? (get ctx (str (first (rest pred)))))))
((= op "eq") (= (str (get ctx (str (first (rest pred))))) (str (first (rest (rest pred))))))
((= op "not") (not (host/comp--pred? (first (rest pred)) ctx)))
(else false)))))
;; the value of a leaf (field): the current :item's key, else the context's key.
(define host/comp--field
(fn (k ctx)
(let ((item (get ctx "item")) (key (str k)))
(if (and item (not (nil? (get item key))))
(str (get item key))
(str (or (get ctx key) ""))))))
;; the source collection for `each`: literal items, the :item's :children (trees), or a
;; named list field on the :item. (A graph-query source is wiring step 3, plan roadmap.)
(define host/comp--source
(fn (src ctx)
(let ((op (str (first src))) (item (get ctx "item")))
(cond
((= op "items") (rest src))
((= op "children") (if item (or (get item "children") (list)) (list)))
((= op "field") (if item (or (get item (str (first (rest src)))) (list)) (list)))
(else (list))))))
;; ── template registry (recursion: a template may reference itself by name) ──
(define host/comp--tmpls (dict))
(define host/comp--def-tmpl! (fn (name node) (dict-set! host/comp--tmpls name node)))
;; ── the render-fold (the interpreter) ───────────────────────────────
(define host/comp--render-all
(fn (nodes ctx) (reduce (fn (acc n) (str acc (host/comp--render n ctx))) "" nodes)))
;; alt: render the FIRST branch whose `when` holds (or `else`) — recursive first-match so
;; a branch that legitimately renders empty isn't skipped.
(define host/comp--alt-pick
(fn (branches ctx)
(if (empty? branches)
""
(let ((br (first branches)) (bh (str (first (first branches)))))
(cond
((= bh "else") (host/comp--render (first (rest br)) ctx))
((= bh "when") (if (host/comp--pred? (first (rest br)) ctx)
(host/comp--render (first (rest (rest br))) ctx)
(host/comp--alt-pick (rest branches) ctx)))
(else (host/comp--alt-pick (rest branches) ctx)))))))
;; each: eval source -> items; render template per item with :item bound + :depth+1
;; (depth guard backstops runaway recursion; trees terminate naturally on empty source).
(define host/comp--each
(fn (src tmpl ctx)
(let ((depth (or (get ctx "depth") 0)))
(if (> depth 40)
"<em>(max depth)</em>"
(reduce
(fn (acc item)
(str acc (host/comp--render tmpl (merge ctx {"item" item "depth" (+ depth 1)}))))
"" (host/comp--source src ctx))))))
;; card leaf (proof: a labelled box; in the host this renders via the card-type's :template).
(define host/comp--card
(fn (ctype fields)
(str "<div class=\"card card-" ctype "\">"
(reduce (fn (acc k) (str acc "<b>" k ":</b> " (str (get fields k)) " ")) "" (keys fields))
"</div>")))
(define host/comp--render
(fn (node ctx)
(if (not (= (type-of node) "list"))
(str node)
(let ((h (str (first node))) (args (rest node)))
(cond
((= h "seq") (host/comp--render-all args ctx))
((= h "row") (str "<div class=\"row\" style=\"display:flex;gap:1em\">" (host/comp--render-all args ctx) "</div>"))
((= h "grid") (str "<div class=\"grid\" style=\"display:grid;gap:1em\">" (host/comp--render-all args ctx) "</div>"))
((= h "alt") (host/comp--alt-pick args ctx))
((= h "each") (host/comp--each (first args) (first (rest args)) ctx))
((= h "field") (str "<span>" (host/comp--field (first args) ctx) "</span>"))
((= h "text") (str (first args)))
((= h "card") (host/comp--card (str (first args)) (first (rest args))))
((= h "tmpl") (host/comp--render (get host/comp--tmpls (str (first args))) ctx))
(else ""))))))
;; public entry: render a composition node against a context environment.
(define host/comp-render (fn (node ctx) (host/comp--render node ctx)))