All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 2m33s
Add boundary.sx declaring all 34 I/O primitives, 32 page helpers, and 9 allowed boundary types. Runtime validation in boundary.py checks every registration against the spec — undeclared primitives/helpers crash at startup with SX_BOUNDARY_STRICT=1 (now set in both dev and prod). Key changes: - Move 5 I/O-in-disguise primitives (app-url, asset-url, config, jinja-global, relations-from) from primitives.py to primitives_io.py - Remove duplicate url-for/route-prefix from primitives.py (already in IO) - Fix parse-datetime to return ISO string instead of raw datetime - Add datetime→isoformat conversion in _convert_result at the edge - Wrap page helper return values with boundary type validation - Replace all SxExpr(f"...") patterns with sx_call() or _sx_fragment() - Add assert declaration to primitives.sx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
318 lines
12 KiB
Plaintext
318 lines
12 KiB
Plaintext
;; ==========================================================================
|
|
;; 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)
|
|
(if (is-child-selector-atom? base)
|
|
(append! pseudo-rules
|
|
(list ">:not(:first-child)" decls))
|
|
(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) "}")))
|
|
media-rules)
|
|
(for-each
|
|
(fn (pr)
|
|
(set! hash-input
|
|
(str hash-input (first pr) "{" (nth pr 1) "}")))
|
|
pseudo-rules)
|
|
(for-each
|
|
(fn (kf)
|
|
(set! hash-input (str hash-input (nth kf 1))))
|
|
kf-needed)
|
|
|
|
(let ((cn (str "sx-" (hash-style hash-input)))
|
|
(sv (make-style-value cn
|
|
(join ";" base-decls)
|
|
media-rules
|
|
pseudo-rules
|
|
kf-needed)))
|
|
(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">)
|
|
;; --------------------------------------------------------------------------
|