Refactor spread to use provide/emit! internally

Spreads now emit their attrs into the nearest element's provide scope
instead of requiring per-child spread? checks at every intermediate
layer. emit! is tolerant (no-op when no provider), so spreads in
non-element contexts silently vanish.

- adapter-html: element/lake/marsh wrap children in provide, collect
  emitted; removed 14 spread filters from fragment, forms, components
- adapter-sx: aser wraps result to catch spread values from fn calls;
  aser-call uses provide with attr-parts/child-parts ordering
- adapter-async: same pattern for both render and aser paths
- adapter-dom: added emit! in spread dispatch + provide in element
  rendering; kept spread? checks for reactive/island and DOM safety
- platform: emit! returns NIL when no provider instead of erroring
- 3 new aser tests: stored spread, nested element, silent drop

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-13 15:41:32 +00:00
parent 859aad4333
commit 2de4ba8c57
9 changed files with 444 additions and 526 deletions

View File

@@ -30,8 +30,8 @@
"keyword" (escape-html (keyword-name expr))
;; Raw HTML passthrough
"raw-html" (raw-html-content expr)
;; Spread — pass through as-is (parent element will merge attrs)
"spread" expr
;; Spread — emit attrs to nearest element provider
"spread" (do (emit! "element-attrs" (spread-attrs expr)) "")
;; Everything else — evaluate first
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
@@ -44,7 +44,7 @@
"boolean" (if val "true" "false")
"list" (render-list-to-html val env)
"raw-html" (raw-html-content val)
"spread" val
"spread" (do (emit! "element-attrs" (spread-attrs val)) "")
:else (escape-html (str val)))))
@@ -73,16 +73,14 @@
""
(let ((head (first expr)))
(if (not (= (type-of head) "symbol"))
;; Data list — render each item (spreads filtered — no parent element)
(join "" (filter (fn (x) (not (spread? x)))
(map (fn (x) (render-value-to-html x env)) expr)))
;; Data list — render each item
(join "" (map (fn (x) (render-value-to-html x env)) expr))
(let ((name (symbol-name head))
(args (rest expr)))
(cond
;; Fragment (spreads filtered — no parent element)
;; Fragment
(= name "<>")
(join "" (filter (fn (x) (not (spread? x)))
(map (fn (x) (render-to-html x env)) args)))
(join "" (map (fn (x) (render-to-html x env)) args))
;; Raw HTML passthrough
(= name "raw!")
@@ -152,15 +150,14 @@
(render-to-html (nth expr 3) env)
"")))
;; when — single body: pass through (spread propagates). Multi: join strings.
;; when — single body: pass through. Multi: join strings.
(= name "when")
(if (not (trampoline (eval-expr (nth expr 1) env)))
""
(if (= (len expr) 3)
(render-to-html (nth expr 2) env)
(let ((results (map (fn (i) (render-to-html (nth expr i) env))
(range 2 (len expr)))))
(join "" (filter (fn (r) (not (spread? r))) results)))))
(join "" (map (fn (i) (render-to-html (nth expr i) env))
(range 2 (len expr))))))
;; cond
(= name "cond")
@@ -178,64 +175,59 @@
(let ((local (process-bindings (nth expr 1) env)))
(if (= (len expr) 3)
(render-to-html (nth expr 2) local)
(let ((results (map (fn (i) (render-to-html (nth expr i) local))
(range 2 (len expr)))))
(join "" (filter (fn (r) (not (spread? r))) results)))))
(join "" (map (fn (i) (render-to-html (nth expr i) local))
(range 2 (len expr))))))
;; begin / do — single body: pass through. Multi: join strings.
(or (= name "begin") (= name "do"))
(if (= (len expr) 2)
(render-to-html (nth expr 1) env)
(let ((results (map (fn (i) (render-to-html (nth expr i) env))
(range 1 (len expr)))))
(join "" (filter (fn (r) (not (spread? r))) results))))
(join "" (map (fn (i) (render-to-html (nth expr i) env))
(range 1 (len expr)))))
;; Definition forms — eval for side effects
(definition-form? name)
(do (trampoline (eval-expr expr env)) "")
;; map — spreads filtered (no parent element in list context)
;; map
(= name "map")
(let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env))))
(join ""
(filter (fn (r) (not (spread? r)))
(map
(fn (item)
(if (lambda? f)
(render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env)))
coll))))
(map
(fn (item)
(if (lambda? f)
(render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env)))
coll)))
;; map-indexed — spreads filtered
;; map-indexed
(= name "map-indexed")
(let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env))))
(join ""
(filter (fn (r) (not (spread? r)))
(map-indexed
(fn (i item)
(if (lambda? f)
(render-lambda-html f (list i item) env)
(render-to-html (apply f (list i item)) env)))
coll))))
(map-indexed
(fn (i item)
(if (lambda? f)
(render-lambda-html f (list i item) env)
(render-to-html (apply f (list i item)) env)))
coll)))
;; filter — evaluate fully then render
(= name "filter")
(render-to-html (trampoline (eval-expr expr env)) env)
;; for-each (render variant) — spreads filtered
;; for-each (render variant)
(= name "for-each")
(let ((f (trampoline (eval-expr (nth expr 1) env)))
(coll (trampoline (eval-expr (nth expr 2) env))))
(join ""
(filter (fn (r) (not (spread? r)))
(map
(fn (item)
(if (lambda? f)
(render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env)))
coll))))
(map
(fn (item)
(if (lambda? f)
(render-lambda-html f (list item) env)
(render-to-html (apply f (list item)) env)))
coll)))
;; provide — render-time dynamic scope
(= name "provide")
@@ -246,9 +238,8 @@
(provide-push! prov-name prov-val)
(let ((result (if (= body-count 1)
(render-to-html (nth expr body-start) env)
(join "" (filter (fn (r) (not (spread? r)))
(map (fn (i) (render-to-html (nth expr i) env))
(range body-start (+ body-start body-count))))))))
(join "" (map (fn (i) (render-to-html (nth expr i) env))
(range body-start (+ body-start body-count)))))))
(provide-pop! prov-name)
result))
@@ -307,17 +298,9 @@
(env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
(component-params comp))
;; If component accepts children, pre-render them to raw HTML
;; Spread values are filtered out (no parent element to merge onto)
(when (component-has-children? comp)
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (render-to-html c env)))
(when (not (spread? r))
(append! parts r))))
children)
(env-set! local "children"
(make-raw-html (join "" parts)))))
(env-set! local "children"
(make-raw-html (join "" (map (fn (c) (render-to-html c env)) children)))))
(render-to-html (component-body comp) local)))))
@@ -329,18 +312,17 @@
(is-void (contains? VOID_ELEMENTS tag)))
(if is-void
(str "<" tag (render-attrs attrs) " />")
;; Render children, collecting spreads and content separately
(let ((content-parts (list)))
(for-each
(fn (c)
(let ((result (render-to-html c env)))
(if (spread? result)
(merge-spread-attrs attrs (spread-attrs result))
(append! content-parts result))))
children)
(str "<" tag (render-attrs attrs) ">"
(join "" content-parts)
"</" tag ">"))))))
;; Provide scope for spread emit!
(do
(provide-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each
(fn (spread-dict) (merge-spread-attrs attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(str "<" tag (render-attrs attrs) ">"
content
"</" tag ">")))))))
;; --------------------------------------------------------------------------
@@ -375,19 +357,17 @@
(assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false)
args)
;; Render children, handling spreads
(let ((lake-attrs (dict "data-sx-lake" (or lake-id "")))
(content-parts (list)))
(for-each
(fn (c)
(let ((result (render-to-html c env)))
(if (spread? result)
(merge-spread-attrs lake-attrs (spread-attrs result))
(append! content-parts result))))
children)
(str "<" lake-tag (render-attrs lake-attrs) ">"
(join "" content-parts)
"</" lake-tag ">")))))
;; Provide scope for spread emit!
(let ((lake-attrs (dict "data-sx-lake" (or lake-id ""))))
(provide-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each
(fn (spread-dict) (merge-spread-attrs lake-attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(str "<" lake-tag (render-attrs lake-attrs) ">"
content
"</" lake-tag ">"))))))
;; --------------------------------------------------------------------------
@@ -425,19 +405,17 @@
(assoc state "i" (inc (get state "i"))))))))
(dict "i" 0 "skip" false)
args)
;; Render children, handling spreads
(let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id "")))
(content-parts (list)))
(for-each
(fn (c)
(let ((result (render-to-html c env)))
(if (spread? result)
(merge-spread-attrs marsh-attrs (spread-attrs result))
(append! content-parts result))))
children)
(str "<" marsh-tag (render-attrs marsh-attrs) ">"
(join "" content-parts)
"</" marsh-tag ">")))))
;; Provide scope for spread emit!
(let ((marsh-attrs (dict "data-sx-marsh" (or marsh-id ""))))
(provide-push! "element-attrs" nil)
(let ((content (join "" (map (fn (c) (render-to-html c env)) children))))
(for-each
(fn (spread-dict) (merge-spread-attrs marsh-attrs spread-dict))
(emitted "element-attrs"))
(provide-pop! "element-attrs")
(str "<" marsh-tag (render-attrs marsh-attrs) ">"
content
"</" marsh-tag ">"))))))
;; --------------------------------------------------------------------------
@@ -487,17 +465,9 @@
(component-params island))
;; If island accepts children, pre-render them to raw HTML
;; Spread values filtered out (no parent element)
(when (component-has-children? island)
(let ((parts (list)))
(for-each
(fn (c)
(let ((r (render-to-html c env)))
(when (not (spread? r))
(append! parts r))))
children)
(env-set! local "children"
(make-raw-html (join "" parts)))))
(env-set! local "children"
(make-raw-html (join "" (map (fn (c) (render-to-html c env)) children)))))
;; Render the island body as HTML
(let ((body-html (render-to-html (component-body island) local))