├── .vscode └── settings.json ├── .gitattributes ├── .gitignore ├── cljfmt.edn ├── src └── fkcss │ ├── misc.cljc │ ├── cljs.cljs │ ├── core.cljc │ └── render.cljc ├── deps.edn ├── test └── fkcss │ ├── core_test.cljc │ └── render_test.cljc ├── LICENSE ├── pom.xml └── README.md /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto eol=crlf 2 | *.sh text eol=lf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | **/.lsp 2 | **/.calva 3 | **/.idea 4 | **/.cpcache 5 | **/.nrepl-port 6 | bin/ 7 | target/ 8 | .settings/ 9 | .project 10 | .classpath 11 | fkcss.jar 12 | fkcss*.asc -------------------------------------------------------------------------------- /cljfmt.edn: -------------------------------------------------------------------------------- 1 | {:remove-surrounding-whitespace? true 2 | :remove-trailing-whitespace? true 3 | :remove-consecutive-blank-lines? false 4 | :insert-missing-whitespace? false 5 | :align-associative? false 6 | :indents ^:replace {#"^.*" [[:inner 0]]}} -------------------------------------------------------------------------------- /src/fkcss/misc.cljc: -------------------------------------------------------------------------------- 1 | (ns fkcss.misc 2 | (:require 3 | [clojure.string :as str])) 4 | 5 | (defn panic [msg kvs] 6 | (throw 7 | #?(:clj (ex-info msg kvs) 8 | :cljs (js/Error. msg #js{:cause (str kvs)})))) 9 | 10 | (defn reduce-whitespace [s] 11 | (-> s (str/replace #"\s+" " ") str/trim)) -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:paths ["src"] 2 | 3 | :aliases 4 | {:test 5 | {:extra-paths ["test"] 6 | 7 | :extra-deps 8 | {io.github.cognitect-labs/test-runner 9 | {:git/url "https://github.com/cognitect-labs/test-runner.git" 10 | :sha "8c3f22363d63715de4087b038d79ae0de36a3263"}} 11 | 12 | :exec-fn cognitect.test-runner.api/test} 13 | 14 | :build 15 | {:replace-deps {com.github.seancorfield/depstar {:mvn/version "2.0.216"}} 16 | :exec-fn hf.depstar/jar 17 | :exec-args {:jar "fkcss.jar"}} 18 | 19 | :deploy 20 | {:extra-deps {slipset/deps-deploy {:mvn/version "RELEASE"}} 21 | :exec-fn deps-deploy.deps-deploy/deploy 22 | :exec-args 23 | {:installer :remote 24 | :sign-releases? true 25 | :artifact "fkcss.jar"}}}} -------------------------------------------------------------------------------- /test/fkcss/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns fkcss.core-test 2 | (:require 3 | #?(:clj [fkcss.core :as ss] 4 | :cljs [fkcss.core :as ss :require-macros true]) 5 | [fkcss.misc :as ss-misc] 6 | #?(:clj [clojure.test :refer [deftest is are run-tests testing]] 7 | :cljs [cljs.test :refer [deftest is are run-tests testing] :include-macros true]))) 8 | 9 | (deftest gen-css-test 10 | (binding [ss/*registry* (atom ss/EMPTY-REGISTRY)] 11 | (ss/defclass thing {:color "red"}) 12 | (let [css (-> (ss/gen-css) ss-misc/reduce-whitespace)] 13 | (is (= css ".fkcss-core-test-thing { color: red; }")))) 14 | (binding [ss/*registry* (atom ss/EMPTY-REGISTRY)] 15 | (ss/defanimation thing {:from {:color "red"} :to {:color "blue"}}) 16 | (let [css (-> (ss/gen-css) ss-misc/reduce-whitespace)] 17 | (is (= css "@keyframes fkcss-core-test-thing { from { color: red; } to { color: blue; } }")))) 18 | (binding [ss/*registry* (atom ss/EMPTY-REGISTRY)] 19 | (ss/reg-font! "SomeFont" {}) 20 | (let [css (-> (ss/gen-css) ss-misc/reduce-whitespace)] 21 | (is (= css "@font-face { font-family: 'SomeFont'; }"))))) -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Ray Stubbs 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/fkcss/cljs.cljs: -------------------------------------------------------------------------------- 1 | (ns fkcss.cljs 2 | (:require 3 | [fkcss.core :as ss])) 4 | 5 | (defonce ^:private mounted-styles (atom {})) 6 | 7 | (defn- get-style-node [id] 8 | (or (js/document.getElementById (str id)) 9 | (let [node (js/document.createElement "style")] 10 | (set! (.-id node) (str id)) 11 | (js/document.body.prepend node) 12 | node))) 13 | 14 | (defn- inject-css [id opts] 15 | (let [node (get-style-node id)] 16 | (set! (.-innerHTML node) (ss/gen-css opts)) 17 | (swap! mounted-styles assoc id {:node node :opts opts}))) 18 | 19 | (defn mount! 20 | ([] 21 | (mount! "fkcss-styles" {})) 22 | ([id opts] 23 | (if (some? js/document.body) 24 | (inject-css id opts) 25 | (js/window.addEventListener "DOMContentLoaded" (partial inject-css id opts))))) 26 | 27 | (defn ^:private ^:dev/after-load ^:after-load re-gen [] 28 | (doseq [{:keys [node opts]} (vals @mounted-styles)] 29 | (set! (.-innerHTML node) (ss/gen-css opts)))) 30 | 31 | (defn unmount! 32 | ([] 33 | (unmount! "fkcss-styles")) 34 | ([id] 35 | (let [el (js/document.getElementById (str id))] 36 | (-> el .-parent (.removeChild el)) 37 | (swap! mounted-styles dissoc id)))) -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4.0.0 4 | jar 5 | io.fkcss 6 | fkcss 7 | 0.1.3 8 | fkcss 9 | Web styling in Clojure. 10 | https://github.com/raystubbs/FkCSS 11 | 2021 12 | 13 | 14 | MIT 15 | https://opensource.org/licenses/MIT 16 | 17 | 18 | 19 | 20 | org.clojure 21 | clojure 22 | 1.10.3 23 | 24 | 25 | 26 | src 27 | 28 | 29 | 30 | clojars 31 | https://repo.clojars.org/ 32 | 33 | 34 | 35 | -------------------------------------------------------------------------------- /test/fkcss/render_test.cljc: -------------------------------------------------------------------------------- 1 | (ns fkcss.render-test 2 | (:require 3 | [fkcss.render :as ss-render] 4 | [fkcss.misc :as ss-misc] 5 | [clojure.string :as str] 6 | #?(:clj [clojure.test :refer [deftest is are run-tests testing]] 7 | :cljs [cljs.test :refer [deftest is are run-tests testing] :include-macros true]))) 8 | 9 | (deftest resolve-properties-test 10 | (are [x y] (= (#'fkcss.render/resolve-properties x) y) 11 | {:margin {:left "1rem" :top "1rem"}} 12 | {:margin-left "1rem" :margin-top "1rem"} 13 | 14 | {:margin-x "1rem"} 15 | {:margin-left "1rem" :margin-right "1rem"} 16 | 17 | {:margin-y "1rem"} 18 | {:margin-top "1rem" :margin-bottom "1rem"})) 19 | 20 | (deftest resolve-selectors-test 21 | (are [x y] (= (into {} (#'fkcss.render/resolve-selectors x)) y) 22 | {:div> {}} 23 | {[] {} [{:tag "div"}] {}} 24 | 25 | {:div> {"btn btn-red" {}}} 26 | {[] {} [{:tag "div"}] {} [{:tag "div" :classes ["btn" "btn-red"]}] {}} 27 | 28 | {[:div> :hovered?] {}} 29 | {[] {} [{:tag "div" :predicates #{(:hovered? ss-render/default-predicates)}}] {}} 30 | 31 | {[:div> :button> :before>>] {}} 32 | {[] {} [{:tag "div"} {:tag "button" :pseudo-el "before"}] {}})) 33 | 34 | (deftest render-style-test 35 | (are [x y] (= (-> x (ss-render/render-style {}) ss-misc/reduce-whitespace) y) 36 | {:div> {:color "red"}} 37 | "div { color: red; }" 38 | 39 | {[:div> :hovered?] {:color "red"}} 40 | "div:hover { color: red; }" 41 | 42 | {[:div> :before>>] {:color "red"}} 43 | "div::before { color: red; }" 44 | 45 | {[:hoverable? :div>] {:color "red"}} 46 | "@media (hover: hover) { div { color: red; } }")) 47 | 48 | (deftest render-font-test 49 | (are [x y] (= (-> x (ss-render/render-font {}) ss-misc/reduce-whitespace) y) 50 | [{:font-family "Test" 51 | :src "url(none)"}] 52 | "@font-face { font-family: 'Test'; src: url(none); }")) 53 | -------------------------------------------------------------------------------- /src/fkcss/core.cljc: -------------------------------------------------------------------------------- 1 | (ns fkcss.core 2 | (:require 3 | [clojure.string :as str] 4 | [fkcss.render :as ss-render])) 5 | 6 | (def EMPTY-REGISTRY {:styles {} :fonts {} :animations {}}) 7 | (defonce ^:dynamic *registry* (atom EMPTY-REGISTRY)) 8 | 9 | (defn reg-style! [reg-key style] 10 | (swap! *registry* assoc-in [:styles reg-key] style)) 11 | 12 | (defn reg-font! [font-name font] 13 | (let [font-spec 14 | (cond->> font 15 | (map? font) 16 | (conj []) 17 | 18 | true 19 | (map #(assoc % :font-family font-name)))] 20 | (swap! *registry* assoc-in [:fonts font-name] font-spec))) 21 | 22 | (defn reg-animation! [animation-name animation-frames] 23 | (swap! *registry* assoc-in [:animations animation-name] animation-frames)) 24 | 25 | (defmacro defclass 26 | ([var-name style] 27 | (macroexpand `(defclass ~var-name nil ~style))) 28 | ([var-name doc-string style] 29 | {:pre [(symbol? var-name) (map? style)]} 30 | `(let [full-var-name# 31 | (-> (symbol ~(str *ns*) ~(name var-name)) 32 | (with-meta 33 | (cond-> ~(meta var-name) 34 | (string? ~doc-string) 35 | (assoc :doc ~doc-string)))) 36 | 37 | class-name# 38 | (-> full-var-name# str (str/replace #"[^A-Za-z0-9_-]" "-"))] 39 | (def ~var-name 40 | (do 41 | (fkcss.core/reg-style! full-var-name# {class-name# ~style}) 42 | class-name#))))) 43 | 44 | (defmacro defanimation 45 | ([var-name frames] 46 | (macroexpand `(defanimation ~var-name nil ~frames))) 47 | ([var-name doc-string frames] 48 | {:pre [(symbol? var-name) (map? frames)]} 49 | `(let [full-var-name# 50 | (-> (symbol ~(str *ns*) ~(name var-name)) 51 | (with-meta 52 | (cond-> ~(meta var-name) 53 | (string? ~doc-string) 54 | (assoc :doc ~doc-string)))) 55 | 56 | animation-name# 57 | (-> full-var-name# str (str/replace #"[^A-Za-z0-9_-]" "-"))] 58 | (def ~var-name 59 | (do 60 | (fkcss.core/reg-animation! animation-name# ~frames) 61 | animation-name#))))) 62 | 63 | (defn gen-css 64 | ([] 65 | (gen-css {})) 66 | ([opts] 67 | (str 68 | (->> @*registry* :fonts vals 69 | (map #(ss-render/render-font % opts)) 70 | str/join) 71 | (->> @*registry* :animations 72 | (map #(ss-render/render-animation (key %) (val %))) 73 | str/join) 74 | (->> @*registry* :styles vals 75 | (map #(ss-render/render-style % opts)) 76 | str/join)))) -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [![Clojars Project](https://img.shields.io/clojars/v/io.fkcss/fkcss.svg)](https://clojars.org/io.fkcss/fkcss) 2 | > [!WARNING] 3 | > This project has been archived and will no longer be maintained. There are many alternatives 4 | > on the market, including: 5 | > - [noprompt/garden](https://github.com/noprompt/garden) 6 | > - [green-coder/girouette](https://github.com/green-coder/girouette) 7 | 8 | 9 | # FkCSS 10 | Powerful styling without leaving Clojure/ClojureScript - f**k CSS. 11 | FkCSS is a minimal `CLJ->CSS` library without the weight of `CLJSS` 12 | and other alternatives. 13 | 14 | ## Features 15 | - Styles scoped by Clojure namespace 16 | - Fonts and animations via `@font-face` and `@keyframes` 17 | - Concise syntax, but more expressive property values where desired 18 | - Auto-prefixing 19 | - Custom property handlers 20 | - Only a few hundred lines of code 21 | 22 | ## Usage 23 | Most useful things are defined in `fkcss.core`, so require that in 24 | your module. 25 | ```clj 26 | (ns ... 27 | (:require 28 | [fkcss.core :as ss])) 29 | ``` 30 | 31 | Styles are represented by maps of properties and nested style maps.The key determines how FkCSS interprets a value in the style map: 32 | - Keywords ending in `>` denote a nested tag style 33 | - Keywords ending in `>>` denote a nested pseudo-element style 34 | - Keywords ending in `?` denote conditional properties 35 | - Strings denote some number of whitespace delimited classes 36 | 37 | Here's an example: 38 | ```clj 39 | {:div> 40 | {:hovered? 41 | {:color "red"} 42 | 43 | :before>> 44 | {:color "blue"} 45 | 46 | "foo bar" 47 | {:color "pink"}}} 48 | ``` 49 | Which yields: 50 | ```css 51 | div:hover { 52 | color: red; 53 | } 54 | 55 | div::before { 56 | color: blue; 57 | } 58 | 59 | div.foo.bar { 60 | color: pink; 61 | } 62 | ``` 63 | 64 | Use a vector for more concise nesting. 65 | ```clj 66 | {[:div> :before>>] 67 | {:color "blue"}} 68 | ``` 69 | Use a map for more concise sub-properties. 70 | ```clj 71 | {:div> 72 | {:margin {:left "1rem" :right "1rem"}}} 73 | ``` 74 | Yields: 75 | ```css 76 | div { 77 | margin-left: 1rem; 78 | margin-right: 1rem; 79 | } 80 | ``` 81 | 82 | ### `defclass` 83 | Use `defclass` to define namespace scoped classes, it'll bind the 84 | given var name to the name of the generated class. 85 | ```clj 86 | (ss/defclass my-class 87 | {:color "red" 88 | 89 | :hovered? 90 | {:color "blue"}}) 91 | 92 | (defn my-component [] 93 | [:div {:class my-class} 94 | "Hello"]) 95 | ``` 96 | 97 | Properties at the root of a `defclass` apply to elements with the 98 | defined class. Properties in a nested node within a `defclass` 99 | apply to elements within an element with the defined class. 100 | 101 | ### `defanimation`, `reg-animation!` 102 | Namespace scoped animations can be defined with `defanimation`, or 103 | animations with custom names can be registered with `reg-animation!`. 104 | ```clj 105 | (ns example-ns) 106 | 107 | (ss/defanimation example-1 108 | {:from {:opacity 0} 109 | :to {:opacity 1}}) 110 | 111 | (ss/reg-animation! "example-2" 112 | {0 {:opacity 0} 113 | 1 {:opacity 1}}) 114 | 115 | (ss/reg-animation "example-3" 116 | {"0%" {:opacity 0} 117 | "100%" {:opacity 1}}) 118 | ``` 119 | This yields. 120 | ```css 121 | @keyframes example-ns-example-1 { 122 | from { opacity: 0; } 123 | to { opacity: 1; } 124 | } 125 | 126 | @keyframes example-2 { 127 | 0% { opacity: 0; } 128 | 100% { opacity: 1; } 129 | } 130 | 131 | @keyframes example-3 { 132 | 0% { opacity: 0; } 133 | 100% { opacity: 1; } 134 | } 135 | ``` 136 | Nested nodes aren't allowed in animation property maps. 137 | 138 | ### `reg-font!` 139 | Add fonts to the generated CSS with `reg-font!`. 140 | ```clj 141 | (ss/reg-font! "Tangerine" 142 | [{:src "url(angerine-Regular.ttf) format('opentype')" 143 | :font-weight 400 144 | :font-style "normal"} 145 | {:src "url(Tangerine-Bold.ttf) format('opentype')" 146 | :font-weight 700 147 | :font-style "normal"}]) 148 | ``` 149 | This yields. 150 | ```css 151 | @font-face { 152 | src: url(/fonts/Tangerine-Regular.ttf) format('opentype'); 153 | font-weight: 400; 154 | font-style: normal; 155 | font-family: 'Tangerine'; 156 | } 157 | @font-face { 158 | src: url(/fonts/Tangerine-Bold.ttf) format('opentype'); 159 | font-weight: 700; 160 | font-style: normal; 161 | font-family: 'Tangerine'; 162 | } 163 | ``` 164 | A single map can be given instead of the vector when only 165 | one `@font-face` is needed. 166 | 167 | ### `reg-style!` 168 | Use `reg-style!` to register global styles. Properties at the 169 | root of such style maps apply to all elements. `reg-style!` 170 | requires a key in addition to the style map itself so it can do 171 | the right replacement/cleanup when namespaces are reloaded. 172 | ```clj 173 | (ss/reg-style! ::global 174 | {:a> 175 | {:color "blue" 176 | :text-decoration "none"}}) 177 | ``` 178 | 179 | ### `gen-css` 180 | Use `gen-css` to generate CSS for all registered styles. 181 | ```clj 182 | (def css (ss/gen-css)) 183 | ``` 184 | 185 | ### `fkcss.cljs/mount!` 186 | FkCSS can generate the CSS and add it to a `style` tag in 187 | the DOM in one go, if running in a browser. 188 | ```clj 189 | (ns ... 190 | (:require 191 | [fkcss.cljs :as ss-cljs])) 192 | 193 | (ss-cljs/mount!) 194 | ``` 195 | Use `unmount!` to remove it. 196 | 197 | ## Property Handlers 198 | Property handlers allow for custom translations from 199 | FkCSS properties to CSS properties. FkCSS comes with 200 | some builtin handlers in `fkcss.render/default-property-handlers` which handle vendor prefixing and allow for some conveniences like `margin-x/margin-y` properties. Custom handlers 201 | can be passed into `gen-css`, but be sure to merge 202 | them with the defaults if you want to keep the bultin ones. 203 | ```clj 204 | (ss/gen-css 205 | {:property-handlers 206 | (merge 207 | fkcss.render/default-property-handlers 208 | {...custom handlers...})}) 209 | ``` 210 | 211 | The map of property handlers should look like this: 212 | ```clj 213 | {:property-name 214 | (fn [property-value] 215 | {:props 216 | {:property-name property-value 217 | :-webkit-property-name property-value 218 | :-ms-property-name property-value}})} 219 | ``` 220 | Where the `:props` map in the handlers result gives the 221 | final CSS properties. 222 | 223 | ### Built-in Property Handlers 224 | - `margin-x/margin-y` shorthand 225 | - `padding-x/padding-y` shorthand 226 | - `border--radius` shorthand (`top/right/bottom/left`) 227 | - `box-shadow` map value with explicit keys `#{:offset-x :offset-y :inset? :blur-radius :spread-radius}` 228 | - Vendor prefixes for appropriate properties 229 | 230 | Example of more expressive box shadow syntax. 231 | ```clj 232 | {:box-shadow {:inset? true :offset-x 0 :offset-y 2}} 233 | ``` 234 | 235 | 236 | ## Predicates 237 | Predicates allow for conditional rules without depending on how the test is implemented. Predicates are keys ending in `?` within a style map. FkCSS has builtin predicates for the most 238 | common cases, but custom predicates can also be given in `gen-css`. 239 | ```clj 240 | (ss/gen-css 241 | {:predicates 242 | (merge 243 | fkcss.render/default-predicates 244 | {...custom predicates...})}) 245 | ``` 246 | 247 | The predicates map should look like: 248 | ```clj 249 | {:predicate-key? 250 | {:selector 251 | :exec 252 | :query }} 253 | ``` 254 | Any predicate field can be omitted, in which case it simply won't apply. 255 | 256 | The `:selector` field should give a CSS selector to limit where the conditional rules will apply. For example `:hover` or `.selected`. 257 | 258 | The `:exec` field should give a function to be executed when 259 | the CSS is being generated; if the function returns `false` 260 | then the conditional CSS simply won't be generated. 261 | 262 | The `:query` field should give a `@media` or `@supports` query 263 | to predicate the rule on. 264 | 265 | ### Built-in Predicates 266 | For CLJ and CLJS: 267 | `:hovered?`, `:active?` `:focused?`, `:focus-visible?`, 268 | `:enabled?`, `:disabled?`, `:visited?`, `:checked?`, 269 | `:expanded?`, `:current?`, `:screen-tiny?`, `:screen-small?`, 270 | `:screen-large?`, `:screen-huge?`, `:pointer-fine?`, 271 | `:pointer-coarse?`, `:pointer-none?`, `:hoverable?` 272 | 273 | For CLJS only: `:touchable?` 274 | 275 | See `fkcss.render/default-predicates` for how these or implemented 276 | and as examples for custom predicates. 277 | -------------------------------------------------------------------------------- /src/fkcss/render.cljc: -------------------------------------------------------------------------------- 1 | (ns fkcss.render 2 | (:require 3 | [clojure.string :as str] 4 | [fkcss.misc :refer [panic]])) 5 | 6 | (declare ^:dynamic ^:private *context*) 7 | 8 | (def default-property-handlers 9 | "Custom property handling, includes vendor prefixing." 10 | {:margin-x 11 | (fn [v] 12 | {:props 13 | {:margin-left v :margin-right v}}) 14 | 15 | :margin-y 16 | (fn [v] 17 | {:props 18 | {:margin-top v :margin-bottom v}}) 19 | 20 | :padding-x 21 | (fn [v] 22 | {:props 23 | {:padding-left v :padding-right v}}) 24 | 25 | :padding-y 26 | (fn [v] 27 | {:props 28 | {:padding-top v :padding-bottom v}}) 29 | 30 | :border-top-radius 31 | (fn [v] 32 | {:props 33 | {:border-top-left-radius v 34 | :border-top-right-radius v}}) 35 | 36 | :border-bottom-radius 37 | (fn [v] 38 | {:props 39 | {:border-bottom-left-radius v 40 | :border-bottom-right-radius v}}) 41 | 42 | :border-right-radius 43 | (fn [v] 44 | {:props 45 | {:border-top-right-radius v 46 | :border-bottom-right-radius v}}) 47 | 48 | :border-left-radius 49 | (fn [v] 50 | {:props 51 | {:border-top-left-radius v 52 | :border-bottom-left-radius v}}) 53 | 54 | :box-shadow 55 | (fn box-shadow-handler [v] 56 | (cond 57 | (string? v) 58 | v 59 | 60 | (map? v) 61 | (let [{:keys [offset-x offset-y blur-radius spread-radius color inset?]} v] 62 | (str 63 | (when inset? 64 | "inset ") 65 | (or offset-x 0) " " 66 | (or offset-y 0) " " 67 | (when (some? blur-radius) 68 | (str blur-radius " ")) 69 | (when (some? spread-radius) 70 | (str spread-radius " ")) 71 | (or color "black"))) 72 | 73 | (seqable? v) 74 | (str/join ", " (map box-shadow-handler v)) 75 | 76 | :else 77 | (str v))) 78 | 79 | :background-clip 80 | (fn [v] 81 | {:props 82 | (case (name v) 83 | "text" 84 | {:-webkit-background-clip v} 85 | 86 | #_else 87 | {:background-clip v})}) 88 | 89 | :box-reflect 90 | (fn [v] 91 | {:props 92 | {:-webkit-box-reflext v 93 | :box-reflect v}}) 94 | 95 | :filter 96 | (fn [v] 97 | {:props 98 | {:-webkit-filter v 99 | :filter v}}) 100 | 101 | :display 102 | (fn [v] 103 | {:props 104 | (case (name v) 105 | "flex" 106 | {:wk2/display "-webkit-box" 107 | :wk/display "-webkit-flexbox" 108 | :ms/display "-ms-flexbox" 109 | :display "flex"} 110 | 111 | "grid" 112 | {:ms/display "-ms-grid" 113 | :display "grid"} 114 | 115 | #_else 116 | {:display v})}) 117 | 118 | :flex 119 | (fn [v] 120 | {:props 121 | {:-webkit-box-flex v 122 | :width "10%" ; for old syntax, otherwise collapses (from shouldiprefix.com#flexbox) 123 | :-webkit-flex v 124 | :-ms-flex v 125 | :flex v}}) 126 | 127 | :font-feature-settings 128 | (fn [v] 129 | {:props 130 | {:-webkit-font-feature-settings v 131 | :-moz-font-feature-settings v 132 | :font-feature-settings v}}) 133 | 134 | :hyphens 135 | (fn [v] 136 | {:props 137 | {:-webkit-hyphens v 138 | :-moz-hyphens v 139 | :-ms-hyphens v 140 | :hyphens v}}) 141 | 142 | :word-break 143 | (fn [v] 144 | {:props 145 | {:-ms-word-break v 146 | :word-break v}}) 147 | 148 | :mask-image 149 | (fn [v] 150 | {:props 151 | {:-webkit-mask-image v 152 | :mask-image v}}) 153 | 154 | :column-count 155 | (fn [v] 156 | {:props 157 | {:-webkit-column-count v 158 | :-moz-column-count v 159 | :-column-count v}}) 160 | 161 | :column-gap 162 | (fn [v] 163 | {:props 164 | {:-webkit-column-gap v 165 | :-moz-column-gap v 166 | :column-gap v}}) 167 | 168 | :column-rule 169 | (fn [v] 170 | {:props 171 | {:-webkit-column-rule v 172 | :-moz-column-rule v 173 | :column-rule v}}) 174 | 175 | :object-fit 176 | (fn [v] 177 | {:props 178 | {:-o-object-fit v 179 | :object-fit v}}) 180 | 181 | :transform 182 | (fn [v] 183 | {:props 184 | {:-webkit-transform v 185 | :-ms-transform v 186 | :transform v}}) 187 | 188 | :appearance 189 | (fn [v] 190 | {:props 191 | {:-webkit-appearance v 192 | :-moz-appearance v 193 | :appearance v}}) 194 | 195 | :font-family 196 | (fn font-family-handler [v] 197 | {:props 198 | {:font-family 199 | (cond 200 | (string? v) 201 | (if (or (str/includes? v "'") (str/includes? v "\"") (str/includes? v ",") (not (re-find #"\W" v))) 202 | v 203 | (str "'" v "'")) 204 | 205 | (sequential? v) 206 | (str/join ", " (map font-family-handler v)) 207 | 208 | :else 209 | (str v))}})}) 210 | 211 | (def default-predicates 212 | (merge 213 | {:hovered? {:selector ":hover"} 214 | :active? {:selector ":active"} 215 | :focused? {:selector ":focus"} 216 | :focus-visible? {:selector ":focus-visible"} 217 | :enabled? {:selector ":enabled"} 218 | :disabled? {:selector ":disabled"} 219 | :visited? {:selector ":visited"} 220 | :checked? {:selector ":checked"} 221 | :expanded? {:selector "[aria-expanded=\"true\"]"} 222 | :current? {:selector "[aria-current]"} 223 | :screen-tiny? {:query "@media (max-width: 480px)"} 224 | :screen-small? {:query "@media (max-width: 768px)"} 225 | :screen-large? {:query "@media (min-width: 1025px)"} 226 | :screen-huge? {:query "@media (min-width: 1200px)"} 227 | :pointer-fine? {:query "@media (pointer: fine)"} 228 | :pointer-coarse? {:query "@media (pointer: coarse)"} 229 | :pointer-none? {:query "@media (pointer: none)"} 230 | :hoverable? {:query "@media (hover: hover)"}} 231 | #?(:cljs 232 | {:touchable? 233 | {:exec 234 | (fn is-touch-device? [] 235 | (or 236 | (js-in js/window "ontouchstart") 237 | (< 0 js/navigator.maxTouchPoints) 238 | (< 0 js/navigator.msMaxTouchPoints)))}}))) 239 | 240 | (def ^:private ^:dynamic *context* 241 | {:property-handlers default-property-handlers 242 | :predicates default-predicates}) 243 | 244 | (defn- resolve-properties [props] 245 | (into {} 246 | (mapcat 247 | (fn [[prop-key prop-val]] 248 | (or 249 | (when-let [handler (get-in *context* [:property-handlers prop-key])] 250 | (:props (handler prop-val))) 251 | (when (map? prop-val) 252 | (resolve-properties 253 | (into {} 254 | (map 255 | (fn [[sub-key sub-val]] 256 | [(keyword (str (name prop-key) "-" (name sub-key))) sub-val]) 257 | prop-val)))) 258 | [[prop-key (str prop-val)]])) 259 | props))) 260 | 261 | (defn- drop-chars [s n] 262 | (subs s 0 (- (count s) n))) 263 | 264 | (defn- tag-selector-key? [k] 265 | (and 266 | (keyword? k) 267 | (let [k-name (name k)] 268 | (and 269 | (not (str/ends-with? k-name ">>")) 270 | (str/ends-with? k-name ">"))))) 271 | 272 | (defn- predicate-selector-key? [k] 273 | (and 274 | (keyword? k) 275 | (-> k name (str/ends-with? "?")))) 276 | 277 | (defn- classes-selector-key? [k] 278 | (string? k)) 279 | 280 | (defn- pseudo-el-selector-key? [k] 281 | (and 282 | (keyword? k) 283 | (-> k name (str/ends-with? ">>")))) 284 | 285 | (defn- prop-key? [k] 286 | (and 287 | (keyword? k) 288 | (not 289 | (or 290 | (tag-selector-key? k) 291 | (predicate-selector-key? k) 292 | (classes-selector-key? k) 293 | (pseudo-el-selector-key? k))))) 294 | 295 | (defn- select-style-props [style] 296 | (into {} 297 | (keep 298 | (fn [[k v :as kv]] 299 | (when (prop-key? k) 300 | kv)) 301 | style))) 302 | 303 | (defn- select-style-nested [style] 304 | (into {} 305 | (keep 306 | (fn [[k v :as kv]] 307 | (when-not (prop-key? k) 308 | kv)) 309 | style))) 310 | 311 | (defn- update-last [v f & args] 312 | (let [new-v (apply f (last v) args)] 313 | (-> v pop (conj new-v)))) 314 | 315 | (defn- resolve-selector-next [resolved-path next-selector-key] 316 | (cond 317 | (tag-selector-key? next-selector-key) 318 | (conj resolved-path {:tag (-> next-selector-key name (drop-chars 1))}) 319 | 320 | (pseudo-el-selector-key? next-selector-key) 321 | (do 322 | (when (empty? resolved-path) 323 | (panic "unnested pseudo-element selector" {:key next-selector-key})) 324 | (let [pseudo-el-name (-> next-selector-key name (drop-chars 2))] 325 | (update-last resolved-path 326 | (fn [last-selector] 327 | (when-let [parent-pseudo-el-name (:pseudo-el last-selector)] 328 | (panic "nested pseudo-selectors" {:child pseudo-el-name :parent parent-pseudo-el-name})) 329 | (assoc last-selector :pseudo-el pseudo-el-name))))) 330 | 331 | (classes-selector-key? next-selector-key) 332 | (let [classes (-> next-selector-key (str/split #"\s+") vec)] 333 | (conj resolved-path {:classes classes})) 334 | 335 | (predicate-selector-key? next-selector-key) 336 | (let [predicate (get-in *context* [:predicates next-selector-key])] 337 | (when (and (:selector predicate) (empty? resolved-path)) 338 | (panic "selector predicate not nested" {:key next-selector-key})) 339 | (if (empty? resolved-path) 340 | [{:predicates #{predicate}}] 341 | (update-last resolved-path update :predicates (fnil conj #{}) predicate))) 342 | 343 | (vector? next-selector-key) 344 | (reduce 345 | (fn [m inner-next-selector-key] 346 | (resolve-selector-next m inner-next-selector-key)) 347 | resolved-path 348 | next-selector-key))) 349 | 350 | (defn- resolve-selectors-inner [resolved-path style] 351 | (into [[resolved-path (select-style-props style)]] 352 | (mapcat 353 | (fn [[k v]] 354 | (resolve-selectors-inner (resolve-selector-next resolved-path k) v)) 355 | (select-style-nested style)))) 356 | 357 | (defn- resolve-selectors [style] 358 | (resolve-selectors-inner [] style)) 359 | 360 | (defn- resolved-path->queries [resolved-path] 361 | (->> resolved-path 362 | (mapcat :predicates) 363 | (keep :query) 364 | set)) 365 | 366 | (defn- indentation [n] 367 | (str/join (repeat (* 2 n) " "))) 368 | 369 | (defn- render-css-selectors [resolved-path] 370 | (let [selectors 371 | (->> resolved-path 372 | (map 373 | (fn [{:keys [tag classes predicates pseudo-el]}] 374 | (str 375 | tag 376 | (when (seq classes) 377 | (str "." (str/join "." classes))) 378 | (str/join (keep :selector predicates)) 379 | (when pseudo-el 380 | (str "::" pseudo-el))))) 381 | (str/join " "))] 382 | (if (str/blank? selectors) 383 | "*" 384 | selectors))) 385 | 386 | (defn- render-css-props [level props] 387 | (->> props 388 | resolve-properties 389 | (map 390 | (fn [[k v]] 391 | (str (indentation level) (name k) ": " v ";\n"))) 392 | str/join)) 393 | 394 | (defn- render-css-rule [level resolved-path props] 395 | (str 396 | (indentation level) (render-css-selectors resolved-path) " {\n" 397 | (render-css-props (inc level) props) 398 | (indentation level) "}\n")) 399 | 400 | (defn- wrapped-in-queries [level queries styles] 401 | (cond 402 | (seq queries) 403 | (str 404 | (indentation level) (first queries) " {\n" 405 | (wrapped-in-queries (inc level) (rest queries) styles) 406 | (indentation level) "}\n") 407 | 408 | :else 409 | (->> styles 410 | (map 411 | (fn [[resolved-path props]] 412 | (render-css-rule level resolved-path props))) 413 | (str/join)))) 414 | 415 | (defn- exec-predicates [resolved-path] 416 | (->> resolved-path 417 | (mapcat :predicates) 418 | (keep :exec) 419 | (reduce 420 | (fn [acc exec-fn] 421 | (and acc (exec-fn))) 422 | true))) 423 | 424 | (defn render-style [style {:keys [property-handlers predicates]}] 425 | (binding [*context* 426 | {:property-handlers (or property-handlers default-property-handlers) 427 | :predicates (or predicates default-predicates) 428 | :rendering :style}] 429 | (->> style 430 | resolve-selectors 431 | (filter #(and (seq (second %)) (exec-predicates (first %)))) 432 | (group-by (comp resolved-path->queries first)) 433 | (sort-by #(count (first %))) ; rules with queries come after to ensure correct precedence 434 | (map #(wrapped-in-queries 0 (seq (first %)) (second %))) 435 | (str/join "\n")))) 436 | 437 | (defn render-font [font-spec {:keys [property-handlers]}] 438 | (binding [*context* 439 | {:property-handlers (or property-handlers default-property-handlers) 440 | :predicates {} 441 | :rendering :font}] 442 | (->> font-spec 443 | (map 444 | (fn [font-props] 445 | (str 446 | "@font-face {\n" 447 | (render-css-props 1 font-props) 448 | "}\n"))) 449 | str/join))) 450 | 451 | (defn render-animation [animation-name animation-frames] 452 | (str 453 | "@keyframes " animation-name " {\n" 454 | (->> animation-frames 455 | (map 456 | (fn [[k props]] 457 | (str 458 | (indentation 1) 459 | (cond 460 | (number? k) 461 | (str (* 100 k) "%") 462 | 463 | (keyword? k) 464 | (name k) 465 | 466 | :else 467 | k) 468 | " {\n" 469 | (render-css-props 2 props) 470 | (indentation 1) "}\n"))) 471 | str/join) 472 | "}\n")) --------------------------------------------------------------------------------