host: composition editor for and/or/each + relative-addressed refs (resolve-in-context)
The block editor now edits the object's ONE root composition (:body) as three block kinds —
CARD (a ref leaf, the "and"/content), CONDITIONAL (alt+when, the "or": render the first
branch whose live-context condition holds), and REPEATER (each: render a template per graph
query). The render-fold already interprets seq/alt/when/each/ref, so authored compositions
render for free; this adds the editing model + UI.
ADDRESSING (per the design discussion — refs are IPNS-like, not frozen CIDs): refs are
RELATIVE-STORED + RESOLVE-IN-CONTEXT. A :body stores (ref "body__b0") (field-relative); the
render context carries the CONTAINER (the object being rendered) and the resolver combines
them -> the card's storage slug <container>__<field>__<name>. So a body is portable (doesn't
pin the container's name), and editing a card updates everything that refs it for free (no
cascade). A cross-domain ref is absolute with an authority ("market:…"); the resolver
dispatches on the prefix (local today, fetch_data/AP later). A compat shim resolves an older
absolute ref directly. (Snapshot-to-absolute-CID stays a future on-demand op; the CID —
hash(record incl :body) — is the immutable layer over this naming layer.)
MODEL: host/blog--{card-slug,resolve-ref,slug->ref,new-card!,node-kind,node-refs,node-pred,
node-each-type,cond->pred,pred->ckey}; block-add!/add-cond!/add-each!; index-addressed
block-move-idx!/remove-idx!/set-cond! (alt/each aren't single refs). UI: host/blog--block-row
renders by kind (card / "if <cond> → … else → …" / "for each <type> → …") with a condition
<select> + ✎ links to each card's own /<cslug>/edit (external object, CID-neutral). Routes:
POST /:slug/blocks/{add, add-cond, add-each, :idx/{move,remove,cond}}.
Types-define-structure is the next layer (a type declares its composition field(s) + block
grammar). Full host conformance 399/399 (blog 170, incl. 5 new and/or/each tests: add-cond/
add-each/set-cond, a conditional rendering the context-chosen branch, the 3-form editor).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -770,7 +770,7 @@
|
||||
(let ((out (host/comp-render
|
||||
(quote (each (query is-a qtype)
|
||||
(seq (text "<a href=\"/") (val :slug) (text "\">") (field :title) (text "</a>"))))
|
||||
(host/blog--comp-ctx nil nil))))
|
||||
(host/blog--comp-ctx nil nil nil))))
|
||||
;; field wraps in <span> (display); val is raw (for the href attribute).
|
||||
(list (contains? out "Item One") (contains? out "Item Two")
|
||||
(contains? out "/qi-1") (contains? out "<span>Item One</span>"))))
|
||||
@@ -779,21 +779,21 @@
|
||||
(host-bl-test "each(query is-a TYPE) with no instances renders empty"
|
||||
(host/comp-render
|
||||
(quote (each (query is-a no-such-type) (field :title)))
|
||||
(host/blog--comp-ctx nil nil))
|
||||
(host/blog--comp-ctx nil nil nil))
|
||||
"")
|
||||
;; -- live context: the SAME object renders a responsive variant per request (device from
|
||||
;; the User-Agent, locale from Accept-Language) — context is the execution environment. --
|
||||
(host-bl-test "comp-ctx reads device + locale from the request headers"
|
||||
(let ((mob (host/blog--comp-ctx nil (dream-request "GET" "/x" {"user-agent" "X iPhone Y" "accept-language" "fr-FR,fr"} "")))
|
||||
(desk (host/blog--comp-ctx nil (dream-request "GET" "/x" {"user-agent" "Mozilla Linux" "accept-language" "en-US"} ""))))
|
||||
(let ((mob (host/blog--comp-ctx nil (dream-request "GET" "/x" {"user-agent" "X iPhone Y" "accept-language" "fr-FR,fr"} "") nil))
|
||||
(desk (host/blog--comp-ctx nil (dream-request "GET" "/x" {"user-agent" "Mozilla Linux" "accept-language" "en-US"} "") nil)))
|
||||
(list (get mob "device") (get mob "locale") (get desk "device") (get desk "locale")))
|
||||
(list "mobile" "fr" "desktop" "en"))
|
||||
(host-bl-test "one object renders a device-specific variant via (alt (when (eq device …)))"
|
||||
(let ((body (quote (alt (when (eq "device" "mobile") (text "M")) (else (text "D")))))
|
||||
(mob (dream-request "GET" "/x" {"user-agent" "iPhone"} ""))
|
||||
(desk (dream-request "GET" "/x" {"user-agent" "Linux"} "")))
|
||||
(list (host/comp-render body (host/blog--comp-ctx nil mob))
|
||||
(host/comp-render body (host/blog--comp-ctx nil desk))))
|
||||
(list (host/comp-render body (host/blog--comp-ctx nil mob nil))
|
||||
(host/comp-render body (host/blog--comp-ctx nil desk nil))))
|
||||
(list "M" "D"))
|
||||
;; -- cards-as-objects: the importer decomposes content into card OBJECTS + a contains body
|
||||
;; (not one opaque sx_content string). Each top-level block becomes a stored card object
|
||||
@@ -805,11 +805,11 @@
|
||||
"sx_content" "(article (h1 \"Heading One\") (p \"Para text.\") (img :src \"p.jpg\" :alt \"alt\"))"
|
||||
"status" "published"})
|
||||
(list (len (host/blog-out "imp-x" "contains"))
|
||||
(host/blog-is-a? "imp-x__b0" "card-heading")
|
||||
(host/blog-is-a? "imp-x__b1" "card-text")
|
||||
(host/blog-is-a? "imp-x__b2" "card-image")
|
||||
(get (host/blog-field-values-of "imp-x__b0") "text")
|
||||
(get (host/blog-field-values-of "imp-x__b2") "src")))
|
||||
(host/blog-is-a? "imp-x__body__b0" "card-heading")
|
||||
(host/blog-is-a? "imp-x__body__b1" "card-text")
|
||||
(host/blog-is-a? "imp-x__body__b2" "card-image")
|
||||
(get (host/blog-field-values-of "imp-x__body__b0") "text")
|
||||
(get (host/blog-field-values-of "imp-x__body__b2") "src")))
|
||||
(list 3 true true true "Heading One" "p.jpg"))
|
||||
(host-bl-test "a decomposed post :body is a (seq (ref …) …) composition"
|
||||
(let ((body (host/blog-body-of "imp-x")))
|
||||
@@ -817,7 +817,7 @@
|
||||
(list "seq" 3 "ref"))
|
||||
;; the card objects are status "block" — stored but NOT listed as top-level posts.
|
||||
(host-bl-test "decomposed card objects do not appear on the published home index"
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/"))) "imp-x__b0") false)
|
||||
(contains? (dream-resp-body (host-bl-app (host-bl-req "/"))) "imp-x__body__b0") false)
|
||||
;; the post page renders the cards by TRANSCLUSION (ref -> card-type template).
|
||||
(host-bl-test "decomposed post page renders the transcluded cards"
|
||||
(let ((html (dream-resp-body (host-bl-app (host-bl-req "/imp-x/")))))
|
||||
@@ -833,22 +833,22 @@
|
||||
(list (host/blog-body-refs "bdoc")
|
||||
(host/blog-is-a? c0 "card-text")
|
||||
(contains? (host/blog-out "bdoc" "contains") c1))))
|
||||
(list (list "bdoc__b0" "bdoc__b1") true true))
|
||||
(host-bl-test "block-move! reorders the body refs (and is a no-op at the ends)"
|
||||
(list (list "body__b0" "body__b1") true true))
|
||||
(host-bl-test "block-move-idx! reorders the body by index (no-op at the ends)"
|
||||
(begin
|
||||
(host/blog-block-move! "bdoc" "bdoc__b1" "up") ;; b1 before b0
|
||||
(host/blog-block-move-idx! "bdoc" 1 "up") ;; node 1 before node 0
|
||||
(let ((after-up (host/blog-body-refs "bdoc")))
|
||||
(host/blog-block-move! "bdoc" "bdoc__b1" "up") ;; b1 already first -> no-op
|
||||
(host/blog-block-move-idx! "bdoc" 0 "up") ;; index 0 up -> no-op
|
||||
(list after-up (host/blog-body-refs "bdoc"))))
|
||||
(list (list "bdoc__b1" "bdoc__b0") (list "bdoc__b1" "bdoc__b0")))
|
||||
(host-bl-test "block-remove! drops the ref from the body + the contains edge"
|
||||
(list (list "body__b1" "body__b0") (list "body__b1" "body__b0")))
|
||||
(host-bl-test "block-remove-idx! drops the node + its contained card's contains edge"
|
||||
(begin
|
||||
(host/blog-block-remove! "bdoc" "bdoc__b1")
|
||||
(list (host/blog-body-refs "bdoc") (contains? (host/blog-out "bdoc" "contains") "bdoc__b1")))
|
||||
(list (list "bdoc__b0") false))
|
||||
(host/blog-block-remove-idx! "bdoc" 0) ;; node 0 is now body__b1
|
||||
(list (host/blog-body-refs "bdoc") (contains? (host/blog-out "bdoc" "contains") "bdoc__body__b1")))
|
||||
(list (list "body__b0") false))
|
||||
(host-bl-test "the edit page shows the block editor (#block-editor + an add-block form)"
|
||||
(let ((html (dream-resp-body (host-bl-wapp (host-bl-send "GET" "/bdoc/edit" "Bearer good" "" "")))))
|
||||
(list (contains? html "block-editor") (contains? html "+ add block")))
|
||||
(list (contains? html "block-editor") (contains? html "+ card")))
|
||||
(list true true))
|
||||
(host-bl-test "POST /bdoc/blocks/add (auth) adds a block -> body grows"
|
||||
(begin
|
||||
@@ -856,6 +856,47 @@
|
||||
"application/x-www-form-urlencoded" "ctype=card-text&text=added+block"))
|
||||
(len (host/blog-body-refs "bdoc")))
|
||||
2)
|
||||
;; -- and/or/each authoring: card (and) / conditional (or) / repeater (each) blocks over the
|
||||
;; type-defined :body composition. Refs are field-relative; contains edges track the cards. --
|
||||
(host-bl-test "block-add-cond! appends an (alt (when …) (else …)) with then/else cards"
|
||||
(begin
|
||||
(host/blog-put! "cdoc2" "C2" "(article)" "published")
|
||||
(host/blog--set-body! "cdoc2" (quote (seq)))
|
||||
(host/blog-block-add-cond! "cdoc2" "device:mobile")
|
||||
(let ((n (host/blog--nth (host/blog-body-nodes "cdoc2") 0)))
|
||||
(list (host/blog--node-kind n)
|
||||
(host/blog--pred->ckey (host/blog--node-pred n))
|
||||
(len (host/blog-out "cdoc2" "contains"))))) ;; two cards contained
|
||||
(list "cond" "device:mobile" 2))
|
||||
(host-bl-test "block-set-cond! changes the condition (branches kept)"
|
||||
(begin
|
||||
(host/blog-block-set-cond! "cdoc2" 0 "locale:fr")
|
||||
(let ((n (host/blog--nth (host/blog-body-nodes "cdoc2") 0)))
|
||||
(list (host/blog--pred->ckey (host/blog--node-pred n)) (host/blog--node-kind n))))
|
||||
(list "locale:fr" "cond"))
|
||||
(host-bl-test "block-add-each! appends an (each (query is-a TYPE) (ref …)) repeater"
|
||||
(begin
|
||||
(host/blog-block-add-each! "cdoc2" "compose-item")
|
||||
(let ((n (host/blog--nth (host/blog-body-nodes "cdoc2") 1)))
|
||||
(list (host/blog--node-kind n) (host/blog--node-each-type n))))
|
||||
(list "each" "compose-item"))
|
||||
;; the WHOLE point: a conditional block renders its chosen branch per the live context, via
|
||||
;; the SAME render-fold. (End-to-end: alt+when over "device", cards resolved by relative ref.)
|
||||
(host-bl-test "a conditional block renders the branch chosen by context"
|
||||
(begin
|
||||
(host/blog-put! "cdoc3" "C3" "(article)" "published")
|
||||
(host/blog--set-body! "cdoc3" (quote (seq)))
|
||||
(host/blog-block-add-cond! "cdoc3" "auth")
|
||||
(let ((body (host/blog-body-of "cdoc3")))
|
||||
(list (contains? (host/comp-render body (host/blog--comp-ctx "u" nil "cdoc3")) "shown when the condition holds")
|
||||
(contains? (host/comp-render body (host/blog--comp-ctx nil nil "cdoc3")) "shown otherwise"))))
|
||||
(list true true))
|
||||
;; the editor offers all three block kinds.
|
||||
(host-bl-test "the block editor offers card + conditional + repeater add forms"
|
||||
(let ((html (render-page (host/blog--block-editor "cdoc2"))))
|
||||
(list (contains? html "+ card") (contains? html "+ conditional")
|
||||
(contains? html "+ repeater") (contains? html "for each")))
|
||||
(list true true true true))
|
||||
;; -- /workflow-demo: ONE composition object run through the EXECUTE-fold (step 7 live). The
|
||||
;; same :body structure the render-fold renders, folded to an effect log (status=ready ->
|
||||
;; validate, publish, notify each — not hold). --
|
||||
|
||||
Reference in New Issue
Block a user