diff --git a/lib/host/blog.sx b/lib/host/blog.sx
index 419a0588..6473bfe4 100644
--- a/lib/host/blog.sx
+++ b/lib/host/blog.sx
@@ -481,6 +481,19 @@
(host/blog-fields-of t)))
(list)
(host/blog-types-of slug))))
+;; the COMPOSITION fields an object has — the fields its (transitive) types declare with
+;; :type "Composition" (each edited by its own block editor). Default ["body"] if a type
+;; declares none, so every object still has one root composition. This is layer 2: "types
+;; declare which fields are compositions" (the schema for the object's structure).
+(define host/blog--composition-fields
+ (fn (slug)
+ (let ((cf (reduce (fn (acc f) (if (= (get f :type) "Composition") (concat acc (list (str (get f :name)))) acc))
+ (list) (host/blog--fields-for-post slug))))
+ (if (empty? cf) (list "body") cf))))
+;; the SCALAR (non-composition) fields — the generic edit form's inputs (compositions get a
+;; block editor instead of a text input).
+(define host/blog--scalar-fields
+ (fn (slug) (filter (fn (f) (not (= (get f :type) "Composition"))) (host/blog--fields-for-post slug))))
;; render one labelled input per field, pre-filled from `values`. Widget per value-type
;; (textarea for Text, else a typed ). Pure — takes pre-fetched fields + values.
(define host/blog--field-inputs
@@ -516,11 +529,8 @@
;; A record may carry a :body — a composition node (seq/par/alt/each over object refs)
;; rendered by the render-fold (lib/host/compose.sx) against a context. When present it
;; supersedes :sx-content. This is fold #1; the same object renders differently per context.
-(define host/blog-body-of (fn (slug) (get (host/blog-get slug) :body)))
-(define host/blog--set-body!
- (fn (slug body)
- (let ((r (host/blog-get slug)))
- (when r (host/blog--write! slug (merge r {:body body}))))))
+(define host/blog-body-of (fn (slug) (host/blog--comp-of slug "body")))
+(define host/blog--set-body! (fn (slug body) (host/blog--set-comp! slug "body" body)))
;; The resolver for the composition `each` graph-query source (compose.sx asks the context
;; for "query"). `(query REL TYPE)` -> the objects related to TYPE by REL, as full records
;; so the per-item template can field them. Today the supported relation is is-a (TYPE's
@@ -731,13 +741,30 @@
;; CONDITIONAL (alt+when — the "or": show the first branch whose condition holds), or a
;; REPEATER (each — the loop: render a template per graph-query result). seq is the "and".
;; The editor edits this inline tree; leaves stay external refs. (composition-objects.md.)
-(define host/blog-body-nodes
- (fn (slug)
- (let ((body (host/blog-body-of slug)))
- (if (and (= (type-of body) "list") (= (str (first body)) "seq"))
- (rest body) (list)))))
-(define host/blog--set-body-nodes!
- (fn (slug nodes) (host/blog--set-body! slug (cons (quote seq) nodes))))
+;; a composition FIELD's value on an object — inline, part of the CID. A type declares which
+;; of its fields are compositions (host/blog--composition-fields); an object may carry several
+;; (:body, :aside, …), each edited by its own block editor. Compositions live in a STRING-KEYED
+;; sub-dict :comps (string keys round-trip through persist cleanly, unlike a mix of keyword and
+;; string top-level keys). The default "body" field falls back to a legacy top-level :body.
+(define host/blog--comps (fn (rec) (or (get rec :comps) {})))
+(define host/blog--comp-of
+ (fn (slug field)
+ (let ((r (host/blog-get slug)))
+ (let ((c (get (host/blog--comps r) field)))
+ (if (nil? c) (if (= field "body") (get r :body) nil) c)))))
+(define host/blog--set-comp!
+ (fn (slug field v)
+ (let ((r (host/blog-get slug)))
+ (when r (host/blog--write! slug (assoc r :comps (assoc (host/blog--comps r) field v)))))))
+(define host/blog--comp-nodes
+ (fn (slug field)
+ (let ((c (host/blog--comp-of slug field)))
+ (if (and (= (type-of c) "list") (= (str (first c)) "seq")) (rest c) (list)))))
+(define host/blog--set-comp-nodes!
+ (fn (slug field nodes) (host/blog--set-comp! slug field (cons (quote seq) nodes))))
+;; back-compat: the default "body" field.
+(define host/blog-body-nodes (fn (slug) (host/blog--comp-nodes slug "body")))
+(define host/blog--set-body-nodes! (fn (slug nodes) (host/blog--set-comp-nodes! slug "body" nodes)))
;; the value at index k of a list (any element type).
(define host/blog--nth
(fn (xs k) (let loop ((i 0) (ys xs))
@@ -770,7 +797,7 @@
(fn (container slug)
(if (starts-with? slug (str container "__")) (substr slug (+ (len container) 2)) slug)))
(define host/blog--append-node!
- (fn (slug node) (host/blog--set-body-nodes! slug (concat (host/blog-body-nodes slug) (list node)))))
+ (fn (slug field node) (host/blog--set-comp-nodes! slug field (concat (host/blog--comp-nodes slug field) (list node)))))
;; the kind of a body node, for the editor: "card" | "cond" | "each" | "other".
(define host/blog--node-kind
(fn (node)
@@ -863,55 +890,55 @@
(else "auth")))
"auth")))
-;; add a CARD block to `field`: (ref ). Returns the new card's storage slug.
+;; add a CARD block to a composition `field`: (ref ). Returns the card slug.
(define host/blog-block-add!
- (fn (slug ctype fields)
- (let ((cslug (host/blog--new-card! slug "body" ctype fields)))
- (begin (host/blog--append-node! slug (list (quote ref) (host/blog--slug->ref slug cslug))) cslug))))
+ (fn (slug field ctype fields)
+ (let ((cslug (host/blog--new-card! slug field ctype fields)))
+ (begin (host/blog--append-node! slug field (list (quote ref) (host/blog--slug->ref slug cslug))) cslug))))
;; add a CONDITIONAL (or) block: (alt (when (ref A)) (else (ref B))) — A/B relative refs.
(define host/blog-block-add-cond!
- (fn (slug ckey)
- (let ((a (host/blog--slug->ref slug (host/blog--new-card! slug "body" "card-text" {"text" "shown when the condition holds"})))
- (b (host/blog--slug->ref slug (host/blog--new-card! slug "body" "card-text" {"text" "shown otherwise"}))))
- (host/blog--append-node! slug
+ (fn (slug field ckey)
+ (let ((a (host/blog--slug->ref slug (host/blog--new-card! slug field "card-text" {"text" "shown when the condition holds"})))
+ (b (host/blog--slug->ref slug (host/blog--new-card! slug field "card-text" {"text" "shown otherwise"}))))
+ (host/blog--append-node! slug field
(list (quote alt)
(list (quote when) (host/blog--cond->pred ckey) (list (quote ref) a))
(list (quote else) (list (quote ref) b)))))))
;; add a REPEATER (each) block: (each (query is-a TYPE) (ref )) — template relative.
(define host/blog-block-add-each!
- (fn (slug type)
- (let ((t (host/blog--slug->ref slug (host/blog--new-card! slug "body" "card-text" {"text" "rendered once per item"}))))
- (host/blog--append-node! slug
+ (fn (slug field type)
+ (let ((t (host/blog--slug->ref slug (host/blog--new-card! slug field "card-text" {"text" "rendered once per item"}))))
+ (host/blog--append-node! slug field
(list (quote each)
(list (quote query) (string->symbol "is-a") (string->symbol type))
(list (quote ref) t))))))
-;; move / remove a block by its INDEX (blocks aren't all single refs, so index-addressed).
+;; move / remove a block in `field` by its INDEX (blocks aren't all single refs).
(define host/blog-block-move-idx!
- (fn (slug i dir)
- (let ((nodes (host/blog-body-nodes slug)))
+ (fn (slug field i dir)
+ (let ((nodes (host/blog--comp-nodes slug field)))
(let ((j (if (= dir "up") (- i 1) (+ i 1))))
(when (and (>= i 0) (< i (len nodes)) (>= j 0) (< j (len nodes)))
- (host/blog--set-body-nodes! slug
+ (host/blog--set-comp-nodes! slug field
(map-indexed (fn (k n) (cond ((= k i) (host/blog--nth nodes j))
((= k j) (host/blog--nth nodes i))
(else n))) nodes)))))))
(define host/blog-block-remove-idx!
- (fn (slug i)
- (let ((nodes (host/blog-body-nodes slug)))
+ (fn (slug field i)
+ (let ((nodes (host/blog--comp-nodes slug field)))
(when (and (>= i 0) (< i (len nodes)))
(begin
;; refs are field-relative; contains edges are keyed by SLUG — resolve before dropping.
(for-each (fn (r) (host/blog-unrelate! slug (host/blog--resolve-ref r {"container" slug}) "contains"))
(host/blog--node-refs (host/blog--nth nodes i)))
- (host/blog--set-body-nodes! slug (host/blog--remove-at nodes i)))))))
+ (host/blog--set-comp-nodes! slug field (host/blog--remove-at nodes i)))))))
;; change a conditional block's `when` condition (its then/else branches are kept).
(define host/blog-block-set-cond!
- (fn (slug i ckey)
- (let ((nodes (host/blog-body-nodes slug)))
+ (fn (slug field i ckey)
+ (let ((nodes (host/blog--comp-nodes slug field)))
(when (and (>= i 0) (< i (len nodes)) (= (host/blog--node-kind (host/blog--nth nodes i)) "cond"))
(let ((node (host/blog--nth nodes i)))
(let ((wb (first (rest node))) (eb (first (rest (rest node)))))
- (host/blog--set-body-nodes! slug
+ (host/blog--set-comp-nodes! slug field
(map-indexed
(fn (k n) (if (= k i)
(list (quote alt)
@@ -1564,22 +1591,28 @@
;; #block-editor with the re-render (live reorder/add/remove). Wrapped for the swap target.
;; one sx-post button-form targeting #block-editor (dir passed as a hidden field).
;; index-addressed control button (move up/down, remove, set-cond) -> re-renders #block-editor.
+;; every block-editor form carries a hidden "field" (which composition field it edits) and
+;; targets #comp-, so an object with several composition fields gets several editors
+;; that don't collide. host/blog--fld makes the hidden input; host/blog--tgt the target id.
+(define host/blog--fld (fn (field) (quasiquote (input :type "hidden" :name "field" :value (unquote field)))))
+(define host/blog--tgt (fn (field) (str "#comp-" field)))
(define host/blog--block-btn
- (fn (slug idx action dir label)
+ (fn (slug field idx action dir label)
(let ((url (str "/" slug "/blocks/" idx "/" action)))
(quasiquote
- ;; :sx-post (NOT sx-disable) so the click is a text/sx round-trip through the engine —
- ;; the handler returns the re-rendered #block-editor and sx-swap="outerHTML" replaces it.
+ ;; :sx-post -> a text/sx round-trip; the handler returns the re-rendered field editor
+ ;; and sx-swap="outerHTML" replaces #comp-.
(form :method "post" :action (unquote url) :style "display:inline;margin:0 0.1em"
- :sx-post (unquote url) :sx-target "#block-editor" :sx-swap "outerHTML"
+ :sx-post (unquote url) :sx-target (unquote (host/blog--tgt field)) :sx-swap "outerHTML"
+ (unquote (host/blog--fld field))
(unquote (if (= dir "") "" (quasiquote (input :type "hidden" :name "dir" :value (unquote dir)))))
(button :type "submit" (unquote label)))))))
(define host/blog--block-ctrls
- (fn (slug idx)
+ (fn (slug field idx)
(quasiquote (span :style "white-space:nowrap"
- (unquote (host/blog--block-btn slug idx "move" "up" "↑"))
- (unquote (host/blog--block-btn slug idx "move" "down" "↓"))
- (unquote (host/blog--block-btn slug idx "remove" "" "remove"))))))
+ (unquote (host/blog--block-btn slug field idx "move" "up" "↑"))
+ (unquote (host/blog--block-btn slug field idx "move" "down" "↓"))
+ (unquote (host/blog--block-btn slug field idx "remove" "" "remove"))))))
;; a ✎ edit-fields link + preview for a card REF — refs are field-relative, resolved to the
;; card's own //edit page (external object; editing it is CID-neutral to the container).
(define host/blog--ref-chip
@@ -1588,82 +1621,92 @@
(quasiquote (span
(a :href (unquote (str "/" cslug "/edit")) "✎")
" " (span :style "color:#555" (unquote (host/blog--block-preview (host/blog-field-values-of cslug)))))))))
-;; the condition