Add CSSX and boot adapters to SX spec (style dictionary + browser lifecycle)
- cssx.sx: on-demand CSS style dictionary (variant splitting, atom resolution, content-addressed hashing, style merging) - boot.sx: browser boot lifecycle (script processing, mount/hydrate/update, component caching, head element hoisting) - bootstrap_js.py: platform JS for cssx (FNV-1a hash, regex, CSS injection) and boot (localStorage, cookies, DOM mounting) - Rebuilt sx-browser.js (136K) and sx-ref.js (148K) with all adapters Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
314
shared/sx/ref/cssx.sx
Normal file
314
shared/sx/ref/cssx.sx
Normal file
@@ -0,0 +1,314 @@
|
||||
;; ==========================================================================
|
||||
;; cssx.sx — On-demand CSS style dictionary
|
||||
;;
|
||||
;; Resolves keyword atoms (e.g. :flex, :gap-4, :hover:bg-sky-200) into
|
||||
;; StyleValue objects with content-addressed class names. CSS rules are
|
||||
;; injected into the document on first use.
|
||||
;;
|
||||
;; The style dictionary is loaded from a JSON blob (typically served
|
||||
;; inline in a <script type="text/sx-styles"> tag) containing:
|
||||
;; a — atom → CSS declarations map
|
||||
;; v — pseudo-variant → CSS pseudo-selector map
|
||||
;; b — responsive breakpoint → media query map
|
||||
;; k — keyframe name → @keyframes rule map
|
||||
;; p — arbitrary patterns: [[regex, template], ...]
|
||||
;; c — child selector prefixes: ["space-x-", "space-y-", ...]
|
||||
;;
|
||||
;; Depends on:
|
||||
;; render.sx — StyleValue type
|
||||
;; ==========================================================================
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; State — populated by load-style-dict
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define _style-atoms (dict))
|
||||
(define _pseudo-variants (dict))
|
||||
(define _responsive-breakpoints (dict))
|
||||
(define _style-keyframes (dict))
|
||||
(define _arbitrary-patterns (list))
|
||||
(define _child-selector-prefixes (list))
|
||||
(define _style-cache (dict))
|
||||
(define _injected-styles (dict))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Load style dictionary from parsed JSON data
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define load-style-dict
|
||||
(fn (data)
|
||||
(set! _style-atoms (or (get data "a") (dict)))
|
||||
(set! _pseudo-variants (or (get data "v") (dict)))
|
||||
(set! _responsive-breakpoints (or (get data "b") (dict)))
|
||||
(set! _style-keyframes (or (get data "k") (dict)))
|
||||
(set! _child-selector-prefixes (or (get data "c") (list)))
|
||||
;; Compile arbitrary patterns from [regex, template] pairs
|
||||
(set! _arbitrary-patterns
|
||||
(map
|
||||
(fn (pair)
|
||||
(dict "re" (compile-regex (str "^" (first pair) "$"))
|
||||
"tmpl" (nth pair 1)))
|
||||
(or (get data "p") (list))))
|
||||
;; Clear cache on reload
|
||||
(set! _style-cache (dict))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Variant splitting
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define split-variant
|
||||
(fn (atom)
|
||||
;; Parse variant prefixes: "sm:hover:bg-sky-200" → ["sm:hover", "bg-sky-200"]
|
||||
;; Returns [variant, base] where variant is nil for no variant.
|
||||
|
||||
;; Check responsive prefix first
|
||||
(let ((result nil))
|
||||
(for-each
|
||||
(fn (bp)
|
||||
(when (nil? result)
|
||||
(let ((prefix (str bp ":")))
|
||||
(when (starts-with? atom prefix)
|
||||
(let ((rest-atom (slice atom (len prefix))))
|
||||
;; Check for compound variant (sm:hover:...)
|
||||
(let ((inner-match nil))
|
||||
(for-each
|
||||
(fn (pv)
|
||||
(when (nil? inner-match)
|
||||
(let ((inner-prefix (str pv ":")))
|
||||
(when (starts-with? rest-atom inner-prefix)
|
||||
(set! inner-match
|
||||
(list (str bp ":" pv)
|
||||
(slice rest-atom (len inner-prefix))))))))
|
||||
(keys _pseudo-variants))
|
||||
(set! result
|
||||
(or inner-match (list bp rest-atom)))))))))
|
||||
(keys _responsive-breakpoints))
|
||||
|
||||
(when (nil? result)
|
||||
;; Check pseudo variants
|
||||
(for-each
|
||||
(fn (pv)
|
||||
(when (nil? result)
|
||||
(let ((prefix (str pv ":")))
|
||||
(when (starts-with? atom prefix)
|
||||
(set! result (list pv (slice atom (len prefix))))))))
|
||||
(keys _pseudo-variants)))
|
||||
|
||||
(or result (list nil atom)))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Atom resolution
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define resolve-atom
|
||||
(fn (atom)
|
||||
;; Look up atom → CSS declarations string, or nil
|
||||
(let ((decls (dict-get _style-atoms atom)))
|
||||
(if (not (nil? decls))
|
||||
decls
|
||||
;; Dynamic keyframes: animate-{name}
|
||||
(if (starts-with? atom "animate-")
|
||||
(let ((kf-name (slice atom 8)))
|
||||
(if (dict-has? _style-keyframes kf-name)
|
||||
(str "animation-name:" kf-name)
|
||||
nil))
|
||||
;; Try arbitrary patterns
|
||||
(let ((match-result nil))
|
||||
(for-each
|
||||
(fn (pat)
|
||||
(when (nil? match-result)
|
||||
(let ((m (regex-match (get pat "re") atom)))
|
||||
(when m
|
||||
(set! match-result
|
||||
(regex-replace-groups (get pat "tmpl") m))))))
|
||||
_arbitrary-patterns)
|
||||
match-result))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Child selector detection
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define is-child-selector-atom?
|
||||
(fn (atom)
|
||||
(some
|
||||
(fn (prefix) (starts-with? atom prefix))
|
||||
_child-selector-prefixes)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; FNV-1a 32-bit hash → 6 hex chars
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define hash-style
|
||||
(fn (input)
|
||||
;; FNV-1a 32-bit hash for content-addressed class names
|
||||
(fnv1a-hash input)))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Full style resolution pipeline
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define resolve-style
|
||||
(fn (atoms)
|
||||
;; Resolve a list of atom strings into a StyleValue.
|
||||
;; Uses content-addressed caching.
|
||||
(let ((key (join "\0" atoms)))
|
||||
(let ((cached (dict-get _style-cache key)))
|
||||
(if (not (nil? cached))
|
||||
cached
|
||||
;; Resolve each atom
|
||||
(let ((base-decls (list))
|
||||
(media-rules (list))
|
||||
(pseudo-rules (list))
|
||||
(kf-needed (list)))
|
||||
(for-each
|
||||
(fn (a)
|
||||
(when a
|
||||
(let ((clean (if (starts-with? a ":") (slice a 1) a)))
|
||||
(let ((parts (split-variant clean)))
|
||||
(let ((variant (first parts))
|
||||
(base (nth parts 1))
|
||||
(decls (resolve-atom base)))
|
||||
(when decls
|
||||
;; Check keyframes
|
||||
(when (starts-with? base "animate-")
|
||||
(let ((kf-name (slice base 8)))
|
||||
(when (dict-has? _style-keyframes kf-name)
|
||||
(append! kf-needed
|
||||
(list kf-name (dict-get _style-keyframes kf-name))))))
|
||||
|
||||
(cond
|
||||
(nil? variant)
|
||||
(append! base-decls decls)
|
||||
|
||||
(dict-has? _responsive-breakpoints variant)
|
||||
(append! media-rules
|
||||
(list (dict-get _responsive-breakpoints variant) decls))
|
||||
|
||||
(dict-has? _pseudo-variants variant)
|
||||
(append! pseudo-rules
|
||||
(list (dict-get _pseudo-variants variant) decls))
|
||||
|
||||
;; Compound variant: "sm:hover"
|
||||
:else
|
||||
(let ((vparts (split variant ":"))
|
||||
(media-part nil)
|
||||
(pseudo-part nil))
|
||||
(for-each
|
||||
(fn (vp)
|
||||
(cond
|
||||
(dict-has? _responsive-breakpoints vp)
|
||||
(set! media-part (dict-get _responsive-breakpoints vp))
|
||||
(dict-has? _pseudo-variants vp)
|
||||
(set! pseudo-part (dict-get _pseudo-variants vp))))
|
||||
vparts)
|
||||
(when media-part
|
||||
(append! media-rules (list media-part decls)))
|
||||
(when pseudo-part
|
||||
(append! pseudo-rules (list pseudo-part decls)))
|
||||
(when (and (nil? media-part) (nil? pseudo-part))
|
||||
(append! base-decls decls))))))))))
|
||||
atoms)
|
||||
|
||||
;; Build hash input
|
||||
(let ((hash-input (join ";" base-decls)))
|
||||
(for-each
|
||||
(fn (mr)
|
||||
(set! hash-input
|
||||
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||
(chunk-every media-rules 2))
|
||||
(for-each
|
||||
(fn (pr)
|
||||
(set! hash-input
|
||||
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||
(chunk-every pseudo-rules 2))
|
||||
(for-each
|
||||
(fn (kf)
|
||||
(set! hash-input (str hash-input (nth kf 1))))
|
||||
(chunk-every kf-needed 2))
|
||||
|
||||
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||
(sv (make-style-value cn
|
||||
(join ";" base-decls)
|
||||
(chunk-every media-rules 2)
|
||||
(chunk-every pseudo-rules 2)
|
||||
(chunk-every kf-needed 2))))
|
||||
(dict-set! _style-cache key sv)
|
||||
;; Inject CSS rules
|
||||
(inject-style-value sv atoms)
|
||||
sv))))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Merge multiple StyleValues
|
||||
;; --------------------------------------------------------------------------
|
||||
|
||||
(define merge-style-values
|
||||
(fn (styles)
|
||||
(if (= (len styles) 1)
|
||||
(first styles)
|
||||
(let ((all-decls (list))
|
||||
(all-media (list))
|
||||
(all-pseudo (list))
|
||||
(all-kf (list)))
|
||||
(for-each
|
||||
(fn (sv)
|
||||
(when (style-value-declarations sv)
|
||||
(append! all-decls (style-value-declarations sv)))
|
||||
(set! all-media (concat all-media (style-value-media-rules sv)))
|
||||
(set! all-pseudo (concat all-pseudo (style-value-pseudo-rules sv)))
|
||||
(set! all-kf (concat all-kf (style-value-keyframes sv))))
|
||||
styles)
|
||||
|
||||
(let ((hash-input (join ";" all-decls)))
|
||||
(for-each
|
||||
(fn (mr)
|
||||
(set! hash-input
|
||||
(str hash-input "@" (first mr) "{" (nth mr 1) "}")))
|
||||
all-media)
|
||||
(for-each
|
||||
(fn (pr)
|
||||
(set! hash-input
|
||||
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
||||
all-pseudo)
|
||||
(for-each
|
||||
(fn (kf)
|
||||
(set! hash-input (str hash-input (nth kf 1))))
|
||||
all-kf)
|
||||
|
||||
(let ((cn (str "sx-" (hash-style hash-input)))
|
||||
(merged (make-style-value cn
|
||||
(join ";" all-decls)
|
||||
all-media all-pseudo all-kf)))
|
||||
(inject-style-value merged (list))
|
||||
merged))))))
|
||||
|
||||
|
||||
;; --------------------------------------------------------------------------
|
||||
;; Platform interface — CSSX
|
||||
;; --------------------------------------------------------------------------
|
||||
;;
|
||||
;; Hash:
|
||||
;; (fnv1a-hash input) → 6-char hex string (FNV-1a 32-bit)
|
||||
;;
|
||||
;; Regex:
|
||||
;; (compile-regex pattern) → compiled regex object
|
||||
;; (regex-match re str) → match array or nil
|
||||
;; (regex-replace-groups tmpl match) → string with {0},{1},... replaced
|
||||
;;
|
||||
;; StyleValue construction:
|
||||
;; (make-style-value cn decls media pseudo kf) → StyleValue object
|
||||
;; (style-value-declarations sv) → declarations string
|
||||
;; (style-value-media-rules sv) → list of [query, decls] pairs
|
||||
;; (style-value-pseudo-rules sv) → list of [selector, decls] pairs
|
||||
;; (style-value-keyframes sv) → list of [name, rule] pairs
|
||||
;;
|
||||
;; CSS injection:
|
||||
;; (inject-style-value sv atoms) → void (append CSS rules to <style id="sx-css">)
|
||||
;; --------------------------------------------------------------------------
|
||||
Reference in New Issue
Block a user