├── .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 | [](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"))
--------------------------------------------------------------------------------