Merge branch 'worktree-api-urls' into macros
This commit is contained in:
@@ -48,6 +48,7 @@
|
||||
"string" (escape-html expr)
|
||||
"number" (escape-html (str expr))
|
||||
"raw-html" (raw-html-content expr)
|
||||
"spread" expr
|
||||
"symbol" (let ((val (async-eval expr env ctx)))
|
||||
(async-render val env ctx))
|
||||
"keyword" (escape-html (keyword-name expr))
|
||||
@@ -79,9 +80,10 @@
|
||||
(= name "raw!")
|
||||
(async-render-raw args env ctx)
|
||||
|
||||
;; Fragment
|
||||
;; Fragment (spreads filtered — no parent element)
|
||||
(= name "<>")
|
||||
(join "" (async-map-render args env ctx))
|
||||
(join "" (filter (fn (r) (not (spread? r)))
|
||||
(async-map-render args env ctx)))
|
||||
|
||||
;; html: prefix
|
||||
(starts-with? name "html:")
|
||||
@@ -167,16 +169,24 @@
|
||||
(let ((class-val (dict-get attrs "class")))
|
||||
(when (and (not (nil? class-val)) (not (= class-val false)))
|
||||
(css-class-collect! (str class-val))))
|
||||
;; Build opening tag
|
||||
(let ((opening (str "<" tag (render-attrs attrs) ">")))
|
||||
(if (contains? VOID_ELEMENTS tag)
|
||||
opening
|
||||
(let ((token (if (or (= tag "svg") (= tag "math"))
|
||||
(svg-context-set! true)
|
||||
nil))
|
||||
(child-html (join "" (async-map-render children env ctx))))
|
||||
(when token (svg-context-reset! token))
|
||||
(str opening child-html "</" tag ">")))))))
|
||||
(if (contains? VOID_ELEMENTS tag)
|
||||
(str "<" tag (render-attrs attrs) ">")
|
||||
;; Render children, collecting spreads and content separately
|
||||
(let ((token (if (or (= tag "svg") (= tag "math"))
|
||||
(svg-context-set! true)
|
||||
nil))
|
||||
(content-parts (list)))
|
||||
(for-each
|
||||
(fn (c)
|
||||
(let ((result (async-render c env ctx)))
|
||||
(if (spread? result)
|
||||
(merge-spread-attrs attrs (spread-attrs result))
|
||||
(append! content-parts result))))
|
||||
children)
|
||||
(when token (svg-context-reset! token))
|
||||
(str "<" tag (render-attrs attrs) ">"
|
||||
(join "" content-parts)
|
||||
"</" tag ">"))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -221,10 +231,17 @@
|
||||
(for-each
|
||||
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params comp))
|
||||
;; Pre-render children to raw HTML (filter spreads — no parent element)
|
||||
(when (component-has-children? comp)
|
||||
(env-set! local "children"
|
||||
(make-raw-html
|
||||
(join "" (async-map-render children env ctx)))))
|
||||
(let ((parts (list)))
|
||||
(for-each
|
||||
(fn (c)
|
||||
(let ((r (async-render c env ctx)))
|
||||
(when (not (spread? r))
|
||||
(append! parts r))))
|
||||
children)
|
||||
(env-set! local "children"
|
||||
(make-raw-html (join "" parts)))))
|
||||
(async-render (component-body comp) local ctx)))))
|
||||
|
||||
|
||||
@@ -242,10 +259,17 @@
|
||||
(for-each
|
||||
(fn (p) (env-set! local p (if (dict-has? kwargs p) (dict-get kwargs p) nil)))
|
||||
(component-params island))
|
||||
;; Pre-render children (filter spreads — no parent element)
|
||||
(when (component-has-children? island)
|
||||
(env-set! local "children"
|
||||
(make-raw-html
|
||||
(join "" (async-map-render children env ctx)))))
|
||||
(let ((parts (list)))
|
||||
(for-each
|
||||
(fn (c)
|
||||
(let ((r (async-render c env ctx)))
|
||||
(when (not (spread? r))
|
||||
(append! parts r))))
|
||||
children)
|
||||
(env-set! local "children"
|
||||
(make-raw-html (join "" parts)))))
|
||||
(let ((body-html (async-render (component-body island) local ctx))
|
||||
(state-json (serialize-island-state kwargs)))
|
||||
(str "<span data-sx-island=\"" (escape-attr island-name) "\""
|
||||
@@ -317,7 +341,7 @@
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"deftype" "defeffect"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
"map" "map-indexed" "filter" "for-each" "provide"))
|
||||
|
||||
(define async-render-form? :effects []
|
||||
(fn ((name :as string))
|
||||
@@ -343,11 +367,14 @@
|
||||
(async-render (nth expr 3) env ctx)
|
||||
"")))
|
||||
|
||||
;; when
|
||||
;; when — single body: pass through (spread propagates). Multi: join strings.
|
||||
(= name "when")
|
||||
(if (not (async-eval (nth expr 1) env ctx))
|
||||
""
|
||||
(join "" (async-map-render (slice expr 2) env ctx)))
|
||||
(if (= (len expr) 3)
|
||||
(async-render (nth expr 2) env ctx)
|
||||
(let ((results (async-map-render (slice expr 2) env ctx)))
|
||||
(join "" (filter (fn (r) (not (spread? r))) results)))))
|
||||
|
||||
;; cond — uses cond-scheme? (every? check) from eval.sx
|
||||
(= name "cond")
|
||||
@@ -360,43 +387,66 @@
|
||||
(= name "case")
|
||||
(async-render (async-eval expr env ctx) env ctx)
|
||||
|
||||
;; let / let*
|
||||
;; let / let* — single body: pass through. Multi: join strings.
|
||||
(or (= name "let") (= name "let*"))
|
||||
(let ((local (async-process-bindings (nth expr 1) env ctx)))
|
||||
(join "" (async-map-render (slice expr 2) local ctx)))
|
||||
(if (= (len expr) 3)
|
||||
(async-render (nth expr 2) local ctx)
|
||||
(let ((results (async-map-render (slice expr 2) local ctx)))
|
||||
(join "" (filter (fn (r) (not (spread? r))) results)))))
|
||||
|
||||
;; begin / do
|
||||
;; begin / do — single body: pass through. Multi: join strings.
|
||||
(or (= name "begin") (= name "do"))
|
||||
(join "" (async-map-render (rest expr) env ctx))
|
||||
(if (= (len expr) 2)
|
||||
(async-render (nth expr 1) env ctx)
|
||||
(let ((results (async-map-render (rest expr) env ctx)))
|
||||
(join "" (filter (fn (r) (not (spread? r))) results))))
|
||||
|
||||
;; Definition forms
|
||||
(definition-form? name)
|
||||
(do (async-eval expr env ctx) "")
|
||||
|
||||
;; map
|
||||
;; map — spreads filtered
|
||||
(= name "map")
|
||||
(let ((f (async-eval (nth expr 1) env ctx))
|
||||
(coll (async-eval (nth expr 2) env ctx)))
|
||||
(join ""
|
||||
(async-map-fn-render f coll env ctx)))
|
||||
(filter (fn (r) (not (spread? r)))
|
||||
(async-map-fn-render f coll env ctx))))
|
||||
|
||||
;; map-indexed
|
||||
;; map-indexed — spreads filtered
|
||||
(= name "map-indexed")
|
||||
(let ((f (async-eval (nth expr 1) env ctx))
|
||||
(coll (async-eval (nth expr 2) env ctx)))
|
||||
(join ""
|
||||
(async-map-indexed-fn-render f coll env ctx)))
|
||||
(filter (fn (r) (not (spread? r)))
|
||||
(async-map-indexed-fn-render f coll env ctx))))
|
||||
|
||||
;; filter — eval fully then render
|
||||
(= name "filter")
|
||||
(async-render (async-eval expr env ctx) env ctx)
|
||||
|
||||
;; for-each (render variant)
|
||||
;; for-each (render variant) — spreads filtered
|
||||
(= name "for-each")
|
||||
(let ((f (async-eval (nth expr 1) env ctx))
|
||||
(coll (async-eval (nth expr 2) env ctx)))
|
||||
(join ""
|
||||
(async-map-fn-render f coll env ctx)))
|
||||
(filter (fn (r) (not (spread? r)))
|
||||
(async-map-fn-render f coll env ctx))))
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
(= name "provide")
|
||||
(let ((prov-name (async-eval (nth expr 1) env ctx))
|
||||
(prov-val (async-eval (nth expr 2) env ctx))
|
||||
(body-start 3)
|
||||
(body-count (- (len expr) 3)))
|
||||
(provide-push! prov-name prov-val)
|
||||
(let ((result (if (= body-count 1)
|
||||
(async-render (nth expr body-start) env ctx)
|
||||
(let ((results (async-map-render (slice expr body-start) env ctx)))
|
||||
(join "" (filter (fn (r) (not (spread? r))) results))))))
|
||||
(provide-pop! prov-name)
|
||||
result))
|
||||
|
||||
;; Fallback
|
||||
:else
|
||||
@@ -565,6 +615,9 @@
|
||||
|
||||
"dict" (async-aser-dict expr env ctx)
|
||||
|
||||
;; Spread — pass through for client rendering
|
||||
"spread" expr
|
||||
|
||||
"list"
|
||||
(if (empty? expr)
|
||||
(list)
|
||||
@@ -855,7 +908,7 @@
|
||||
"define" "defcomp" "defmacro" "defstyle"
|
||||
"defhandler" "defpage" "defquery" "defaction"
|
||||
"begin" "do" "quote" "->" "set!" "defisland"
|
||||
"deftype" "defeffect"))
|
||||
"deftype" "defeffect" "provide"))
|
||||
|
||||
(define ASYNC_ASER_HO_NAMES
|
||||
(list "map" "map-indexed" "filter" "for-each"))
|
||||
@@ -993,6 +1046,17 @@
|
||||
(= name "deftype") (= name "defeffect"))
|
||||
(do (async-eval expr env ctx) nil)
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
(= name "provide")
|
||||
(let ((prov-name (async-eval (first args) env ctx))
|
||||
(prov-val (async-eval (nth args 1) env ctx))
|
||||
(result nil))
|
||||
(provide-push! prov-name prov-val)
|
||||
(for-each (fn (body) (set! result (async-aser body env ctx)))
|
||||
(slice args 2))
|
||||
(provide-pop! prov-name)
|
||||
result)
|
||||
|
||||
;; Fallback
|
||||
:else
|
||||
(async-eval expr env ctx)))))
|
||||
@@ -1250,6 +1314,14 @@
|
||||
;; (svg-context-reset! token) — reset SVG context
|
||||
;; (css-class-collect! val) — collect CSS classes
|
||||
;;
|
||||
;; Spread + collect (from render.sx):
|
||||
;; (spread? x) — check if spread value
|
||||
;; (spread-attrs s) — extract attrs dict from spread
|
||||
;; (merge-spread-attrs tgt src) — merge spread attrs onto target
|
||||
;; (collect! bucket value) — add to render-time accumulator
|
||||
;; (collected bucket) — read render-time accumulator
|
||||
;; (clear-collected! bucket) — clear accumulator
|
||||
;;
|
||||
;; Raw HTML:
|
||||
;; (is-raw-html? x) — check if raw HTML marker
|
||||
;; (make-raw-html s) — wrap string as raw HTML
|
||||
|
||||
@@ -44,6 +44,9 @@
|
||||
;; Pre-rendered DOM node → pass through
|
||||
"dom-node" expr
|
||||
|
||||
;; Spread → pass through (parent element handles it)
|
||||
"spread" expr
|
||||
|
||||
;; Dict → empty
|
||||
"dict" (create-fragment)
|
||||
|
||||
@@ -221,10 +224,34 @@
|
||||
(dom-set-attr el attr-name (str attr-val)))))
|
||||
(assoc state "skip" true "i" (inc (get state "i"))))
|
||||
|
||||
;; Positional arg → child
|
||||
;; Positional arg → child (or spread → merge attrs onto element)
|
||||
(do
|
||||
(when (not (contains? VOID_ELEMENTS tag))
|
||||
(dom-append el (render-to-dom arg env new-ns)))
|
||||
(let ((child (render-to-dom arg env new-ns)))
|
||||
(if (spread? child)
|
||||
;; Spread: merge attrs onto parent element
|
||||
(for-each
|
||||
(fn ((key :as string))
|
||||
(let ((val (dict-get (spread-attrs child) key)))
|
||||
(if (= key "class")
|
||||
;; Class: append to existing
|
||||
(let ((existing (dom-get-attr el "class")))
|
||||
(dom-set-attr el "class"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing " " val)
|
||||
val)))
|
||||
(if (= key "style")
|
||||
;; Style: append with semicolon
|
||||
(let ((existing (dom-get-attr el "style")))
|
||||
(dom-set-attr el "style"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing ";" val)
|
||||
val)))
|
||||
;; Other attrs: overwrite
|
||||
(dom-set-attr el key (str val))))))
|
||||
(keys (spread-attrs child)))
|
||||
;; Normal child: append to element
|
||||
(dom-append el child))))
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
@@ -332,7 +359,7 @@
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"map" "map-indexed" "filter" "for-each" "portal"
|
||||
"error-boundary"))
|
||||
"error-boundary" "provide"))
|
||||
|
||||
(define render-dom-form? :effects []
|
||||
(fn ((name :as string))
|
||||
@@ -571,6 +598,19 @@
|
||||
coll)
|
||||
frag)
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
(= name "provide")
|
||||
(let ((prov-name (trampoline (eval-expr (nth expr 1) env)))
|
||||
(prov-val (trampoline (eval-expr (nth expr 2) env)))
|
||||
(frag (create-fragment)))
|
||||
(provide-push! prov-name prov-val)
|
||||
(for-each
|
||||
(fn (i)
|
||||
(dom-append frag (render-to-dom (nth expr i) env ns)))
|
||||
(range 3 (len expr)))
|
||||
(provide-pop! prov-name)
|
||||
frag)
|
||||
|
||||
;; Fallback
|
||||
:else
|
||||
(render-to-dom (trampoline (eval-expr expr env)) env ns))))
|
||||
|
||||
@@ -30,6 +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
|
||||
;; Everything else — evaluate first
|
||||
:else (render-value-to-html (trampoline (eval-expr expr env)) env))))
|
||||
|
||||
@@ -42,6 +44,7 @@
|
||||
"boolean" (if val "true" "false")
|
||||
"list" (render-list-to-html val env)
|
||||
"raw-html" (raw-html-content val)
|
||||
"spread" val
|
||||
:else (escape-html (str val)))))
|
||||
|
||||
|
||||
@@ -53,7 +56,7 @@
|
||||
(list "if" "when" "cond" "case" "let" "let*" "begin" "do"
|
||||
"define" "defcomp" "defisland" "defmacro" "defstyle" "defhandler"
|
||||
"deftype" "defeffect"
|
||||
"map" "map-indexed" "filter" "for-each"))
|
||||
"map" "map-indexed" "filter" "for-each" "provide"))
|
||||
|
||||
(define render-html-form? :effects []
|
||||
(fn ((name :as string))
|
||||
@@ -70,14 +73,16 @@
|
||||
""
|
||||
(let ((head (first expr)))
|
||||
(if (not (= (type-of head) "symbol"))
|
||||
;; Data list — render each item
|
||||
(join "" (map (fn (x) (render-value-to-html x env)) expr))
|
||||
;; 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)))
|
||||
(let ((name (symbol-name head))
|
||||
(args (rest expr)))
|
||||
(cond
|
||||
;; Fragment
|
||||
;; Fragment (spreads filtered — no parent element)
|
||||
(= name "<>")
|
||||
(join "" (map (fn (x) (render-to-html x env)) args))
|
||||
(join "" (filter (fn (x) (not (spread? x)))
|
||||
(map (fn (x) (render-to-html x env)) args)))
|
||||
|
||||
;; Raw HTML passthrough
|
||||
(= name "raw!")
|
||||
@@ -147,14 +152,15 @@
|
||||
(render-to-html (nth expr 3) env)
|
||||
"")))
|
||||
|
||||
;; when
|
||||
;; when — single body: pass through (spread propagates). Multi: join strings.
|
||||
(= name "when")
|
||||
(if (not (trampoline (eval-expr (nth expr 1) env)))
|
||||
""
|
||||
(join ""
|
||||
(map
|
||||
(fn (i) (render-to-html (nth expr i) env))
|
||||
(range 2 (len expr)))))
|
||||
(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)))))
|
||||
|
||||
;; cond
|
||||
(= name "cond")
|
||||
@@ -167,64 +173,84 @@
|
||||
(= name "case")
|
||||
(render-to-html (trampoline (eval-expr expr env)) env)
|
||||
|
||||
;; let / let*
|
||||
;; let / let* — single body: pass through. Multi: join strings.
|
||||
(or (= name "let") (= name "let*"))
|
||||
(let ((local (process-bindings (nth expr 1) env)))
|
||||
(join ""
|
||||
(map
|
||||
(fn (i) (render-to-html (nth expr i) local))
|
||||
(range 2 (len expr)))))
|
||||
(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)))))
|
||||
|
||||
;; begin / do
|
||||
;; begin / do — single body: pass through. Multi: join strings.
|
||||
(or (= name "begin") (= name "do"))
|
||||
(join ""
|
||||
(map
|
||||
(fn (i) (render-to-html (nth expr i) env))
|
||||
(range 1 (len expr))))
|
||||
(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))))
|
||||
|
||||
;; Definition forms — eval for side effects
|
||||
(definition-form? name)
|
||||
(do (trampoline (eval-expr expr env)) "")
|
||||
|
||||
;; map
|
||||
;; map — spreads filtered (no parent element in list context)
|
||||
(= name "map")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env))))
|
||||
(join ""
|
||||
(map
|
||||
(fn (item)
|
||||
(if (lambda? f)
|
||||
(render-lambda-html f (list item) env)
|
||||
(render-to-html (apply f (list item)) env)))
|
||||
coll)))
|
||||
(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-indexed
|
||||
;; map-indexed — spreads filtered
|
||||
(= name "map-indexed")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env))))
|
||||
(join ""
|
||||
(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 (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))))
|
||||
|
||||
;; filter — evaluate fully then render
|
||||
(= name "filter")
|
||||
(render-to-html (trampoline (eval-expr expr env)) env)
|
||||
|
||||
;; for-each (render variant)
|
||||
;; for-each (render variant) — spreads filtered
|
||||
(= name "for-each")
|
||||
(let ((f (trampoline (eval-expr (nth expr 1) env)))
|
||||
(coll (trampoline (eval-expr (nth expr 2) env))))
|
||||
(join ""
|
||||
(map
|
||||
(fn (item)
|
||||
(if (lambda? f)
|
||||
(render-lambda-html f (list item) env)
|
||||
(render-to-html (apply f (list item)) env)))
|
||||
coll)))
|
||||
(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))))
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
(= name "provide")
|
||||
(let ((prov-name (trampoline (eval-expr (nth expr 1) env)))
|
||||
(prov-val (trampoline (eval-expr (nth expr 2) env)))
|
||||
(body-start 3)
|
||||
(body-count (- (len expr) 3)))
|
||||
(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))))))))
|
||||
(provide-pop! prov-name)
|
||||
result))
|
||||
|
||||
;; Fallback
|
||||
:else
|
||||
@@ -281,10 +307,17 @@
|
||||
(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)
|
||||
(env-set! local "children"
|
||||
(make-raw-html
|
||||
(join "" (map (fn (c) (render-to-html c env)) children)))))
|
||||
(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)))))
|
||||
(render-to-html (component-body comp) local)))))
|
||||
|
||||
|
||||
@@ -294,12 +327,19 @@
|
||||
(attrs (first parsed))
|
||||
(children (nth parsed 1))
|
||||
(is-void (contains? VOID_ELEMENTS tag)))
|
||||
(str "<" tag
|
||||
(render-attrs attrs)
|
||||
(if is-void
|
||||
" />"
|
||||
(str ">"
|
||||
(join "" (map (fn (c) (render-to-html c env)) children))
|
||||
(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 ">"))))))
|
||||
|
||||
|
||||
@@ -335,9 +375,19 @@
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
(str "<" lake-tag " data-sx-lake=\"" (escape-attr (or lake-id "")) "\">"
|
||||
(join "" (map (fn (c) (render-to-html c env)) children))
|
||||
"</" lake-tag ">"))))
|
||||
;; 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 ">")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -375,9 +425,19 @@
|
||||
(assoc state "i" (inc (get state "i"))))))))
|
||||
(dict "i" 0 "skip" false)
|
||||
args)
|
||||
(str "<" marsh-tag " data-sx-marsh=\"" (escape-attr (or marsh-id "")) "\">"
|
||||
(join "" (map (fn (c) (render-to-html c env)) children))
|
||||
"</" marsh-tag ">"))))
|
||||
;; 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 ">")))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -427,10 +487,17 @@
|
||||
(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)
|
||||
(env-set! local "children"
|
||||
(make-raw-html
|
||||
(join "" (map (fn (c) (render-to-html c env)) children)))))
|
||||
(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)))))
|
||||
|
||||
;; Render the island body as HTML
|
||||
(let ((body-html (render-to-html (component-body island) local))
|
||||
|
||||
@@ -48,6 +48,9 @@
|
||||
(list)
|
||||
(aser-list expr env))
|
||||
|
||||
;; Spread — pass through for client rendering
|
||||
"spread" expr
|
||||
|
||||
:else expr)))
|
||||
|
||||
|
||||
@@ -171,7 +174,7 @@
|
||||
"defhandler" "defpage" "defquery" "defaction" "defrelation"
|
||||
"begin" "do" "quote" "quasiquote"
|
||||
"->" "set!" "letrec" "dynamic-wind" "defisland"
|
||||
"deftype" "defeffect"))
|
||||
"deftype" "defeffect" "provide"))
|
||||
|
||||
(define HO_FORM_NAMES
|
||||
(list "map" "map-indexed" "filter" "reduce"
|
||||
@@ -309,6 +312,17 @@
|
||||
(= name "deftype") (= name "defeffect"))
|
||||
(do (trampoline (eval-expr expr env)) nil)
|
||||
|
||||
;; provide — render-time dynamic scope
|
||||
(= name "provide")
|
||||
(let ((prov-name (trampoline (eval-expr (first args) env)))
|
||||
(prov-val (trampoline (eval-expr (nth args 1) env)))
|
||||
(result nil))
|
||||
(provide-push! prov-name prov-val)
|
||||
(for-each (fn (body) (set! result (aser body env)))
|
||||
(slice args 2))
|
||||
(provide-pop! prov-name)
|
||||
result)
|
||||
|
||||
;; Everything else — evaluate normally
|
||||
:else
|
||||
(trampoline (eval-expr expr env))))))
|
||||
|
||||
@@ -285,6 +285,19 @@ class PyEmitter:
|
||||
"svg-context-set!": "svg_context_set",
|
||||
"svg-context-reset!": "svg_context_reset",
|
||||
"css-class-collect!": "css_class_collect",
|
||||
# spread + collect primitives
|
||||
"make-spread": "make_spread",
|
||||
"spread?": "is_spread",
|
||||
"spread-attrs": "spread_attrs",
|
||||
"merge-spread-attrs": "merge_spread_attrs",
|
||||
"collect!": "sx_collect",
|
||||
"collected": "sx_collected",
|
||||
"clear-collected!": "sx_clear_collected",
|
||||
"provide-push!": "provide_push",
|
||||
"provide-pop!": "provide_pop",
|
||||
"context": "sx_context",
|
||||
"emit!": "sx_emit",
|
||||
"emitted": "sx_emitted",
|
||||
"is-raw-html?": "is_raw_html",
|
||||
"async-coroutine?": "is_async_coroutine",
|
||||
"async-await!": "async_await",
|
||||
|
||||
@@ -313,3 +313,109 @@
|
||||
:effects [mutation]
|
||||
:doc "Group multiple signal writes. Subscribers are notified once at the end,
|
||||
after all values have been updated.")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Tier 4: Spread + Collect — render-time attribute injection and accumulation
|
||||
;;
|
||||
;; `spread` is a new type: a dict of attributes that, when returned as a child
|
||||
;; of an HTML element, merges its attrs onto the parent element rather than
|
||||
;; rendering as content. This enables components like `~cssx/tw` to inject
|
||||
;; classes and styles onto their parent from inside the child list.
|
||||
;;
|
||||
;; `collect!` / `collected` are render-time accumulators. Values are collected
|
||||
;; into named buckets (with deduplication) during rendering and retrieved at
|
||||
;; flush points (e.g. a single <style> tag for all collected CSS rules).
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(declare-tier :spread :source "render.sx")
|
||||
|
||||
(declare-spread-primitive "make-spread"
|
||||
:params (attrs)
|
||||
:returns "spread"
|
||||
:effects []
|
||||
:doc "Create a spread value from an attrs dict. When this value appears as
|
||||
a child of an HTML element, its attrs are merged onto the parent
|
||||
element (class values joined, others overwritten).")
|
||||
|
||||
(declare-spread-primitive "spread?"
|
||||
:params (x)
|
||||
:returns "boolean"
|
||||
:effects []
|
||||
:doc "Test whether a value is a spread.")
|
||||
|
||||
(declare-spread-primitive "spread-attrs"
|
||||
:params (s)
|
||||
:returns "dict"
|
||||
:effects []
|
||||
:doc "Extract the attrs dict from a spread value.")
|
||||
|
||||
(declare-spread-primitive "collect!"
|
||||
:params (bucket value)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Add value to a named render-time accumulator bucket. Values are
|
||||
deduplicated (no duplicates added). Buckets persist for the duration
|
||||
of the current render pass.")
|
||||
|
||||
(declare-spread-primitive "collected"
|
||||
:params (bucket)
|
||||
:returns "list"
|
||||
:effects []
|
||||
:doc "Return all values collected in the named bucket during the current
|
||||
render pass. Returns an empty list if the bucket doesn't exist.")
|
||||
|
||||
(declare-spread-primitive "clear-collected!"
|
||||
:params (bucket)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Clear a named render-time accumulator bucket. Used at flush points
|
||||
after emitting collected values (e.g. after writing a <style> tag).")
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Tier 5: Dynamic scope — render-time provide/context/emit!
|
||||
;;
|
||||
;; `provide` is a special form (not a primitive) that creates a named scope
|
||||
;; with a value and an empty accumulator. `context` reads the value from the
|
||||
;; nearest enclosing provider. `emit!` appends to the accumulator, `emitted`
|
||||
;; reads the accumulated values.
|
||||
;;
|
||||
;; The platform must implement per-name stacks. Each entry has a value and
|
||||
;; an emitted list. `provide-push!`/`provide-pop!` manage the stack.
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(declare-tier :dynamic-scope :source "eval.sx")
|
||||
|
||||
(declare-spread-primitive "provide-push!"
|
||||
:params (name value)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Push a provider scope with name and value (platform internal).")
|
||||
|
||||
(declare-spread-primitive "provide-pop!"
|
||||
:params (name)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Pop the most recent provider scope for name (platform internal).")
|
||||
|
||||
(declare-spread-primitive "context"
|
||||
:params (name &rest default)
|
||||
:returns "any"
|
||||
:effects []
|
||||
:doc "Read value from nearest enclosing provide with matching name.
|
||||
Errors if no provider and no default given.")
|
||||
|
||||
(declare-spread-primitive "emit!"
|
||||
:params (name value)
|
||||
:returns "nil"
|
||||
:effects [mutation]
|
||||
:doc "Append value to nearest enclosing provide's accumulator.
|
||||
Errors if no matching provider. No deduplication.")
|
||||
|
||||
(declare-spread-primitive "emitted"
|
||||
:params (name)
|
||||
:returns "list"
|
||||
:effects []
|
||||
:doc "Return list of values emitted into nearest matching provider.
|
||||
Empty list if no provider.")
|
||||
|
||||
@@ -162,6 +162,7 @@
|
||||
(= name "reset") (sf-reset args env)
|
||||
(= name "shift") (sf-shift args env)
|
||||
(= name "dynamic-wind") (sf-dynamic-wind args env)
|
||||
(= name "provide") (sf-provide args env)
|
||||
|
||||
;; Higher-order forms
|
||||
(= name "map") (ho-map args env)
|
||||
@@ -949,6 +950,25 @@
|
||||
result))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6a2. provide — render-time dynamic scope
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; (provide name value body...) — push a named scope with value and empty
|
||||
;; accumulator, evaluate body, pop scope. Returns last body result.
|
||||
|
||||
(define sf-provide
|
||||
(fn ((args :as list) (env :as dict))
|
||||
(let ((name (trampoline (eval-expr (first args) env)))
|
||||
(val (trampoline (eval-expr (nth args 1) env)))
|
||||
(body-exprs (slice args 2))
|
||||
(result nil))
|
||||
(provide-push! name val)
|
||||
(for-each (fn (e) (set! result (trampoline (eval-expr e env)))) body-exprs)
|
||||
(provide-pop! name)
|
||||
result)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; 6b. Macro expansion
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -1064,6 +1084,7 @@
|
||||
;; (type-of x) → "number" | "string" | "boolean" | "nil"
|
||||
;; | "symbol" | "keyword" | "list" | "dict"
|
||||
;; | "lambda" | "component" | "macro" | "thunk"
|
||||
;; | "spread"
|
||||
;; (symbol-name sym) → string
|
||||
;; (keyword-name kw) → string
|
||||
;;
|
||||
@@ -1089,6 +1110,10 @@
|
||||
;; (island? x) → boolean
|
||||
;; ;; Islands reuse component accessors: component-params, component-body, etc.
|
||||
;;
|
||||
;; (make-spread attrs) → Spread (attrs dict injected onto parent element)
|
||||
;; (spread? x) → boolean
|
||||
;; (spread-attrs s) → dict
|
||||
;;
|
||||
;; (macro-params m) → list of strings
|
||||
;; (macro-rest-param m) → string or nil
|
||||
;; (macro-body m) → expr
|
||||
@@ -1132,4 +1157,9 @@
|
||||
;; (push-wind! before after) → void (push wind record onto stack)
|
||||
;; (pop-wind!) → void (pop wind record from stack)
|
||||
;; (call-thunk f env) → value (call a zero-arg function)
|
||||
;;
|
||||
;; Render-time accumulators:
|
||||
;; (collect! bucket value) → void (add to named bucket, deduplicated)
|
||||
;; (collected bucket) → list (all values in bucket)
|
||||
;; (clear-collected! bucket) → void (empty the bucket)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -520,6 +520,18 @@
|
||||
"match-route-segments" "matchRouteSegments"
|
||||
"match-route" "matchRoute"
|
||||
"find-matching-route" "findMatchingRoute"
|
||||
"make-spread" "makeSpread"
|
||||
"spread?" "isSpread"
|
||||
"spread-attrs" "spreadAttrs"
|
||||
"merge-spread-attrs" "mergeSpreadAttrs"
|
||||
"collect!" "sxCollect"
|
||||
"collected" "sxCollected"
|
||||
"clear-collected!" "sxClearCollected"
|
||||
"provide-push!" "providePush"
|
||||
"provide-pop!" "providePop"
|
||||
"context" "sxContext"
|
||||
"emit!" "sxEmit"
|
||||
"emitted" "sxEmitted"
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -864,6 +864,12 @@ PREAMBLE = '''\
|
||||
function RawHTML(html) { this.html = html; }
|
||||
RawHTML.prototype._raw = true;
|
||||
|
||||
function SxSpread(attrs) { this.attrs = attrs || {}; }
|
||||
SxSpread.prototype._spread = true;
|
||||
|
||||
var _collectBuckets = {};
|
||||
var _provideStacks = {};
|
||||
|
||||
function isSym(x) { return x != null && x._sym === true; }
|
||||
function isKw(x) { return x != null && x._kw === true; }
|
||||
|
||||
@@ -1073,6 +1079,22 @@ PRIMITIVES_JS_MODULES: dict[str, str] = {
|
||||
return true;
|
||||
};
|
||||
''',
|
||||
|
||||
"stdlib.spread": '''
|
||||
// stdlib.spread — spread + collect primitives
|
||||
PRIMITIVES["make-spread"] = makeSpread;
|
||||
PRIMITIVES["spread?"] = isSpread;
|
||||
PRIMITIVES["spread-attrs"] = spreadAttrs;
|
||||
PRIMITIVES["collect!"] = sxCollect;
|
||||
PRIMITIVES["collected"] = sxCollected;
|
||||
PRIMITIVES["clear-collected!"] = sxClearCollected;
|
||||
// provide/context/emit! — render-time dynamic scope
|
||||
PRIMITIVES["provide-push!"] = providePush;
|
||||
PRIMITIVES["provide-pop!"] = providePop;
|
||||
PRIMITIVES["context"] = sxContext;
|
||||
PRIMITIVES["emit!"] = sxEmit;
|
||||
PRIMITIVES["emitted"] = sxEmitted;
|
||||
''',
|
||||
}
|
||||
# Modules to include by default (all)
|
||||
_ALL_JS_MODULES = list(PRIMITIVES_JS_MODULES.keys())
|
||||
@@ -1108,6 +1130,7 @@ PLATFORM_JS_PRE = '''
|
||||
if (x._component) return "component";
|
||||
if (x._island) return "island";
|
||||
if (x._signal) return "signal";
|
||||
if (x._spread) return "spread";
|
||||
if (x._macro) return "macro";
|
||||
if (x._raw) return "raw-html";
|
||||
if (typeof Node !== "undefined" && x instanceof Node) return "dom-node";
|
||||
@@ -1130,6 +1153,51 @@ PLATFORM_JS_PRE = '''
|
||||
}
|
||||
function makeThunk(expr, env) { return new Thunk(expr, env); }
|
||||
|
||||
function makeSpread(attrs) { return new SxSpread(attrs || {}); }
|
||||
function isSpread(x) { return x != null && x._spread === true; }
|
||||
function spreadAttrs(s) { return s && s._spread ? s.attrs : {}; }
|
||||
|
||||
function sxCollect(bucket, value) {
|
||||
if (!_collectBuckets[bucket]) _collectBuckets[bucket] = [];
|
||||
var items = _collectBuckets[bucket];
|
||||
if (items.indexOf(value) === -1) items.push(value);
|
||||
}
|
||||
function sxCollected(bucket) {
|
||||
return _collectBuckets[bucket] ? _collectBuckets[bucket].slice() : [];
|
||||
}
|
||||
function sxClearCollected(bucket) {
|
||||
if (_collectBuckets[bucket]) _collectBuckets[bucket] = [];
|
||||
}
|
||||
|
||||
function providePush(name, value) {
|
||||
if (!_provideStacks[name]) _provideStacks[name] = [];
|
||||
_provideStacks[name].push({value: value !== undefined ? value : NIL, emitted: []});
|
||||
}
|
||||
function providePop(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) _provideStacks[name].pop();
|
||||
}
|
||||
function sxContext(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) {
|
||||
return _provideStacks[name][_provideStacks[name].length - 1].value;
|
||||
}
|
||||
if (arguments.length > 1) return arguments[1];
|
||||
throw new Error("No provider for: " + name);
|
||||
}
|
||||
function sxEmit(name, value) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) {
|
||||
_provideStacks[name][_provideStacks[name].length - 1].emitted.push(value);
|
||||
} else {
|
||||
throw new Error("No provider for emit!: " + name);
|
||||
}
|
||||
return NIL;
|
||||
}
|
||||
function sxEmitted(name) {
|
||||
if (_provideStacks[name] && _provideStacks[name].length) {
|
||||
return _provideStacks[name][_provideStacks[name].length - 1].emitted.slice();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
function lambdaParams(f) { return f.params; }
|
||||
function lambdaBody(f) { return f.body; }
|
||||
function lambdaClosure(f) { return f.closure; }
|
||||
@@ -3154,6 +3222,17 @@ def public_api_js(has_html, has_sx, has_dom, has_engine, has_orch, has_boot, has
|
||||
api_lines.append(' emitEvent: emitEvent,')
|
||||
api_lines.append(' onEvent: onEvent,')
|
||||
api_lines.append(' bridgeEvent: bridgeEvent,')
|
||||
api_lines.append(' makeSpread: makeSpread,')
|
||||
api_lines.append(' isSpread: isSpread,')
|
||||
api_lines.append(' spreadAttrs: spreadAttrs,')
|
||||
api_lines.append(' collect: sxCollect,')
|
||||
api_lines.append(' collected: sxCollected,')
|
||||
api_lines.append(' clearCollected: sxClearCollected,')
|
||||
api_lines.append(' providePush: providePush,')
|
||||
api_lines.append(' providePop: providePop,')
|
||||
api_lines.append(' context: sxContext,')
|
||||
api_lines.append(' emit: sxEmit,')
|
||||
api_lines.append(' emitted: sxEmitted,')
|
||||
api_lines.append(f' _version: "{version}"')
|
||||
api_lines.append(' };')
|
||||
api_lines.append('')
|
||||
|
||||
@@ -84,6 +84,63 @@ class _RawHTML:
|
||||
self.html = html
|
||||
|
||||
|
||||
class _Spread:
|
||||
"""Attribute injection value — merges attrs onto parent element."""
|
||||
__slots__ = ("attrs",)
|
||||
def __init__(self, attrs: dict):
|
||||
self.attrs = dict(attrs) if attrs else {}
|
||||
|
||||
|
||||
# Render-time accumulator buckets (per render pass)
|
||||
_collect_buckets: dict[str, list] = {}
|
||||
|
||||
|
||||
def _collect_reset():
|
||||
"""Reset all collect buckets (call at start of each render pass)."""
|
||||
global _collect_buckets
|
||||
_collect_buckets = {}
|
||||
|
||||
|
||||
# Render-time dynamic scope stacks (provide/context/emit!)
|
||||
_provide_stacks: dict[str, list[dict]] = {}
|
||||
|
||||
|
||||
def provide_push(name, value=None):
|
||||
"""Push a provider scope with name, value, and empty emitted list."""
|
||||
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []})
|
||||
|
||||
|
||||
def provide_pop(name):
|
||||
"""Pop the most recent provider scope for name."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name].pop()
|
||||
|
||||
|
||||
def sx_context(name, *default):
|
||||
"""Read value from nearest enclosing provider. Error if no provider and no default."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return _provide_stacks[name][-1]["value"]
|
||||
if default:
|
||||
return default[0]
|
||||
raise RuntimeError(f"No provider for: {name}")
|
||||
|
||||
|
||||
def sx_emit(name, value):
|
||||
"""Append value to nearest enclosing provider's accumulator. Error if no provider."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name][-1]["emitted"].append(value)
|
||||
else:
|
||||
raise RuntimeError(f"No provider for emit!: {name}")
|
||||
return NIL
|
||||
|
||||
|
||||
def sx_emitted(name):
|
||||
"""Return list of values emitted into nearest matching provider."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return list(_provide_stacks[name][-1]["emitted"])
|
||||
return []
|
||||
|
||||
|
||||
def sx_truthy(x):
|
||||
"""SX truthiness: everything is truthy except False, None, and NIL."""
|
||||
if x is False:
|
||||
@@ -167,6 +224,8 @@ def type_of(x):
|
||||
return "island"
|
||||
if isinstance(x, _Signal):
|
||||
return "signal"
|
||||
if isinstance(x, _Spread):
|
||||
return "spread"
|
||||
if isinstance(x, Macro):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
@@ -270,6 +329,38 @@ def make_thunk(expr, env):
|
||||
return _Thunk(expr, env)
|
||||
|
||||
|
||||
def make_spread(attrs):
|
||||
return _Spread(attrs if isinstance(attrs, dict) else {})
|
||||
|
||||
|
||||
def is_spread(x):
|
||||
return isinstance(x, _Spread)
|
||||
|
||||
|
||||
def spread_attrs(s):
|
||||
return s.attrs if isinstance(s, _Spread) else {}
|
||||
|
||||
|
||||
def sx_collect(bucket, value):
|
||||
"""Add value to named render-time accumulator (deduplicated)."""
|
||||
if bucket not in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
items = _collect_buckets[bucket]
|
||||
if value not in items:
|
||||
items.append(value)
|
||||
|
||||
|
||||
def sx_collected(bucket):
|
||||
"""Return all values in named render-time accumulator."""
|
||||
return list(_collect_buckets.get(bucket, []))
|
||||
|
||||
|
||||
def sx_clear_collected(bucket):
|
||||
"""Clear a named render-time accumulator bucket."""
|
||||
if bucket in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
|
||||
|
||||
def lambda_params(f):
|
||||
return f.params
|
||||
|
||||
@@ -881,6 +972,22 @@ def _strip_tags(s):
|
||||
"stdlib.debug": '''
|
||||
# stdlib.debug
|
||||
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
|
||||
''',
|
||||
|
||||
"stdlib.spread": '''
|
||||
# stdlib.spread — spread + collect primitives
|
||||
PRIMITIVES["make-spread"] = make_spread
|
||||
PRIMITIVES["spread?"] = is_spread
|
||||
PRIMITIVES["spread-attrs"] = spread_attrs
|
||||
PRIMITIVES["collect!"] = sx_collect
|
||||
PRIMITIVES["collected"] = sx_collected
|
||||
PRIMITIVES["clear-collected!"] = sx_clear_collected
|
||||
# provide/context/emit! — render-time dynamic scope
|
||||
PRIMITIVES["provide-push!"] = provide_push
|
||||
PRIMITIVES["provide-pop!"] = provide_pop
|
||||
PRIMITIVES["context"] = sx_context
|
||||
PRIMITIVES["emit!"] = sx_emit
|
||||
PRIMITIVES["emitted"] = sx_emitted
|
||||
''',
|
||||
}
|
||||
|
||||
|
||||
@@ -245,6 +245,18 @@
|
||||
"match-route-segments" "match_route_segments"
|
||||
"match-route" "match_route"
|
||||
"find-matching-route" "find_matching_route"
|
||||
"make-spread" "make_spread"
|
||||
"spread?" "is_spread"
|
||||
"spread-attrs" "spread_attrs"
|
||||
"merge-spread-attrs" "merge_spread_attrs"
|
||||
"collect!" "sx_collect"
|
||||
"collected" "sx_collected"
|
||||
"clear-collected!" "sx_clear_collected"
|
||||
"provide-push!" "provide_push"
|
||||
"provide-pop!" "provide_pop"
|
||||
"context" "sx_context"
|
||||
"emit!" "sx_emit"
|
||||
"emitted" "sx_emitted"
|
||||
})
|
||||
|
||||
|
||||
|
||||
@@ -213,6 +213,43 @@
|
||||
(= (type-of (nth expr 1)) "keyword")))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Spread — attribute injection from children into parent elements
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; A spread value is a dict of attributes that, when returned as a child
|
||||
;; of an HTML element, merges its attrs onto the parent element.
|
||||
;; This enables components to inject classes/styles/data-attrs onto their
|
||||
;; parent without the parent knowing about the specific attrs.
|
||||
;;
|
||||
;; merge-spread-attrs: merge a spread's attrs into an element's attrs dict.
|
||||
;; Class values are joined (space-separated); others overwrite.
|
||||
;; Mutates the target attrs dict in place.
|
||||
|
||||
(define merge-spread-attrs :effects [mutation]
|
||||
(fn ((target :as dict) (spread-dict :as dict))
|
||||
(for-each
|
||||
(fn ((key :as string))
|
||||
(let ((val (dict-get spread-dict key)))
|
||||
(if (= key "class")
|
||||
;; Class: join existing + new with space
|
||||
(let ((existing (dict-get target "class")))
|
||||
(dict-set! target "class"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing " " val)
|
||||
val)))
|
||||
;; Style: join with semicolons
|
||||
(if (= key "style")
|
||||
(let ((existing (dict-get target "style")))
|
||||
(dict-set! target "style"
|
||||
(if (and existing (not (= existing "")))
|
||||
(str existing ";" val)
|
||||
val)))
|
||||
;; Everything else: overwrite
|
||||
(dict-set! target key val)))))
|
||||
(keys spread-dict))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface (shared across adapters)
|
||||
;; --------------------------------------------------------------------------
|
||||
@@ -222,6 +259,23 @@
|
||||
;; (escape-attr s) → attribute-value-escaped string
|
||||
;; (raw-html-content r) → unwrap RawHTML marker to string
|
||||
;;
|
||||
;; Spread (render-time attribute injection):
|
||||
;; (make-spread attrs) → Spread value
|
||||
;; (spread? x) → boolean
|
||||
;; (spread-attrs s) → dict
|
||||
;;
|
||||
;; Render-time accumulators:
|
||||
;; (collect! bucket value) → void
|
||||
;; (collected bucket) → list
|
||||
;; (clear-collected! bucket) → void
|
||||
;;
|
||||
;; Dynamic scope (provide/context/emit!):
|
||||
;; (provide-push! name val) → void
|
||||
;; (provide-pop! name) → void
|
||||
;; (context name &rest def) → value from nearest provider
|
||||
;; (emit! name value) → void (append to provider accumulator)
|
||||
;; (emitted name) → list of emitted values
|
||||
;;
|
||||
;; From parser.sx:
|
||||
;; (sx-serialize val) → SX source string (aliased as serialize above)
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
@@ -43,6 +43,63 @@ class _RawHTML:
|
||||
self.html = html
|
||||
|
||||
|
||||
class _Spread:
|
||||
"""Attribute injection value — merges attrs onto parent element."""
|
||||
__slots__ = ("attrs",)
|
||||
def __init__(self, attrs: dict):
|
||||
self.attrs = dict(attrs) if attrs else {}
|
||||
|
||||
|
||||
# Render-time accumulator buckets (per render pass)
|
||||
_collect_buckets: dict[str, list] = {}
|
||||
|
||||
|
||||
def _collect_reset():
|
||||
"""Reset all collect buckets (call at start of each render pass)."""
|
||||
global _collect_buckets
|
||||
_collect_buckets = {}
|
||||
|
||||
|
||||
# Render-time dynamic scope stacks (provide/context/emit!)
|
||||
_provide_stacks: dict[str, list[dict]] = {}
|
||||
|
||||
|
||||
def provide_push(name, value=None):
|
||||
"""Push a provider scope with name, value, and empty emitted list."""
|
||||
_provide_stacks.setdefault(name, []).append({"value": value, "emitted": []})
|
||||
|
||||
|
||||
def provide_pop(name):
|
||||
"""Pop the most recent provider scope for name."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name].pop()
|
||||
|
||||
|
||||
def sx_context(name, *default):
|
||||
"""Read value from nearest enclosing provider. Error if no provider and no default."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return _provide_stacks[name][-1]["value"]
|
||||
if default:
|
||||
return default[0]
|
||||
raise RuntimeError(f"No provider for: {name}")
|
||||
|
||||
|
||||
def sx_emit(name, value):
|
||||
"""Append value to nearest enclosing provider's accumulator. Error if no provider."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
_provide_stacks[name][-1]["emitted"].append(value)
|
||||
else:
|
||||
raise RuntimeError(f"No provider for emit!: {name}")
|
||||
return NIL
|
||||
|
||||
|
||||
def sx_emitted(name):
|
||||
"""Return list of values emitted into nearest matching provider."""
|
||||
if name in _provide_stacks and _provide_stacks[name]:
|
||||
return list(_provide_stacks[name][-1]["emitted"])
|
||||
return []
|
||||
|
||||
|
||||
def sx_truthy(x):
|
||||
"""SX truthiness: everything is truthy except False, None, and NIL."""
|
||||
if x is False:
|
||||
@@ -126,6 +183,8 @@ def type_of(x):
|
||||
return "island"
|
||||
if isinstance(x, _Signal):
|
||||
return "signal"
|
||||
if isinstance(x, _Spread):
|
||||
return "spread"
|
||||
if isinstance(x, Macro):
|
||||
return "macro"
|
||||
if isinstance(x, _RawHTML):
|
||||
@@ -229,6 +288,38 @@ def make_thunk(expr, env):
|
||||
return _Thunk(expr, env)
|
||||
|
||||
|
||||
def make_spread(attrs):
|
||||
return _Spread(attrs if isinstance(attrs, dict) else {})
|
||||
|
||||
|
||||
def is_spread(x):
|
||||
return isinstance(x, _Spread)
|
||||
|
||||
|
||||
def spread_attrs(s):
|
||||
return s.attrs if isinstance(s, _Spread) else {}
|
||||
|
||||
|
||||
def sx_collect(bucket, value):
|
||||
"""Add value to named render-time accumulator (deduplicated)."""
|
||||
if bucket not in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
items = _collect_buckets[bucket]
|
||||
if value not in items:
|
||||
items.append(value)
|
||||
|
||||
|
||||
def sx_collected(bucket):
|
||||
"""Return all values in named render-time accumulator."""
|
||||
return list(_collect_buckets.get(bucket, []))
|
||||
|
||||
|
||||
def sx_clear_collected(bucket):
|
||||
"""Clear a named render-time accumulator bucket."""
|
||||
if bucket in _collect_buckets:
|
||||
_collect_buckets[bucket] = []
|
||||
|
||||
|
||||
def lambda_params(f):
|
||||
return f.params
|
||||
|
||||
@@ -847,6 +938,21 @@ def _strip_tags(s):
|
||||
PRIMITIVES["assert"] = lambda cond, msg="Assertion failed": (_ for _ in ()).throw(RuntimeError(f"Assertion error: {msg}")) if not sx_truthy(cond) else True
|
||||
|
||||
|
||||
# stdlib.spread — spread + collect primitives
|
||||
PRIMITIVES["make-spread"] = make_spread
|
||||
PRIMITIVES["spread?"] = is_spread
|
||||
PRIMITIVES["spread-attrs"] = spread_attrs
|
||||
PRIMITIVES["collect!"] = sx_collect
|
||||
PRIMITIVES["collected"] = sx_collected
|
||||
PRIMITIVES["clear-collected!"] = sx_clear_collected
|
||||
# provide/context/emit! — render-time dynamic scope
|
||||
PRIMITIVES["provide-push!"] = provide_push
|
||||
PRIMITIVES["provide-pop!"] = provide_pop
|
||||
PRIMITIVES["context"] = sx_context
|
||||
PRIMITIVES["emit!"] = sx_emit
|
||||
PRIMITIVES["emitted"] = sx_emitted
|
||||
|
||||
|
||||
def is_primitive(name):
|
||||
if name in PRIMITIVES:
|
||||
return True
|
||||
@@ -1289,6 +1395,8 @@ def eval_list(expr, env):
|
||||
return sf_shift(args, env)
|
||||
elif sx_truthy((name == 'dynamic-wind')):
|
||||
return sf_dynamic_wind(args, env)
|
||||
elif sx_truthy((name == 'provide')):
|
||||
return sf_provide(args, env)
|
||||
elif sx_truthy((name == 'map')):
|
||||
return ho_map(args, env)
|
||||
elif sx_truthy((name == 'map-indexed')):
|
||||
@@ -1780,6 +1888,19 @@ def sf_dynamic_wind(args, env):
|
||||
call_thunk(after, env)
|
||||
return result
|
||||
|
||||
# sf-provide
|
||||
def sf_provide(args, env):
|
||||
_cells = {}
|
||||
name = trampoline(eval_expr(first(args), env))
|
||||
val = trampoline(eval_expr(nth(args, 1), env))
|
||||
body_exprs = slice(args, 2)
|
||||
_cells['result'] = NIL
|
||||
provide_push(name, val)
|
||||
for e in body_exprs:
|
||||
_cells['result'] = trampoline(eval_expr(e, env))
|
||||
provide_pop(name)
|
||||
return _cells['result']
|
||||
|
||||
# expand-macro
|
||||
def expand_macro(mac, raw_args, env):
|
||||
local = env_merge(macro_closure(mac), env)
|
||||
@@ -2056,6 +2177,21 @@ def is_render_expr(expr):
|
||||
n = symbol_name(h)
|
||||
return ((n == '<>') if sx_truthy((n == '<>')) else ((n == 'raw!') if sx_truthy((n == 'raw!')) else (starts_with_p(n, '~') if sx_truthy(starts_with_p(n, '~')) else (starts_with_p(n, 'html:') if sx_truthy(starts_with_p(n, 'html:')) else (contains_p(HTML_TAGS, n) if sx_truthy(contains_p(HTML_TAGS, n)) else ((index_of(n, '-') > 0) if not sx_truthy((index_of(n, '-') > 0)) else ((len(expr) > 1) if not sx_truthy((len(expr) > 1)) else (type_of(nth(expr, 1)) == 'keyword'))))))))
|
||||
|
||||
# merge-spread-attrs
|
||||
def merge_spread_attrs(target, spread_dict):
|
||||
for key in keys(spread_dict):
|
||||
val = dict_get(spread_dict, key)
|
||||
if sx_truthy((key == 'class')):
|
||||
existing = dict_get(target, 'class')
|
||||
target['class'] = (sx_str(existing, ' ', val) if sx_truthy((existing if not sx_truthy(existing) else (not sx_truthy((existing == ''))))) else val)
|
||||
else:
|
||||
if sx_truthy((key == 'style')):
|
||||
existing = dict_get(target, 'style')
|
||||
target['style'] = (sx_str(existing, ';', val) if sx_truthy((existing if not sx_truthy(existing) else (not sx_truthy((existing == ''))))) else val)
|
||||
else:
|
||||
target[key] = val
|
||||
return NIL
|
||||
|
||||
|
||||
# === Transpiled from adapter-html ===
|
||||
|
||||
@@ -2085,6 +2221,8 @@ def render_to_html(expr, env):
|
||||
return escape_html(keyword_name(expr))
|
||||
elif _match == 'raw-html':
|
||||
return raw_html_content(expr)
|
||||
elif _match == 'spread':
|
||||
return expr
|
||||
else:
|
||||
return render_value_to_html(trampoline(eval_expr(expr, env)), env)
|
||||
|
||||
@@ -2106,11 +2244,13 @@ def render_value_to_html(val, env):
|
||||
return render_list_to_html(val, env)
|
||||
elif _match == 'raw-html':
|
||||
return raw_html_content(val)
|
||||
elif _match == 'spread':
|
||||
return val
|
||||
else:
|
||||
return escape_html(sx_str(val))
|
||||
|
||||
# RENDER_HTML_FORMS
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
RENDER_HTML_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each', 'provide']
|
||||
|
||||
# render-html-form?
|
||||
def is_render_html_form(name):
|
||||
@@ -2123,12 +2263,12 @@ def render_list_to_html(expr, env):
|
||||
else:
|
||||
head = first(expr)
|
||||
if sx_truthy((not sx_truthy((type_of(head) == 'symbol')))):
|
||||
return join('', map(lambda x: render_value_to_html(x, env), expr))
|
||||
return join('', filter(lambda x: (not sx_truthy(is_spread(x))), map(lambda x: render_value_to_html(x, env), expr)))
|
||||
else:
|
||||
name = symbol_name(head)
|
||||
args = rest(expr)
|
||||
if sx_truthy((name == '<>')):
|
||||
return join('', map(lambda x: render_to_html(x, env), args))
|
||||
return join('', filter(lambda x: (not sx_truthy(is_spread(x))), map(lambda x: render_to_html(x, env), args)))
|
||||
elif sx_truthy((name == 'raw!')):
|
||||
return join('', map(lambda x: sx_str(trampoline(eval_expr(x, env))), args))
|
||||
elif sx_truthy((name == 'lake')):
|
||||
@@ -2169,7 +2309,11 @@ def dispatch_html_form(name, expr, env):
|
||||
if sx_truthy((not sx_truthy(trampoline(eval_expr(nth(expr, 1), env))))):
|
||||
return ''
|
||||
else:
|
||||
return join('', map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr))))
|
||||
if sx_truthy((len(expr) == 3)):
|
||||
return render_to_html(nth(expr, 2), env)
|
||||
else:
|
||||
results = map(lambda i: render_to_html(nth(expr, i), env), range(2, len(expr)))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
|
||||
elif sx_truthy((name == 'cond')):
|
||||
branch = eval_cond(rest(expr), env)
|
||||
if sx_truthy(branch):
|
||||
@@ -2180,26 +2324,43 @@ def dispatch_html_form(name, expr, env):
|
||||
return render_to_html(trampoline(eval_expr(expr, env)), env)
|
||||
elif sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))):
|
||||
local = process_bindings(nth(expr, 1), env)
|
||||
return join('', map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr))))
|
||||
if sx_truthy((len(expr) == 3)):
|
||||
return render_to_html(nth(expr, 2), local)
|
||||
else:
|
||||
results = map(lambda i: render_to_html(nth(expr, i), local), range(2, len(expr)))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
|
||||
elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))):
|
||||
return join('', map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr))))
|
||||
if sx_truthy((len(expr) == 2)):
|
||||
return render_to_html(nth(expr, 1), env)
|
||||
else:
|
||||
results = map(lambda i: render_to_html(nth(expr, i), env), range(1, len(expr)))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
|
||||
elif sx_truthy(is_definition_form(name)):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return ''
|
||||
elif sx_truthy((name == 'map')):
|
||||
f = trampoline(eval_expr(nth(expr, 1), env))
|
||||
coll = trampoline(eval_expr(nth(expr, 2), env))
|
||||
return join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)))
|
||||
elif sx_truthy((name == 'map-indexed')):
|
||||
f = trampoline(eval_expr(nth(expr, 1), env))
|
||||
coll = trampoline(eval_expr(nth(expr, 2), env))
|
||||
return join('', map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), map_indexed(lambda i, item: (render_lambda_html(f, [i, item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [i, item]), env)), coll)))
|
||||
elif sx_truthy((name == 'filter')):
|
||||
return render_to_html(trampoline(eval_expr(expr, env)), env)
|
||||
elif sx_truthy((name == 'for-each')):
|
||||
f = trampoline(eval_expr(nth(expr, 1), env))
|
||||
coll = trampoline(eval_expr(nth(expr, 2), env))
|
||||
return join('', map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), map(lambda item: (render_lambda_html(f, [item], env) if sx_truthy(is_lambda(f)) else render_to_html(apply(f, [item]), env)), coll)))
|
||||
elif sx_truthy((name == 'provide')):
|
||||
prov_name = trampoline(eval_expr(nth(expr, 1), env))
|
||||
prov_val = trampoline(eval_expr(nth(expr, 2), env))
|
||||
body_start = 3
|
||||
body_count = (len(expr) - 3)
|
||||
provide_push(prov_name, prov_val)
|
||||
result = (render_to_html(nth(expr, body_start), env) if sx_truthy((body_count == 1)) else join('', filter(lambda r: (not sx_truthy(is_spread(r))), map(lambda i: render_to_html(nth(expr, i), env), range(body_start, (body_start + body_count))))))
|
||||
provide_pop(prov_name)
|
||||
return result
|
||||
else:
|
||||
return render_value_to_html(trampoline(eval_expr(expr, env)), env)
|
||||
|
||||
@@ -2218,7 +2379,12 @@ def render_html_component(comp, args, env):
|
||||
for p in component_params(comp):
|
||||
local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
|
||||
if sx_truthy(component_has_children(comp)):
|
||||
local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children)))
|
||||
parts = []
|
||||
for c in children:
|
||||
r = render_to_html(c, env)
|
||||
if sx_truthy((not sx_truthy(is_spread(r)))):
|
||||
parts.append(r)
|
||||
local['children'] = make_raw_html(join('', parts))
|
||||
return render_to_html(component_body(comp), local)
|
||||
|
||||
# render-html-element
|
||||
@@ -2227,7 +2393,17 @@ def render_html_element(tag, args, env):
|
||||
attrs = first(parsed)
|
||||
children = nth(parsed, 1)
|
||||
is_void = contains_p(VOID_ELEMENTS, tag)
|
||||
return sx_str('<', tag, render_attrs(attrs), (' />' if sx_truthy(is_void) else sx_str('>', join('', map(lambda c: render_to_html(c, env), children)), '</', tag, '>')))
|
||||
if sx_truthy(is_void):
|
||||
return sx_str('<', tag, render_attrs(attrs), ' />')
|
||||
else:
|
||||
content_parts = []
|
||||
for c in children:
|
||||
result = render_to_html(c, env)
|
||||
if sx_truthy(is_spread(result)):
|
||||
merge_spread_attrs(attrs, spread_attrs(result))
|
||||
else:
|
||||
content_parts.append(result)
|
||||
return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>')
|
||||
|
||||
# render-html-lake
|
||||
def render_html_lake(args, env):
|
||||
@@ -2236,7 +2412,15 @@ def render_html_lake(args, env):
|
||||
_cells['lake_tag'] = 'div'
|
||||
children = []
|
||||
reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda kname: (lambda kval: _sx_begin((_sx_cell_set(_cells, 'lake_id', kval) if sx_truthy((kname == 'id')) else (_sx_cell_set(_cells, 'lake_tag', kval) if sx_truthy((kname == 'tag')) else NIL)), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))))(keyword_name(arg)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args)
|
||||
return sx_str('<', _cells['lake_tag'], ' data-sx-lake="', escape_attr((_cells['lake_id'] if sx_truthy(_cells['lake_id']) else '')), '">', join('', map(lambda c: render_to_html(c, env), children)), '</', _cells['lake_tag'], '>')
|
||||
lake_attrs = {'data-sx-lake': (_cells['lake_id'] if sx_truthy(_cells['lake_id']) else '')}
|
||||
content_parts = []
|
||||
for c in children:
|
||||
result = render_to_html(c, env)
|
||||
if sx_truthy(is_spread(result)):
|
||||
merge_spread_attrs(lake_attrs, spread_attrs(result))
|
||||
else:
|
||||
content_parts.append(result)
|
||||
return sx_str('<', _cells['lake_tag'], render_attrs(lake_attrs), '>', join('', content_parts), '</', _cells['lake_tag'], '>')
|
||||
|
||||
# render-html-marsh
|
||||
def render_html_marsh(args, env):
|
||||
@@ -2245,7 +2429,15 @@ def render_html_marsh(args, env):
|
||||
_cells['marsh_tag'] = 'div'
|
||||
children = []
|
||||
reduce(lambda state, arg: (lambda skip: (assoc(state, 'skip', False, 'i', (get(state, 'i') + 1)) if sx_truthy(skip) else ((lambda kname: (lambda kval: _sx_begin((_sx_cell_set(_cells, 'marsh_id', kval) if sx_truthy((kname == 'id')) else (_sx_cell_set(_cells, 'marsh_tag', kval) if sx_truthy((kname == 'tag')) else (NIL if sx_truthy((kname == 'transform')) else NIL))), assoc(state, 'skip', True, 'i', (get(state, 'i') + 1))))(trampoline(eval_expr(nth(args, (get(state, 'i') + 1)), env))))(keyword_name(arg)) if sx_truthy(((type_of(arg) == 'keyword') if not sx_truthy((type_of(arg) == 'keyword')) else ((get(state, 'i') + 1) < len(args)))) else _sx_begin(_sx_append(children, arg), assoc(state, 'i', (get(state, 'i') + 1))))))(get(state, 'skip')), {'i': 0, 'skip': False}, args)
|
||||
return sx_str('<', _cells['marsh_tag'], ' data-sx-marsh="', escape_attr((_cells['marsh_id'] if sx_truthy(_cells['marsh_id']) else '')), '">', join('', map(lambda c: render_to_html(c, env), children)), '</', _cells['marsh_tag'], '>')
|
||||
marsh_attrs = {'data-sx-marsh': (_cells['marsh_id'] if sx_truthy(_cells['marsh_id']) else '')}
|
||||
content_parts = []
|
||||
for c in children:
|
||||
result = render_to_html(c, env)
|
||||
if sx_truthy(is_spread(result)):
|
||||
merge_spread_attrs(marsh_attrs, spread_attrs(result))
|
||||
else:
|
||||
content_parts.append(result)
|
||||
return sx_str('<', _cells['marsh_tag'], render_attrs(marsh_attrs), '>', join('', content_parts), '</', _cells['marsh_tag'], '>')
|
||||
|
||||
# render-html-island
|
||||
def render_html_island(island, args, env):
|
||||
@@ -2257,7 +2449,12 @@ def render_html_island(island, args, env):
|
||||
for p in component_params(island):
|
||||
local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
|
||||
if sx_truthy(component_has_children(island)):
|
||||
local['children'] = make_raw_html(join('', map(lambda c: render_to_html(c, env), children)))
|
||||
parts = []
|
||||
for c in children:
|
||||
r = render_to_html(c, env)
|
||||
if sx_truthy((not sx_truthy(is_spread(r)))):
|
||||
parts.append(r)
|
||||
local['children'] = make_raw_html(join('', parts))
|
||||
body_html = render_to_html(component_body(island), local)
|
||||
state_sx = serialize_island_state(kwargs)
|
||||
return sx_str('<span data-sx-island="', escape_attr(island_name), '"', (sx_str(' data-sx-state="', escape_attr(state_sx), '"') if sx_truthy(state_sx) else ''), '>', body_html, '</span>')
|
||||
@@ -2313,6 +2510,8 @@ def aser(expr, env):
|
||||
return []
|
||||
else:
|
||||
return aser_list(expr, env)
|
||||
elif _match == 'spread':
|
||||
return expr
|
||||
else:
|
||||
return expr
|
||||
|
||||
@@ -2400,7 +2599,7 @@ def aser_call(name, args, env):
|
||||
return sx_str('(', join(' ', parts), ')')
|
||||
|
||||
# SPECIAL_FORM_NAMES
|
||||
SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland', 'deftype', 'defeffect']
|
||||
SPECIAL_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'defrelation', 'begin', 'do', 'quote', 'quasiquote', '->', 'set!', 'letrec', 'dynamic-wind', 'defisland', 'deftype', 'defeffect', 'provide']
|
||||
|
||||
# HO_FORM_NAMES
|
||||
HO_FORM_NAMES = ['map', 'map-indexed', 'filter', 'reduce', 'some', 'every?', 'for-each']
|
||||
@@ -2497,6 +2696,15 @@ def aser_special(name, expr, env):
|
||||
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'defrelation') if sx_truthy((name == 'defrelation')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect')))))))))))):
|
||||
trampoline(eval_expr(expr, env))
|
||||
return NIL
|
||||
elif sx_truthy((name == 'provide')):
|
||||
prov_name = trampoline(eval_expr(first(args), env))
|
||||
prov_val = trampoline(eval_expr(nth(args, 1), env))
|
||||
_cells['result'] = NIL
|
||||
provide_push(prov_name, prov_val)
|
||||
for body in slice(args, 2):
|
||||
_cells['result'] = aser(body, env)
|
||||
provide_pop(prov_name)
|
||||
return _cells['result']
|
||||
else:
|
||||
return trampoline(eval_expr(expr, env))
|
||||
|
||||
@@ -3441,6 +3649,8 @@ async def async_render(expr, env, ctx):
|
||||
return escape_html(sx_str(expr))
|
||||
elif _match == 'raw-html':
|
||||
return raw_html_content(expr)
|
||||
elif _match == 'spread':
|
||||
return expr
|
||||
elif _match == 'symbol':
|
||||
val = (await async_eval(expr, env, ctx))
|
||||
return (await async_render(val, env, ctx))
|
||||
@@ -3472,7 +3682,7 @@ async def async_render_list(expr, env, ctx):
|
||||
elif sx_truthy((name == 'raw!')):
|
||||
return (await async_render_raw(args, env, ctx))
|
||||
elif sx_truthy((name == '<>')):
|
||||
return join('', (await async_map_render(args, env, ctx)))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_render(args, env, ctx))))
|
||||
elif sx_truthy(starts_with_p(name, 'html:')):
|
||||
return (await async_render_element(slice(name, 5), args, env, ctx))
|
||||
elif sx_truthy(async_render_form_p(name)):
|
||||
@@ -3522,15 +3732,20 @@ async def async_render_element(tag, args, env, ctx):
|
||||
class_val = dict_get(attrs, 'class')
|
||||
if sx_truthy(((not sx_truthy(is_nil(class_val))) if not sx_truthy((not sx_truthy(is_nil(class_val)))) else (not sx_truthy((class_val == False))))):
|
||||
css_class_collect(sx_str(class_val))
|
||||
opening = sx_str('<', tag, render_attrs(attrs), '>')
|
||||
if sx_truthy(contains_p(VOID_ELEMENTS, tag)):
|
||||
return opening
|
||||
return sx_str('<', tag, render_attrs(attrs), '>')
|
||||
else:
|
||||
token = (svg_context_set(True) if sx_truthy(((tag == 'svg') if sx_truthy((tag == 'svg')) else (tag == 'math'))) else NIL)
|
||||
child_html = join('', (await async_map_render(children, env, ctx)))
|
||||
content_parts = []
|
||||
for c in children:
|
||||
result = (await async_render(c, env, ctx))
|
||||
if sx_truthy(is_spread(result)):
|
||||
merge_spread_attrs(attrs, spread_attrs(result))
|
||||
else:
|
||||
content_parts.append(result)
|
||||
if sx_truthy(token):
|
||||
svg_context_reset(token)
|
||||
return sx_str(opening, child_html, '</', tag, '>')
|
||||
return sx_str('<', tag, render_attrs(attrs), '>', join('', content_parts), '</', tag, '>')
|
||||
|
||||
# async-parse-element-args
|
||||
async def async_parse_element_args(args, attrs, children, env, ctx):
|
||||
@@ -3561,7 +3776,12 @@ async def async_render_component(comp, args, env, ctx):
|
||||
for p in component_params(comp):
|
||||
local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
|
||||
if sx_truthy(component_has_children(comp)):
|
||||
local['children'] = make_raw_html(join('', (await async_map_render(children, env, ctx))))
|
||||
parts = []
|
||||
for c in children:
|
||||
r = (await async_render(c, env, ctx))
|
||||
if sx_truthy((not sx_truthy(is_spread(r)))):
|
||||
parts.append(r)
|
||||
local['children'] = make_raw_html(join('', parts))
|
||||
return (await async_render(component_body(comp), local, ctx))
|
||||
|
||||
# async-render-island
|
||||
@@ -3574,7 +3794,12 @@ async def async_render_island(island, args, env, ctx):
|
||||
for p in component_params(island):
|
||||
local[p] = (dict_get(kwargs, p) if sx_truthy(dict_has(kwargs, p)) else NIL)
|
||||
if sx_truthy(component_has_children(island)):
|
||||
local['children'] = make_raw_html(join('', (await async_map_render(children, env, ctx))))
|
||||
parts = []
|
||||
for c in children:
|
||||
r = (await async_render(c, env, ctx))
|
||||
if sx_truthy((not sx_truthy(is_spread(r)))):
|
||||
parts.append(r)
|
||||
local['children'] = make_raw_html(join('', parts))
|
||||
body_html = (await async_render(component_body(island), local, ctx))
|
||||
state_json = serialize_island_state(kwargs)
|
||||
return sx_str('<span data-sx-island="', escape_attr(island_name), '"', (sx_str(' data-sx-state="', escape_attr(state_json), '"') if sx_truthy(state_json) else ''), '>', body_html, '</span>')
|
||||
@@ -3613,7 +3838,7 @@ async def async_map_render(exprs, env, ctx):
|
||||
return results
|
||||
|
||||
# ASYNC_RENDER_FORMS
|
||||
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each']
|
||||
ASYNC_RENDER_FORMS = ['if', 'when', 'cond', 'case', 'let', 'let*', 'begin', 'do', 'define', 'defcomp', 'defisland', 'defmacro', 'defstyle', 'defhandler', 'deftype', 'defeffect', 'map', 'map-indexed', 'filter', 'for-each', 'provide']
|
||||
|
||||
# async-render-form?
|
||||
def async_render_form_p(name):
|
||||
@@ -3634,7 +3859,11 @@ async def dispatch_async_render_form(name, expr, env, ctx):
|
||||
if sx_truthy((not sx_truthy((await async_eval(nth(expr, 1), env, ctx))))):
|
||||
return ''
|
||||
else:
|
||||
return join('', (await async_map_render(slice(expr, 2), env, ctx)))
|
||||
if sx_truthy((len(expr) == 3)):
|
||||
return (await async_render(nth(expr, 2), env, ctx))
|
||||
else:
|
||||
results = (await async_map_render(slice(expr, 2), env, ctx))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
|
||||
elif sx_truthy((name == 'cond')):
|
||||
clauses = rest(expr)
|
||||
if sx_truthy(cond_scheme_p(clauses)):
|
||||
@@ -3645,26 +3874,43 @@ async def dispatch_async_render_form(name, expr, env, ctx):
|
||||
return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
|
||||
elif sx_truthy(((name == 'let') if sx_truthy((name == 'let')) else (name == 'let*'))):
|
||||
local = (await async_process_bindings(nth(expr, 1), env, ctx))
|
||||
return join('', (await async_map_render(slice(expr, 2), local, ctx)))
|
||||
if sx_truthy((len(expr) == 3)):
|
||||
return (await async_render(nth(expr, 2), local, ctx))
|
||||
else:
|
||||
results = (await async_map_render(slice(expr, 2), local, ctx))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
|
||||
elif sx_truthy(((name == 'begin') if sx_truthy((name == 'begin')) else (name == 'do'))):
|
||||
return join('', (await async_map_render(rest(expr), env, ctx)))
|
||||
if sx_truthy((len(expr) == 2)):
|
||||
return (await async_render(nth(expr, 1), env, ctx))
|
||||
else:
|
||||
results = (await async_map_render(rest(expr), env, ctx))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), results))
|
||||
elif sx_truthy(is_definition_form(name)):
|
||||
(await async_eval(expr, env, ctx))
|
||||
return ''
|
||||
elif sx_truthy((name == 'map')):
|
||||
f = (await async_eval(nth(expr, 1), env, ctx))
|
||||
coll = (await async_eval(nth(expr, 2), env, ctx))
|
||||
return join('', (await async_map_fn_render(f, coll, env, ctx)))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_fn_render(f, coll, env, ctx))))
|
||||
elif sx_truthy((name == 'map-indexed')):
|
||||
f = (await async_eval(nth(expr, 1), env, ctx))
|
||||
coll = (await async_eval(nth(expr, 2), env, ctx))
|
||||
return join('', (await async_map_indexed_fn_render(f, coll, env, ctx)))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_indexed_fn_render(f, coll, env, ctx))))
|
||||
elif sx_truthy((name == 'filter')):
|
||||
return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
|
||||
elif sx_truthy((name == 'for-each')):
|
||||
f = (await async_eval(nth(expr, 1), env, ctx))
|
||||
coll = (await async_eval(nth(expr, 2), env, ctx))
|
||||
return join('', (await async_map_fn_render(f, coll, env, ctx)))
|
||||
return join('', filter(lambda r: (not sx_truthy(is_spread(r))), (await async_map_fn_render(f, coll, env, ctx))))
|
||||
elif sx_truthy((name == 'provide')):
|
||||
prov_name = (await async_eval(nth(expr, 1), env, ctx))
|
||||
prov_val = (await async_eval(nth(expr, 2), env, ctx))
|
||||
body_start = 3
|
||||
body_count = (len(expr) - 3)
|
||||
provide_push(prov_name, prov_val)
|
||||
result = ((await async_render(nth(expr, body_start), env, ctx)) if sx_truthy((body_count == 1)) else (lambda results: join('', filter(lambda r: (not sx_truthy(is_spread(r))), results)))((await async_map_render(slice(expr, body_start), env, ctx))))
|
||||
provide_pop(prov_name)
|
||||
return result
|
||||
else:
|
||||
return (await async_render((await async_eval(expr, env, ctx)), env, ctx))
|
||||
|
||||
@@ -3791,6 +4037,8 @@ async def async_aser(expr, env, ctx):
|
||||
return keyword_name(expr)
|
||||
elif _match == 'dict':
|
||||
return (await async_aser_dict(expr, env, ctx))
|
||||
elif _match == 'spread':
|
||||
return expr
|
||||
elif _match == 'list':
|
||||
if sx_truthy(empty_p(expr)):
|
||||
return []
|
||||
@@ -3988,7 +4236,7 @@ async def async_aser_call(name, args, env, ctx):
|
||||
return make_sx_expr(sx_str('(', join(' ', parts), ')'))
|
||||
|
||||
# ASYNC_ASER_FORM_NAMES
|
||||
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect']
|
||||
ASYNC_ASER_FORM_NAMES = ['if', 'when', 'cond', 'case', 'and', 'or', 'let', 'let*', 'lambda', 'fn', 'define', 'defcomp', 'defmacro', 'defstyle', 'defhandler', 'defpage', 'defquery', 'defaction', 'begin', 'do', 'quote', '->', 'set!', 'defisland', 'deftype', 'defeffect', 'provide']
|
||||
|
||||
# ASYNC_ASER_HO_NAMES
|
||||
ASYNC_ASER_HO_NAMES = ['map', 'map-indexed', 'filter', 'for-each']
|
||||
@@ -4082,6 +4330,15 @@ async def dispatch_async_aser_form(name, expr, env, ctx):
|
||||
elif sx_truthy(((name == 'define') if sx_truthy((name == 'define')) else ((name == 'defcomp') if sx_truthy((name == 'defcomp')) else ((name == 'defmacro') if sx_truthy((name == 'defmacro')) else ((name == 'defstyle') if sx_truthy((name == 'defstyle')) else ((name == 'defhandler') if sx_truthy((name == 'defhandler')) else ((name == 'defpage') if sx_truthy((name == 'defpage')) else ((name == 'defquery') if sx_truthy((name == 'defquery')) else ((name == 'defaction') if sx_truthy((name == 'defaction')) else ((name == 'deftype') if sx_truthy((name == 'deftype')) else (name == 'defeffect'))))))))))):
|
||||
(await async_eval(expr, env, ctx))
|
||||
return NIL
|
||||
elif sx_truthy((name == 'provide')):
|
||||
prov_name = (await async_eval(first(args), env, ctx))
|
||||
prov_val = (await async_eval(nth(args, 1), env, ctx))
|
||||
_cells['result'] = NIL
|
||||
provide_push(prov_name, prov_val)
|
||||
for body in slice(args, 2):
|
||||
_cells['result'] = (await async_aser(body, env, ctx))
|
||||
provide_pop(prov_name)
|
||||
return _cells['result']
|
||||
else:
|
||||
return (await async_eval(expr, env, ctx))
|
||||
|
||||
|
||||
@@ -1,68 +1,30 @@
|
||||
;; @client — send all define forms to browser for client-side use.
|
||||
;; CSSX — computed CSS from s-expressions.
|
||||
;;
|
||||
;; Generic mechanism: cssx is a macro that groups CSS property declarations.
|
||||
;; The vocabulary (property mappings, value functions) is pluggable — the
|
||||
;; Tailwind-inspired defaults below are just one possible style system.
|
||||
;; Tailwind-style utility component using spread + collect primitives.
|
||||
;; Use as a child of any element — injects classes onto the parent:
|
||||
;;
|
||||
;; Usage:
|
||||
;; (cssx (:text (colour "violet" 699) (size "4xl") (weight "bold") (family "mono"))
|
||||
;; (:bg (colour "stone" 50)))
|
||||
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; "content")
|
||||
;;
|
||||
;; Each group is (:keyword value ...modifiers):
|
||||
;; - keyword maps to a CSS property via cssx-properties dict
|
||||
;; - value is the CSS value for that property
|
||||
;; - modifiers are extra CSS declaration strings, concatenated in
|
||||
;; (button (~cssx/tw "hover:bg-rose-500 md:text-xl")
|
||||
;; "click me")
|
||||
;;
|
||||
;; Single group:
|
||||
;; (cssx (:text (colour "violet" 699)))
|
||||
;; Each token becomes a deterministic class + JIT CSS rule.
|
||||
;; Rules are collected into the "cssx" bucket, flushed once by ~cssx/flush.
|
||||
;; No wrapper elements, no per-element <style> tags.
|
||||
;;
|
||||
;; Modifiers without a colour:
|
||||
;; (cssx (:text nil (size "4xl") (weight "bold")))
|
||||
;; Reusable style variables:
|
||||
;; (define fancy (~cssx/tw "font-bold text-violet-700 text-4xl"))
|
||||
;; (div fancy "styled content")
|
||||
;;
|
||||
;; Unknown keywords pass through as raw CSS property names:
|
||||
;; (cssx (:outline (colour "red" 500))) → "outline:hsl(0,72%,53%);"
|
||||
;;
|
||||
;; Standalone modifiers work outside cssx too:
|
||||
;; :style (size "4xl")
|
||||
;; :style (str (weight "bold") (family "mono"))
|
||||
;; This is one instance of the CSSX component pattern — other styling
|
||||
;; components are possible with different vocabulary.
|
||||
|
||||
;; =========================================================================
|
||||
;; Layer 1: Generic mechanism — cssx macro + cssxgroup function
|
||||
;; Colour data — hue/saturation bases + shade-to-lightness curve
|
||||
;; =========================================================================
|
||||
|
||||
;; Property keyword → CSS property name. Extend this dict for new mappings.
|
||||
(define cssx-properties
|
||||
{"text" "color"
|
||||
"bg" "background-color"
|
||||
"border" "border-color"})
|
||||
|
||||
;; Evaluate one property group: (:text value modifier1 modifier2 ...)
|
||||
;; If value is nil, only modifiers are emitted (no property declaration).
|
||||
;; NOTE: name must NOT contain hyphens — the evaluator's isRenderExpr check
|
||||
;; treats (hyphenated-name :keyword ...) as a custom HTML element.
|
||||
(define cssxgroup
|
||||
(fn (prop value b c d e)
|
||||
(let ((css-prop (or (get cssx-properties prop) prop)))
|
||||
(str (if (nil? value) "" (str css-prop ":" value ";"))
|
||||
(or b "") (or c "") (or d "") (or e "")))))
|
||||
|
||||
;; cssx macro — takes one or more property groups, expands to (str ...).
|
||||
;; (cssx (:text val ...) (:bg val ...))
|
||||
;; → (str (cssxgroup :text val ...) (cssxgroup :bg val ...))
|
||||
(defmacro cssx (&rest groups)
|
||||
`(str ,@(map (fn (g) (cons 'cssxgroup g)) groups)))
|
||||
|
||||
;; =========================================================================
|
||||
;; Layer 2: Value vocabulary — colour, size, weight, family
|
||||
;; These are independent functions. Use inside cssx groups or standalone.
|
||||
;; Replace or extend with any style system.
|
||||
;; =========================================================================
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Colour — compute CSS colour value from name + shade
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
(define colour-bases
|
||||
{"violet" {"h" 263 "s" 70}
|
||||
"purple" {"h" 271 "s" 81}
|
||||
@@ -84,7 +46,9 @@
|
||||
"slate" {"h" 215 "s" 16}
|
||||
"gray" {"h" 220 "s" 9}
|
||||
"zinc" {"h" 240 "s" 5}
|
||||
"neutral" {"h" 0 "s" 0}})
|
||||
"neutral" {"h" 0 "s" 0}
|
||||
"white" {"h" 0 "s" 0}
|
||||
"black" {"h" 0 "s" 0}})
|
||||
|
||||
(define lerp (fn (a b t) (+ a (* t (- b a)))))
|
||||
|
||||
@@ -114,29 +78,50 @@
|
||||
(l (shade-to-lightness shade)))
|
||||
(str "hsl(" h "," s "%," (round l) "%)"))))))
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Font sizes — named size → font-size + line-height (Tailwind v3 scale)
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; =========================================================================
|
||||
;; Lookup tables — all the value vocabularies
|
||||
;; =========================================================================
|
||||
|
||||
;; Colour property shorthands → CSS property name
|
||||
(define cssx-colour-props
|
||||
{"bg" "background-color"
|
||||
"text" "color"
|
||||
"border" "border-color"})
|
||||
|
||||
;; Spacing property shorthands → CSS declaration template ({v} = computed value)
|
||||
(define cssx-spacing-props
|
||||
{"p" "padding:{v}"
|
||||
"px" "padding-left:{v};padding-right:{v}"
|
||||
"py" "padding-top:{v};padding-bottom:{v}"
|
||||
"pt" "padding-top:{v}"
|
||||
"pb" "padding-bottom:{v}"
|
||||
"pl" "padding-left:{v}"
|
||||
"pr" "padding-right:{v}"
|
||||
"m" "margin:{v}"
|
||||
"mx" "margin-left:{v};margin-right:{v}"
|
||||
"my" "margin-top:{v};margin-bottom:{v}"
|
||||
"mt" "margin-top:{v}"
|
||||
"mb" "margin-bottom:{v}"
|
||||
"ml" "margin-left:{v}"
|
||||
"mr" "margin-right:{v}"})
|
||||
|
||||
;; Named font sizes (Tailwind v3 scale)
|
||||
(define cssx-sizes
|
||||
{"xs" "font-size:0.75rem;line-height:1rem;"
|
||||
"sm" "font-size:0.875rem;line-height:1.25rem;"
|
||||
"base" "font-size:1rem;line-height:1.5rem;"
|
||||
"lg" "font-size:1.125rem;line-height:1.75rem;"
|
||||
"xl" "font-size:1.25rem;line-height:1.75rem;"
|
||||
"2xl" "font-size:1.5rem;line-height:2rem;"
|
||||
"3xl" "font-size:1.875rem;line-height:2.25rem;"
|
||||
"4xl" "font-size:2.25rem;line-height:2.5rem;"
|
||||
"5xl" "font-size:3rem;line-height:1;"
|
||||
"6xl" "font-size:3.75rem;line-height:1;"
|
||||
"7xl" "font-size:4.5rem;line-height:1;"
|
||||
"8xl" "font-size:6rem;line-height:1;"
|
||||
"9xl" "font-size:8rem;line-height:1;"})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Font weights — named weight → numeric value
|
||||
;; ---------------------------------------------------------------------------
|
||||
{"xs" "font-size:0.75rem;line-height:1rem"
|
||||
"sm" "font-size:0.875rem;line-height:1.25rem"
|
||||
"base" "font-size:1rem;line-height:1.5rem"
|
||||
"lg" "font-size:1.125rem;line-height:1.75rem"
|
||||
"xl" "font-size:1.25rem;line-height:1.75rem"
|
||||
"2xl" "font-size:1.5rem;line-height:2rem"
|
||||
"3xl" "font-size:1.875rem;line-height:2.25rem"
|
||||
"4xl" "font-size:2.25rem;line-height:2.5rem"
|
||||
"5xl" "font-size:3rem;line-height:1"
|
||||
"6xl" "font-size:3.75rem;line-height:1"
|
||||
"7xl" "font-size:4.5rem;line-height:1"
|
||||
"8xl" "font-size:6rem;line-height:1"
|
||||
"9xl" "font-size:8rem;line-height:1"})
|
||||
|
||||
;; Named font weights
|
||||
(define cssx-weights
|
||||
{"thin" "100"
|
||||
"extralight" "200"
|
||||
@@ -148,67 +133,25 @@
|
||||
"extrabold" "800"
|
||||
"black" "900"})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Font families — named family → CSS font stack
|
||||
;; ---------------------------------------------------------------------------
|
||||
|
||||
;; Named font families
|
||||
(define cssx-families
|
||||
{"sans" "ui-sans-serif,system-ui,-apple-system,BlinkMacSystemFont,\"Segoe UI\",Roboto,\"Helvetica Neue\",Arial,\"Noto Sans\",sans-serif"
|
||||
"serif" "ui-serif,Georgia,Cambria,\"Times New Roman\",Times,serif"
|
||||
"mono" "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace"})
|
||||
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Standalone modifier functions — return CSS declaration strings
|
||||
;; Each returns a complete CSS declaration string. Use inside cssx groups
|
||||
;; or standalone on :style with str.
|
||||
;; ---------------------------------------------------------------------------
|
||||
;; Text alignment keywords
|
||||
(define cssx-alignments
|
||||
{"left" true "center" true "right" true "justify" true})
|
||||
|
||||
;; -- Typography --
|
||||
|
||||
(define size
|
||||
(fn (s) (or (get cssx-sizes s) (str "font-size:" s ";"))))
|
||||
|
||||
(define weight
|
||||
(fn (w)
|
||||
(let ((v (get cssx-weights w)))
|
||||
(str "font-weight:" (or v w) ";"))))
|
||||
|
||||
(define family
|
||||
(fn (f)
|
||||
(let ((v (get cssx-families f)))
|
||||
(str "font-family:" (or v f) ";"))))
|
||||
|
||||
(define align
|
||||
(fn (a) (str "text-align:" a ";")))
|
||||
|
||||
(define decoration
|
||||
(fn (d) (str "text-decoration:" d ";")))
|
||||
|
||||
;; -- Spacing (Tailwind scale: 1 unit = 0.25rem) --
|
||||
|
||||
(define spacing (fn (n) (str (* n 0.25) "rem")))
|
||||
|
||||
(define p (fn (n) (str "padding:" (spacing n) ";")))
|
||||
(define px (fn (n) (str "padding-left:" (spacing n) ";padding-right:" (spacing n) ";")))
|
||||
(define py (fn (n) (str "padding-top:" (spacing n) ";padding-bottom:" (spacing n) ";")))
|
||||
(define pt (fn (n) (str "padding-top:" (spacing n) ";")))
|
||||
(define pb (fn (n) (str "padding-bottom:" (spacing n) ";")))
|
||||
(define pl (fn (n) (str "padding-left:" (spacing n) ";")))
|
||||
(define pr (fn (n) (str "padding-right:" (spacing n) ";")))
|
||||
|
||||
(define m (fn (n) (str "margin:" (spacing n) ";")))
|
||||
(define mx (fn (n) (str "margin-left:" (spacing n) ";margin-right:" (spacing n) ";")))
|
||||
(define my (fn (n) (str "margin-top:" (spacing n) ";margin-bottom:" (spacing n) ";")))
|
||||
(define mt (fn (n) (str "margin-top:" (spacing n) ";")))
|
||||
(define mb (fn (n) (str "margin-bottom:" (spacing n) ";")))
|
||||
(define ml (fn (n) (str "margin-left:" (spacing n) ";")))
|
||||
(define mr (fn (n) (str "margin-right:" (spacing n) ";")))
|
||||
(define mx-auto (fn () "margin-left:auto;margin-right:auto;"))
|
||||
|
||||
;; -- Display & layout --
|
||||
|
||||
(define display (fn (d) (str "display:" d ";")))
|
||||
(define max-w (fn (w) (str "max-width:" w ";")))
|
||||
;; Display keywords → CSS value
|
||||
(define cssx-displays
|
||||
{"block" "block"
|
||||
"inline" "inline"
|
||||
"inline-block" "inline-block"
|
||||
"flex" "flex"
|
||||
"inline-flex" "inline-flex"
|
||||
"grid" "grid"
|
||||
"hidden" "none"})
|
||||
|
||||
;; Named max-widths (Tailwind scale)
|
||||
(define cssx-max-widths
|
||||
@@ -216,4 +159,342 @@
|
||||
"lg" "32rem" "xl" "36rem" "2xl" "42rem"
|
||||
"3xl" "48rem" "4xl" "56rem" "5xl" "64rem"
|
||||
"6xl" "72rem" "7xl" "80rem"
|
||||
"full" "100%" "none" "none"})
|
||||
"full" "100%" "none" "none"
|
||||
"prose" "65ch" "screen" "100vw"})
|
||||
|
||||
;; Responsive breakpoints (mobile-first min-width)
|
||||
(define cssx-breakpoints
|
||||
{"sm" "640px"
|
||||
"md" "768px"
|
||||
"lg" "1024px"
|
||||
"xl" "1280px"
|
||||
"2xl" "1536px"})
|
||||
|
||||
;; Pseudo-class states
|
||||
(define cssx-states
|
||||
{"hover" ":hover"
|
||||
"focus" ":focus"
|
||||
"active" ":active"
|
||||
"focus-within" ":focus-within"
|
||||
"focus-visible" ":focus-visible"
|
||||
"first" ":first-child"
|
||||
"last" ":last-child"})
|
||||
|
||||
;; =========================================================================
|
||||
;; Utility resolver — token string → CSS declarations
|
||||
;; =========================================================================
|
||||
|
||||
;; Spacing value: number → rem, "auto" → "auto", "px" → "1px"
|
||||
(define cssx-spacing-value
|
||||
(fn (v)
|
||||
(cond
|
||||
(= v "auto") "auto"
|
||||
(= v "px") "1px"
|
||||
(= v "0") "0px"
|
||||
true (let ((n (parse-int v nil)))
|
||||
(if (nil? n) nil
|
||||
(str (* n 0.25) "rem"))))))
|
||||
|
||||
;; Replace {v} in a template string with a value
|
||||
(define cssx-template
|
||||
(fn (tmpl v)
|
||||
(let ((i (index-of tmpl "{v}")))
|
||||
(if (< i 0) tmpl
|
||||
(let ((result (str (substring tmpl 0 i) v (substring tmpl (+ i 3) (len tmpl)))))
|
||||
;; Handle templates with multiple {v} (e.g. padding-left:{v};padding-right:{v})
|
||||
(let ((j (index-of result "{v}")))
|
||||
(if (< j 0) result
|
||||
(str (substring result 0 j) v (substring result (+ j 3) (len result))))))))))
|
||||
|
||||
;; Resolve a base utility token (no state/bp prefix) → CSS declaration string or nil.
|
||||
;; Tries matchers in order: colour, text-size, text-align, font, spacing, display, max-w, rounded, opacity.
|
||||
(define cssx-resolve
|
||||
(fn (token)
|
||||
(let ((parts (split token "-")))
|
||||
(if (empty? parts) nil
|
||||
(let ((head (first parts))
|
||||
(rest (slice parts 1)))
|
||||
(cond
|
||||
;; ---------------------------------------------------------
|
||||
;; Colour utilities: bg-{colour}-{shade}, text-{colour}-{shade}, border-{colour}-{shade}
|
||||
;; ---------------------------------------------------------
|
||||
(and (get cssx-colour-props head)
|
||||
(>= (len rest) 2)
|
||||
(not (nil? (parse-int (last rest) nil)))
|
||||
(not (nil? (get colour-bases (join "-" (slice rest 0 (- (len rest) 1)))))))
|
||||
(let ((css-prop (get cssx-colour-props head))
|
||||
(cname (join "-" (slice rest 0 (- (len rest) 1))))
|
||||
(shade (parse-int (last rest) 0)))
|
||||
(str css-prop ":" (colour cname shade)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Text size: text-{size-name} (e.g. text-xl, text-2xl)
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "text")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-sizes (first rest)))))
|
||||
(get cssx-sizes (first rest))
|
||||
|
||||
;; Also handle text-2xl etc where rest might be ("2xl") — covered above
|
||||
;; But also "text-sm" etc — covered above since rest is ("sm")
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Text alignment: text-left, text-center, text-right, text-justify
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "text")
|
||||
(= (len rest) 1)
|
||||
(get cssx-alignments (first rest)))
|
||||
(str "text-align:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Font weight: font-bold, font-semibold, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-weights (first rest)))))
|
||||
(str "font-weight:" (get cssx-weights (first rest)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Font family: font-sans, font-serif, font-mono
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "font")
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-families (first rest)))))
|
||||
(str "font-family:" (get cssx-families (first rest)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Spacing: p-4, px-2, mt-8, mx-auto, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (get cssx-spacing-props head)
|
||||
(= (len rest) 1))
|
||||
(let ((tmpl (get cssx-spacing-props head))
|
||||
(v (cssx-spacing-value (first rest))))
|
||||
(if (nil? v) nil (cssx-template tmpl v)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Display: block, flex, grid, hidden, inline, inline-block
|
||||
;; ---------------------------------------------------------
|
||||
(and (= (len parts) 1)
|
||||
(not (nil? (get cssx-displays head))))
|
||||
(str "display:" (get cssx-displays head))
|
||||
|
||||
;; Inline-block, inline-flex (multi-word)
|
||||
(and (= (len parts) 2)
|
||||
(not (nil? (get cssx-displays token))))
|
||||
(str "display:" (get cssx-displays token))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Max-width: max-w-xl, max-w-3xl, max-w-prose
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "max")
|
||||
(>= (len rest) 2)
|
||||
(= (first rest) "w"))
|
||||
(let ((val-name (join "-" (slice rest 1)))
|
||||
(val (get cssx-max-widths val-name)))
|
||||
(if (nil? val) nil (str "max-width:" val)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Rounded: rounded, rounded-lg, rounded-full, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(= head "rounded")
|
||||
(cond
|
||||
(empty? rest) "border-radius:0.25rem"
|
||||
(= (first rest) "none") "border-radius:0"
|
||||
(= (first rest) "sm") "border-radius:0.125rem"
|
||||
(= (first rest) "md") "border-radius:0.375rem"
|
||||
(= (first rest) "lg") "border-radius:0.5rem"
|
||||
(= (first rest) "xl") "border-radius:0.75rem"
|
||||
(= (first rest) "2xl") "border-radius:1rem"
|
||||
(= (first rest) "3xl") "border-radius:1.5rem"
|
||||
(= (first rest) "full") "border-radius:9999px"
|
||||
true nil)
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Opacity: opacity-{n} (0-100)
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "opacity")
|
||||
(= (len rest) 1))
|
||||
(let ((n (parse-int (first rest) nil)))
|
||||
(if (nil? n) nil (str "opacity:" (/ n 100))))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Width/height: w-{n}, h-{n}, w-full, h-full, h-screen
|
||||
;; ---------------------------------------------------------
|
||||
(and (or (= head "w") (= head "h"))
|
||||
(= (len rest) 1))
|
||||
(let ((prop (if (= head "w") "width" "height"))
|
||||
(val (first rest)))
|
||||
(cond
|
||||
(= val "full") (str prop ":100%")
|
||||
(= val "screen") (str prop (if (= head "w") ":100vw" ":100vh"))
|
||||
(= val "auto") (str prop ":auto")
|
||||
(= val "min") (str prop ":min-content")
|
||||
(= val "max") (str prop ":max-content")
|
||||
(= val "fit") (str prop ":fit-content")
|
||||
true (let ((n (parse-int val nil)))
|
||||
(if (nil? n) nil
|
||||
(str prop ":" (* n 0.25) "rem")))))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Gap: gap-{n}
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "gap")
|
||||
(= (len rest) 1))
|
||||
(let ((v (cssx-spacing-value (first rest))))
|
||||
(if (nil? v) nil (str "gap:" v)))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Text decoration: underline, no-underline, line-through
|
||||
;; ---------------------------------------------------------
|
||||
(and (= (len parts) 1)
|
||||
(or (= head "underline") (= head "overline") (= head "line-through")))
|
||||
(str "text-decoration-line:" head)
|
||||
|
||||
(and (= (len parts) 2) (= head "no") (= (first rest) "underline"))
|
||||
"text-decoration-line:none"
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Cursor: cursor-pointer, cursor-default, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "cursor") (= (len rest) 1))
|
||||
(str "cursor:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Overflow: overflow-hidden, overflow-auto, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "overflow") (= (len rest) 1))
|
||||
(str "overflow:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Transition: transition, transition-colors, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "transition") (empty? rest))
|
||||
"transition-property:color,background-color,border-color,text-decoration-color,fill,stroke,opacity,box-shadow,transform,filter,backdrop-filter;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
|
||||
|
||||
(and (= head "transition") (= (first rest) "colors"))
|
||||
"transition-property:color,background-color,border-color,text-decoration-color,fill,stroke;transition-timing-function:cubic-bezier(0.4,0,0.2,1);transition-duration:150ms"
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Fallback: unrecognised → nil (skip)
|
||||
;; ---------------------------------------------------------
|
||||
true nil))))))
|
||||
|
||||
;; =========================================================================
|
||||
;; Token processor — full token with optional state/bp prefixes
|
||||
;; =========================================================================
|
||||
|
||||
;; Process one token string → {:cls "sx-..." :rule ".sx-...{...}"} or nil
|
||||
;; Token format: [bp:]?[state:]?utility
|
||||
;; Examples: "bg-yellow-199", "hover:bg-rose-500", "md:hover:text-xl"
|
||||
(define cssx-process-token
|
||||
(fn (token)
|
||||
(let ((colon-parts (split token ":"))
|
||||
(n (len colon-parts)))
|
||||
;; Extract state, bp, and base utility from colon-separated parts
|
||||
(let ((bp nil) (state nil) (base nil))
|
||||
;; 1 part: just utility
|
||||
;; 2 parts: modifier:utility (could be bp or state)
|
||||
;; 3 parts: bp:state:utility
|
||||
(cond
|
||||
(= n 1)
|
||||
(do (set! base (first colon-parts)))
|
||||
(= n 2)
|
||||
(let ((prefix (first colon-parts)))
|
||||
(set! base (last colon-parts))
|
||||
(if (not (nil? (get cssx-breakpoints prefix)))
|
||||
(set! bp prefix)
|
||||
(set! state prefix)))
|
||||
(>= n 3)
|
||||
(do
|
||||
(set! bp (first colon-parts))
|
||||
(set! state (nth colon-parts 1))
|
||||
(set! base (last colon-parts))))
|
||||
|
||||
;; Resolve the base utility to CSS declarations
|
||||
(let ((css (cssx-resolve base)))
|
||||
(if (nil? css) nil
|
||||
(let ((cls (str "sx-" (join "-" (split token ":"))))
|
||||
(pseudo (if (nil? state) ""
|
||||
(or (get cssx-states state) (str ":" state))))
|
||||
(decl (str "." cls pseudo "{" css "}")))
|
||||
(if (nil? bp)
|
||||
{"cls" cls "rule" decl}
|
||||
(let ((min-w (or (get cssx-breakpoints bp) bp)))
|
||||
{"cls" cls
|
||||
"rule" (str "@media(min-width:" min-w "){" decl "}")})))))))))
|
||||
|
||||
;; =========================================================================
|
||||
;; tw — Tailwind-style inline style string from utility tokens
|
||||
;;
|
||||
;; Same token format as ~cssx/tw, returns a CSS declaration string for :style.
|
||||
;; Cannot do hover/responsive (use ~cssx/tw for those).
|
||||
;;
|
||||
;; Usage:
|
||||
;; (div :style (tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; "content")
|
||||
;; =========================================================================
|
||||
|
||||
(define tw
|
||||
(fn (tokens-str)
|
||||
(let ((tokens (split (or tokens-str "") " "))
|
||||
(parts (list)))
|
||||
(for-each (fn (tok)
|
||||
(when (not (= tok ""))
|
||||
(let ((css (cssx-resolve tok)))
|
||||
(when (not (nil? css))
|
||||
(append! parts (str css ";"))))))
|
||||
tokens)
|
||||
(join "" parts))))
|
||||
|
||||
;; =========================================================================
|
||||
;; ~cssx/tw — spread component that injects JIT classes onto parent element
|
||||
;;
|
||||
;; Usage — as a child of any element:
|
||||
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; (h1 "styled content"))
|
||||
;;
|
||||
;; (button (~cssx/tw "hover:bg-rose-500 focus:border-blue-400")
|
||||
;; "interactive")
|
||||
;;
|
||||
;; Returns a spread value that merges :class and :data-tw onto the parent
|
||||
;; element. Collects CSS rules into the "cssx" bucket for a single global
|
||||
;; <style> flush. No wrapper element, no per-element <style> tags.
|
||||
;;
|
||||
;; Reusable as variables:
|
||||
;; (define important (~cssx/tw "font-bold text-4xl"))
|
||||
;; (div important "the queen is dead")
|
||||
;;
|
||||
;; Multiple spreads merge naturally:
|
||||
;; (div (~cssx/tw "bg-red-500") (~cssx/tw "p-4") "content")
|
||||
;; =========================================================================
|
||||
|
||||
(defcomp ~cssx/tw (tokens)
|
||||
(let ((token-list (filter (fn (t) (not (= t "")))
|
||||
(split (or tokens "") " ")))
|
||||
(results (map cssx-process-token token-list))
|
||||
(valid (filter (fn (r) (not (nil? r))) results))
|
||||
(classes (map (fn (r) (get r "cls")) valid))
|
||||
(rules (map (fn (r) (get r "rule")) valid))
|
||||
(_ (for-each (fn (rule) (collect! "cssx" rule)) rules)))
|
||||
;; Return spread: injects class + data-tw onto parent element
|
||||
(if (empty? classes)
|
||||
nil
|
||||
(make-spread {"class" (join " " classes)
|
||||
"data-tw" (or tokens "")}))))
|
||||
|
||||
|
||||
;; =========================================================================
|
||||
;; ~cssx/flush — emit collected CSS rules as a single <style> tag
|
||||
;;
|
||||
;; Place once in the page (typically in the layout, before </body>).
|
||||
;; Emits all accumulated CSSX rules and clears the bucket.
|
||||
;;
|
||||
;; Usage:
|
||||
;; (~cssx/flush)
|
||||
;; =========================================================================
|
||||
|
||||
(defcomp ~cssx/flush ()
|
||||
(let ((rules (collected "cssx")))
|
||||
(clear-collected! "cssx")
|
||||
(when (not (empty? rules))
|
||||
(raw! (str "<style data-cssx>" (join "" rules) "</style>")))))
|
||||
|
||||
Reference in New Issue
Block a user