(define colour-bases {:orange {:s 95 :h 25} :cyan {:s 94 :h 188} :sky {:s 89 :h 199} :pink {:s 81 :h 330} :zinc {:s 5 :h 240} :amber {:s 92 :h 38} :neutral {:s 0 :h 0} :lime {:s 78 :h 84} :violet {:s 70 :h 263} :fuchsia {:s 84 :h 292} :stone {:s 6 :h 25} :black {:s 0 :h 0} :teal {:s 80 :h 173} :gray {:s 9 :h 220} :red {:s 72 :h 0} :rose {:s 89 :h 350} :blue {:s 91 :h 217} :emerald {:s 84 :h 160} :green {:s 71 :h 142} :yellow {:s 96 :h 48} :purple {:s 81 :h 271} :indigo {:s 84 :h 239} :white {:s 0 :h 0} :slate {:s 16 :h 215}}) (define lerp (fn (a b t) (+ a (* t (- b a))))) (define shade-to-lightness (fn (shade) (cond (<= shade 50) (lerp 100 97 (/ shade 50)) (<= shade 100) (lerp 97 93 (/ (- shade 50) 50)) (<= shade 200) (lerp 93 87 (/ (- shade 100) 100)) (<= shade 300) (lerp 87 77 (/ (- shade 200) 100)) (<= shade 400) (lerp 77 64 (/ (- shade 300) 100)) (<= shade 500) (lerp 64 53 (/ (- shade 400) 100)) (<= shade 600) (lerp 53 45 (/ (- shade 500) 100)) (<= shade 700) (lerp 45 38 (/ (- shade 600) 100)) (<= shade 800) (lerp 38 30 (/ (- shade 700) 100)) (<= shade 900) (lerp 30 21 (/ (- shade 800) 100)) (<= shade 950) (lerp 21 13 (/ (- shade 900) 50)) true 13))) (define colour (fn (name shade) (let ((base (get colour-bases name))) (if (nil? base) name (let ((h (get base "h")) (s (get base "s")) (l (shade-to-lightness shade))) (str "hsl(" h "," s "%," (round l) "%)")))))) (define cssx-colour-props {:bg "background-color" :border "border-color" :text "color"}) (define cssx-spacing-props {:ml "margin-left:{v}" :mr "margin-right:{v}" :mt "margin-top:{v}" :mb "margin-bottom:{v}" :pl "padding-left:{v}" :m "margin:{v}" :my "margin-top:{v};margin-bottom:{v}" :px "padding-left:{v};padding-right:{v}" :pb "padding-bottom:{v}" :pr "padding-right:{v}" :p "padding:{v}" :py "padding-top:{v};padding-bottom:{v}" :pt "padding-top:{v}" :mx "margin-left:{v};margin-right:{v}"}) (define cssx-sizes {:xs "font-size:0.75rem;line-height:1rem" :3xl "font-size:1.875rem;line-height:2.25rem" :7xl "font-size:4.5rem;line-height:1" :sm "font-size:0.875rem;line-height:1.25rem" :8xl "font-size:6rem;line-height:1" :xl "font-size:1.25rem;line-height:1.75rem" :6xl "font-size:3.75rem;line-height:1" :9xl "font-size:8rem;line-height:1" :5xl "font-size:3rem;line-height:1" :lg "font-size:1.125rem;line-height:1.75rem" :2xl "font-size:1.5rem;line-height:2rem" :base "font-size:1rem;line-height:1.5rem" :4xl "font-size:2.25rem;line-height:2.5rem"}) (define cssx-weights {:light "300" :semibold "600" :bold "700" :extrabold "800" :black "900" :extralight "200" :thin "100" :medium "500" :normal "400"}) (define cssx-families {:mono "ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,\"Liberation Mono\",\"Courier New\",monospace" :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"}) (define cssx-alignments {:center true :left true :right true :justify true}) (define cssx-displays {:flex "flex" :grid "grid" :inline-block "inline-block" :inline "inline" :hidden "none" :block "block" :inline-flex "inline-flex"}) (define cssx-max-widths {:xs "20rem" :3xl "48rem" :7xl "80rem" :sm "24rem" :xl "36rem" :full "100%" :md "28rem" :6xl "72rem" :prose "65ch" :5xl "64rem" :lg "32rem" :2xl "42rem" :4xl "56rem" :none "none" :screen "100vw"}) (define cssx-breakpoints {:sm "640px" :xl "1280px" :md "768px" :lg "1024px" :2xl "1536px"}) (define cssx-states {:focus ":focus" :first ":first-child" :hover ":hover" :focus-visible ":focus-visible" :last ":last-child" :active ":active" :focus-within ":focus-within"}) (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")))))) (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))))) (let ((j (index-of result "{v}"))) (if (< j 0) result (str (substring result 0 j) v (substring result (+ j 3) (len result)))))))))) (define cssx-resolve (fn (token) (let ((parts (split token "-"))) (if (empty? parts) nil (let ((head (first parts)) (rest (slice parts 1))) (cond (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))) (and (= head "text") (= (len rest) 1) (not (nil? (get cssx-sizes (first rest))))) (get cssx-sizes (first rest)) (and (= head "text") (= (len rest) 1) (get cssx-alignments (first rest))) (str "text-align:" (first rest)) (and (= head "font") (= (len rest) 1) (not (nil? (get cssx-weights (first rest))))) (str "font-weight:" (get cssx-weights (first rest))) (and (= head "font") (= (len rest) 1) (not (nil? (get cssx-families (first rest))))) (str "font-family:" (get cssx-families (first rest))) (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))) (and (= (len parts) 1) (not (nil? (get cssx-displays head)))) (str "display:" (get cssx-displays head)) (and (= (len parts) 2) (not (nil? (get cssx-displays token)))) (str "display:" (get cssx-displays token)) (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))) (= 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) (and (= head "opacity") (= (len rest) 1)) (let ((n (parse-int (first rest) nil))) (if (nil? n) nil (str "opacity:" (/ n 100)))) (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"))))) (and (= head "gap") (= (len rest) 1)) (let ((v (cssx-spacing-value (first rest)))) (if (nil? v) nil (str "gap:" v))) (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" (and (= head "cursor") (= (len rest) 1)) (str "cursor:" (first rest)) (and (= head "overflow") (= (len rest) 1)) (str "overflow:" (first rest)) (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" true nil)))))) (define cssx-process-token (fn (token) (let ((colon-parts (split token ":")) (n (len colon-parts))) (let ((bp nil) (state nil) (base nil)) (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)))) (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) {:rule decl :cls cls} (let ((min-w (or (get cssx-breakpoints bp) bp))) {:rule (str "@media(min-width:" min-w "){" decl "}") :cls cls}))))))))) (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))))