Add spread + collect primitives, rewrite ~cssx/tw as defcomp
New SX primitives for child-to-parent communication in the render tree: - spread type: make-spread, spread?, spread-attrs — child injects attrs onto parent element (class joins with space, style with semicolon) - collect!/collected/clear-collected! — render-time accumulation with dedup into named buckets ~cssx/tw is now a proper defcomp returning a spread value instead of a macro wrapping children. ~cssx/flush reads collected "cssx" rules and emits a single <style data-cssx> tag. All four render adapters (html, async, dom, aser) handle spread values. Both bootstraps (Python + JS) regenerated. Also fixes length→len in cssx.sx (length was never a registered primitive). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,15 +1,23 @@
|
||||
;; @client — send all define forms to browser for client-side use.
|
||||
;; CSSX — computed CSS from s-expressions.
|
||||
;;
|
||||
;; Tailwind-style utility component. Write styling as utility tokens:
|
||||
;; Tailwind-style utility component using spread + collect primitives.
|
||||
;; Use as a child of any element — injects classes onto the parent:
|
||||
;;
|
||||
;; (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold"
|
||||
;; (div "content"))
|
||||
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; "content")
|
||||
;;
|
||||
;; (~cssx/tw "hover:bg-rose-500 md:text-xl"
|
||||
;; (button "click me"))
|
||||
;; (button (~cssx/tw "hover:bg-rose-500 md:text-xl")
|
||||
;; "click me")
|
||||
;;
|
||||
;; 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.
|
||||
;;
|
||||
;; Reusable style variables:
|
||||
;; (define fancy (~cssx/tw "font-bold text-violet-700 text-4xl"))
|
||||
;; (div fancy "styled content")
|
||||
;;
|
||||
;; Each token becomes a deterministic class + JIT <style> rule.
|
||||
;; This is one instance of the CSSX component pattern — other styling
|
||||
;; components are possible with different vocabulary.
|
||||
|
||||
@@ -192,11 +200,11 @@
|
||||
(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) (length 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) (length 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.
|
||||
@@ -211,11 +219,11 @@
|
||||
;; Colour utilities: bg-{colour}-{shade}, text-{colour}-{shade}, border-{colour}-{shade}
|
||||
;; ---------------------------------------------------------
|
||||
(and (get cssx-colour-props head)
|
||||
(>= (length rest) 2)
|
||||
(>= (len rest) 2)
|
||||
(not (nil? (parse-int (last rest) nil)))
|
||||
(not (nil? (get colour-bases (join "-" (slice rest 0 (- (length rest) 1)))))))
|
||||
(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 (- (length rest) 1))))
|
||||
(cname (join "-" (slice rest 0 (- (len rest) 1))))
|
||||
(shade (parse-int (last rest) 0)))
|
||||
(str css-prop ":" (colour cname shade)))
|
||||
|
||||
@@ -223,7 +231,7 @@
|
||||
;; Text size: text-{size-name} (e.g. text-xl, text-2xl)
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "text")
|
||||
(= (length rest) 1)
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-sizes (first rest)))))
|
||||
(get cssx-sizes (first rest))
|
||||
|
||||
@@ -234,7 +242,7 @@
|
||||
;; Text alignment: text-left, text-center, text-right, text-justify
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "text")
|
||||
(= (length rest) 1)
|
||||
(= (len rest) 1)
|
||||
(get cssx-alignments (first rest)))
|
||||
(str "text-align:" (first rest))
|
||||
|
||||
@@ -242,7 +250,7 @@
|
||||
;; Font weight: font-bold, font-semibold, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "font")
|
||||
(= (length rest) 1)
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-weights (first rest)))))
|
||||
(str "font-weight:" (get cssx-weights (first rest)))
|
||||
|
||||
@@ -250,7 +258,7 @@
|
||||
;; Font family: font-sans, font-serif, font-mono
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "font")
|
||||
(= (length rest) 1)
|
||||
(= (len rest) 1)
|
||||
(not (nil? (get cssx-families (first rest)))))
|
||||
(str "font-family:" (get cssx-families (first rest)))
|
||||
|
||||
@@ -258,7 +266,7 @@
|
||||
;; Spacing: p-4, px-2, mt-8, mx-auto, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (get cssx-spacing-props head)
|
||||
(= (length rest) 1))
|
||||
(= (len rest) 1))
|
||||
(let ((tmpl (get cssx-spacing-props head))
|
||||
(v (cssx-spacing-value (first rest))))
|
||||
(if (nil? v) nil (cssx-template tmpl v)))
|
||||
@@ -266,12 +274,12 @@
|
||||
;; ---------------------------------------------------------
|
||||
;; Display: block, flex, grid, hidden, inline, inline-block
|
||||
;; ---------------------------------------------------------
|
||||
(and (= (length parts) 1)
|
||||
(and (= (len parts) 1)
|
||||
(not (nil? (get cssx-displays head))))
|
||||
(str "display:" (get cssx-displays head))
|
||||
|
||||
;; Inline-block, inline-flex (multi-word)
|
||||
(and (= (length parts) 2)
|
||||
(and (= (len parts) 2)
|
||||
(not (nil? (get cssx-displays token))))
|
||||
(str "display:" (get cssx-displays token))
|
||||
|
||||
@@ -279,7 +287,7 @@
|
||||
;; Max-width: max-w-xl, max-w-3xl, max-w-prose
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "max")
|
||||
(>= (length rest) 2)
|
||||
(>= (len rest) 2)
|
||||
(= (first rest) "w"))
|
||||
(let ((val-name (join "-" (slice rest 1)))
|
||||
(val (get cssx-max-widths val-name)))
|
||||
@@ -305,7 +313,7 @@
|
||||
;; Opacity: opacity-{n} (0-100)
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "opacity")
|
||||
(= (length rest) 1))
|
||||
(= (len rest) 1))
|
||||
(let ((n (parse-int (first rest) nil)))
|
||||
(if (nil? n) nil (str "opacity:" (/ n 100))))
|
||||
|
||||
@@ -313,7 +321,7 @@
|
||||
;; Width/height: w-{n}, h-{n}, w-full, h-full, h-screen
|
||||
;; ---------------------------------------------------------
|
||||
(and (or (= head "w") (= head "h"))
|
||||
(= (length rest) 1))
|
||||
(= (len rest) 1))
|
||||
(let ((prop (if (= head "w") "width" "height"))
|
||||
(val (first rest)))
|
||||
(cond
|
||||
@@ -331,30 +339,30 @@
|
||||
;; Gap: gap-{n}
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "gap")
|
||||
(= (length rest) 1))
|
||||
(= (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 (= (length parts) 1)
|
||||
(and (= (len parts) 1)
|
||||
(or (= head "underline") (= head "overline") (= head "line-through")))
|
||||
(str "text-decoration-line:" head)
|
||||
|
||||
(and (= (length parts) 2) (= head "no") (= (first rest) "underline"))
|
||||
(and (= (len parts) 2) (= head "no") (= (first rest) "underline"))
|
||||
"text-decoration-line:none"
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Cursor: cursor-pointer, cursor-default, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "cursor") (= (length rest) 1))
|
||||
(and (= head "cursor") (= (len rest) 1))
|
||||
(str "cursor:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
;; Overflow: overflow-hidden, overflow-auto, etc.
|
||||
;; ---------------------------------------------------------
|
||||
(and (= head "overflow") (= (length rest) 1))
|
||||
(and (= head "overflow") (= (len rest) 1))
|
||||
(str "overflow:" (first rest))
|
||||
|
||||
;; ---------------------------------------------------------
|
||||
@@ -381,7 +389,7 @@
|
||||
(define cssx-process-token
|
||||
(fn (token)
|
||||
(let ((colon-parts (split token ":"))
|
||||
(n (length colon-parts)))
|
||||
(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
|
||||
@@ -439,80 +447,54 @@
|
||||
(join "" parts))))
|
||||
|
||||
;; =========================================================================
|
||||
;; ~cssx/tw — macro that injects JIT classes onto the first child element
|
||||
;; ~cssx/tw — spread component that injects JIT classes onto parent element
|
||||
;;
|
||||
;; Usage:
|
||||
;; (~cssx/tw "bg-yellow-199"
|
||||
;; (p "sunny"))
|
||||
;; Usage — as a child of any element:
|
||||
;; (div (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold")
|
||||
;; (h1 "styled content"))
|
||||
;;
|
||||
;; (~cssx/tw "bg-yellow-199 text-violet-700 p-4 font-bold rounded-lg"
|
||||
;; (div (h1 "styled content")))
|
||||
;; (button (~cssx/tw "hover:bg-rose-500 focus:border-blue-400")
|
||||
;; "interactive")
|
||||
;;
|
||||
;; (~cssx/tw "hover:bg-rose-500 focus:border-blue-400"
|
||||
;; (button "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.
|
||||
;;
|
||||
;; (~cssx/tw "md:text-xl lg:p-8"
|
||||
;; (section "responsive"))
|
||||
;; Reusable as variables:
|
||||
;; (define important (~cssx/tw "font-bold text-4xl"))
|
||||
;; (div important "the queen is dead")
|
||||
;;
|
||||
;; Parses tokens at macro-expansion time, injects :class onto the first
|
||||
;; child element (merging with any existing :class), and prepends a
|
||||
;; <style> tag with the JIT CSS rules. No wrapper element.
|
||||
;; Multiple spreads merge naturally:
|
||||
;; (div (~cssx/tw "bg-red-500") (~cssx/tw "p-4") "content")
|
||||
;; =========================================================================
|
||||
|
||||
;; Merge :class into an element's arg list.
|
||||
;; If element already has :class, prepend our classes to its value.
|
||||
;; If not, inject :class after the tag name.
|
||||
(define cssx-inject-class
|
||||
(fn (element cls-str)
|
||||
(let ((tag (first element))
|
||||
(args (slice element 1)))
|
||||
(cons tag (cssx-merge-class-args args cls-str false)))))
|
||||
(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 "")}))))
|
||||
|
||||
;; Walk arg list: find :class keyword, merge value. If not found, inject at end.
|
||||
(define cssx-merge-class-args
|
||||
(fn (args cls-str found)
|
||||
(if (empty? args)
|
||||
;; End of args — if no :class was found, inject one
|
||||
(if found (list) (list :class cls-str))
|
||||
(let ((head (first args))
|
||||
(tail (slice args 1)))
|
||||
(if (and (not found)
|
||||
(= (type-of head) "keyword")
|
||||
(= (keyword-name head) "class"))
|
||||
;; Found :class — merge with next arg (the value)
|
||||
(if (empty? tail)
|
||||
;; :class with no value — replace with ours
|
||||
(append (list :class cls-str) (list))
|
||||
;; :class with value — prepend our classes
|
||||
(append (list :class (str cls-str " " (first tail)))
|
||||
(cssx-merge-class-args (slice tail 1) cls-str true)))
|
||||
;; Not :class — keep and continue
|
||||
(cons head (cssx-merge-class-args tail cls-str found)))))))
|
||||
|
||||
(defmacro ~cssx/tw (tokens &rest children)
|
||||
(let ((token-list (filter (fn (t) (not (= t ""))) (split tokens " ")))
|
||||
(classes (list))
|
||||
(rules (list)))
|
||||
;; Process each token
|
||||
(for-each (fn (tok)
|
||||
(let ((r (cssx-process-token tok)))
|
||||
(when (not (nil? r))
|
||||
(append! classes (get r "cls"))
|
||||
(append! rules (get r "rule")))))
|
||||
token-list)
|
||||
(let ((cls-str (join " " classes))
|
||||
(rules-str (join "" rules))
|
||||
(first-child (first children))
|
||||
(rest-children (slice children 1)))
|
||||
(if (empty? classes)
|
||||
;; No resolved tokens — pass through unchanged
|
||||
(if (= (length children) 1) first-child `(<> ,@children))
|
||||
;; Inject class onto first child element
|
||||
(if (and (list? first-child) (not (empty? first-child)))
|
||||
;; First child is an element — inject :class, prepend <style>
|
||||
(let ((injected (cssx-inject-class first-child cls-str)))
|
||||
(if (empty? rest-children)
|
||||
`(<> (style ,rules-str) ,injected)
|
||||
`(<> (style ,rules-str) ,injected ,@rest-children)))
|
||||
;; First child isn't an element — wrap everything in div
|
||||
`(<> (style ,rules-str) (div :class ,cls-str ,@children)))))))
|
||||
;; =========================================================================
|
||||
;; ~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