Bundle garden to avoid source conflicts with garden 2.0.0

This commit is contained in:
Daniel Compton 2018-01-27 18:44:45 +13:00
parent 547822e7d9
commit f20dd1b0d0
19 changed files with 3419 additions and 12 deletions

View File

@ -7,7 +7,6 @@
[reagent "0.6.0" :scope "provided"] [reagent "0.6.0" :scope "provided"]
[re-frame "0.10.3" :scope "provided"] [re-frame "0.10.3" :scope "provided"]
[binaryage/devtools "0.9.4"] [binaryage/devtools "0.9.4"]
[garden "1.3.3"]
[cljsjs/react-flip-move "2.9.17-0"]] [cljsjs/react-flip-move "2.9.17-0"]]
:plugins [[thomasa/mranderson "0.4.7"] :plugins [[thomasa/mranderson "0.4.7"]
[lein-less "RELEASE"]] [lein-less "RELEASE"]]
@ -46,4 +45,6 @@
cljsjs/react-dom-server cljsjs/react-dom-server
cljsjs/create-react-class cljsjs/create-react-class
org.clojure/tools.logging org.clojure/tools.logging
net.cgrand/macrovich]]]}}) net.cgrand/macrovich]]
^:source-dep [garden "1.3.3"
:exclusions [com.yahoo.platform.yui/yuicompressor]]]}})

View File

@ -5,4 +5,5 @@ lein with-profile mranderson source-deps
# Then delete the META-INF directories # Then delete the META-INF directories
rm -r target/srcdeps/mranderson047/reagent/v0v7v0/META-INF rm -r target/srcdeps/mranderson047/reagent/v0v7v0/META-INF
rm -r target/srcdeps/mranderson047/re-frame rm -r target/srcdeps/mranderson047/re-frame
rm -r target/srcdeps/mranderson047/garden/v1v3v3/META-INF
cp -r target/srcdeps/mranderson047 src cp -r target/srcdeps/mranderson047 src

View File

@ -1,6 +1,6 @@
(ns day8.re-frame.trace.common-styles (ns day8.re-frame.trace.common-styles
(:require [garden.units :refer [px em]] (:require [mranderson047.garden.v1v3v3.garden.units :refer [px em]]
[garden.compiler :refer [render-css]])) [mranderson047.garden.v1v3v3.garden.compiler :refer [render-css]]))
;; TODO: Switch these to BM (or just use BM defs if available) ;; TODO: Switch these to BM (or just use BM defs if available)

View File

@ -1,9 +1,9 @@
(ns day8.re-frame.trace.styles (ns day8.re-frame.trace.styles
(:require-macros [day8.re-frame.trace.utils.macros :as macros]) (:require-macros [day8.re-frame.trace.utils.macros :as macros])
(:require [garden.core :as garden] (:require [mranderson047.garden.v1v3v3.garden.core :as garden]
[garden.units :refer [em px percent]] [mranderson047.garden.v1v3v3.garden.units :refer [em px percent]]
[garden.color :as color] [mranderson047.garden.v1v3v3.garden.color :as color]
[garden.selectors :as s] [mranderson047.garden.v1v3v3.garden.selectors :as s]
[day8.re-frame.trace.common-styles :as common] [day8.re-frame.trace.common-styles :as common]
[day8.re-frame.trace.utils.re-com :as rc] [day8.re-frame.trace.utils.re-com :as rc]
[day8.re-frame.trace.view.app-db :as app-db] [day8.re-frame.trace.view.app-db :as app-db]

View File

@ -10,8 +10,8 @@
[day8.re-frame.trace.view.timing :as timing] [day8.re-frame.trace.view.timing :as timing]
[day8.re-frame.trace.view.debug :as debug] [day8.re-frame.trace.view.debug :as debug]
[day8.re-frame.trace.view.settings :as settings] [day8.re-frame.trace.view.settings :as settings]
[garden.core :refer [css style]] [mranderson047.garden.v1v3v3.garden.core :refer [css style]]
[garden.units :refer [px]] [mranderson047.garden.v1v3v3.garden.units :refer [px]]
[re-frame.trace] [re-frame.trace]
[day8.re-frame.trace.utils.re-com :as rc] [day8.re-frame.trace.utils.re-com :as rc]
[day8.re-frame.trace.common-styles :as common])) [day8.re-frame.trace.common-styles :as common]))

View File

@ -3,8 +3,8 @@
[mranderson047.reagent.v0v7v0.reagent.core :as r] [mranderson047.reagent.v0v7v0.reagent.core :as r]
[day8.re-frame.trace.utils.re-com :as rc :refer [css-join]] [day8.re-frame.trace.utils.re-com :as rc :refer [css-join]]
[day8.re-frame.trace.common-styles :as common] [day8.re-frame.trace.common-styles :as common]
[garden.units :as units] [mranderson047.garden.v1v3v3.garden.units :as units]
[garden.compiler :refer [render-css]])) [mranderson047.garden.v1v3v3.garden.compiler :refer [render-css]]))
(def comp-section-width "400px") (def comp-section-width "400px")
(def instruction--section-width "190px") (def instruction--section-width "190px")

View File

@ -0,0 +1,92 @@
(ns mranderson047.garden.v1v3v3.garden.arithmetic
"Generic arithmetic operators for computing sums, differences,
products, and quotients between CSSUnits, CSSColors, and numbers."
(:refer-clojure :exclude [+ - * /])
(:require [mranderson047.garden.v1v3v3.garden.units :as u :refer [unit?]]
[mranderson047.garden.v1v3v3.garden.color :as c :refer [color?]]))
;; The motivation for the functions in this namespace is the
;; contention that working with unit arithmetic functions (`px+`,
;; `px-`, etc.) and color arithmetic functions (`color+`, `color-`,
;; etc.) can at times be a bit unweildly. In some cases it would be
;; nice to have functions which could transparently perform unit and
;; color math without the verbosity.
;; Here, such functions are provided.
;; All operations favor `CSSUnit` and `CSSColor` types and fall back to
;; the standard `clojure.core` arithmetic functions. The preference for
;; this order stems from the assertion that it is far more likely unit
;; arithmetic will be performed in the context of a stylesheet versus
;; color or numeric.
(defn +
"Generic addition operator. Transparently computes the sum of
`CSSUnit`s,`CSSColor`s, and numbers."
([] 0)
([x] x)
([x y]
(cond
(unit? x) ((u/make-unit-adder (:unit x)) x y)
(color? x) (c/color+ x y)
:else (if (or (unit? y) (color? y))
(+ y x)
(clojure.core/+ x y))))
([x y & more]
(reduce + (+ x y) more)))
(defn -
"Generic subtraction operator. Transparently computes the difference
between `CSSUnit`s, `CSSColor`s, and numbers."
([x]
(cond
(unit? x) (update-in x [:magnitude] clojure.core/-)
;; Colors shouldn't have negative semantics.
(color? x) x
:else (clojure.core/- x)))
([x y]
(cond
(unit? x) ((u/make-unit-subtractor (:unit x)) x y)
(color? x) (c/color- x y)
:else (cond
(unit? y) (let [{m :magnitude} y]
(assoc y :magnitude (clojure.core/- x m)))
(color? y) (c/color- x y)
:else (clojure.core/- x y))))
([x y & more]
(reduce - (- x y) more)))
(defn *
"Generic multiplication operation. Transparently computes the product
between `CSSUnit`s, `CSSColor`s, and numbers."
([] 1)
([x] x)
([x y]
(cond
(unit? x) ((u/make-unit-multiplier (:unit x)) x y)
(color? x) (c/color* x y)
:else (if (or (unit? y) (color? y))
(* y x)
(clojure.core/* x y))))
([x y & more]
(reduce * (* x y) more)))
(defn /
"Generic division operation. Transparently computes the quotient
between `CSSUnit`s, `CSSColor`s, and numbers."
([x]
(cond
(unit? x) (update-in x [:magnitude] clojure.core//)
(color? x) (c/color-div x)
:else (clojure.core// x)))
([x y]
(cond
(unit? x) ((u/make-unit-divider (:unit x)) x y)
(color? x) (c/color-div x y)
:else (cond
(unit? y) (let [{m :magnitude} y]
(assoc y :magnitude (clojure.core// x m)))
(color? y) (c/color-div x y)
:else (clojure.core// x y))))
([x y & more]
(reduce / (/ x y) more)))

View File

@ -0,0 +1,619 @@
(ns mranderson047.garden.v1v3v3.garden.color
"Utilities for color creation, conversion, and manipulation."
(:refer-clojure :exclude [complement])
#?(:cljs
(:require-macros
[mranderson047.garden.v1v3v3.garden.color :refer [defcolor-operation]]))
(:require
[clojure.string :as string]
[mranderson047.garden.v1v3v3.garden.util :as util])
#?(:clj
(:import clojure.lang.IFn)))
;; Many of the functions in this namespace were ported or inspired by
;; the implementations included with Sass
;; (http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html).
;; Some additional functions have been added such as `triad` and
;; `tetrad` for generating sets of colors.
;; Converts a color to a hexadecimal string (implementation below).
(declare as-hex)
(defrecord CSSColor [red green blue hue saturation lightness alpha]
IFn
#?(:clj
(invoke [this] this))
#?(:clj
(invoke [this k]
(get this k)))
#?(:clj
(invoke [this k missing]
(get this k missing)))
#?(:cljs
(-invoke [this] this))
#?(:cljs
(-invoke [this k]
(get this k)))
#?(:cljs
(-invoke [this k missing]
(get this k missing)))
#?(:clj
(applyTo [this args]
(clojure.lang.AFn/applyToHelper this args))))
(def as-color map->CSSColor)
(defn rgb
"Create an RGB color."
([[r g b :as vs]]
(if (every? #(util/between? % 0 255) vs)
(as-color {:red r :green g :blue b})
(throw
(ex-info "RGB values must be between 0 and 255" {}))))
([r g b]
(rgb [r g b])))
(defn rgba
"Create an RGBA color."
([[r g b a]]
(if (util/between? a 0 1)
(as-color (assoc (rgb [r g b]) :alpha a))
(throw
(ex-info "Alpha value must be between 0 and 1" {}))))
([r g b a]
(rgba [r g b a])))
(defn hsl
"Create an HSL color."
([[h s l]]
;; Handle CSSUnits.
(let [[h s l] (map #(get % :magnitude %) [h s l])]
(if (and (util/between? s 0 100)
(util/between? l 0 100))
(as-color {:hue (mod h 360) :saturation s :lightness l})
(throw
(ex-info "Saturation and lightness must be between 0(%) and 100(%)" {})))))
([h s l]
(hsl [h s l])))
(defn hsla
"Create an HSLA color."
([[h s l a]]
(if (util/between? a 0 1)
(as-color (assoc (hsl [h s l]) :alpha a))
(throw
(ex-info "Alpha value must be between 0 and 1" {}))))
([h s l a]
(hsla [h s l a])))
(defn rgb?
"Return true if color is an RGB color."
[color]
(and (map? color)
(every? color #{:red :green :blue})))
(defn hsl?
"Return true if color is an HSL color."
[color]
(and (map? color)
(every? color #{:hue :saturation :lightness})))
(defn color?
"Return true if x is a color."
[x]
(or (rgb? x) (hsl? x)))
(def ^{:doc "Regular expression for matching a hexadecimal color.
Matches hexadecimal colors of length three or six possibly
lead by a \"#\". The color portion is captured."}
;; Quantifier must be in this order or JavaScript engines will match
;; 3 chars even when 6 are provided (failing re-matches).
hex-re #"#?([\da-fA-F]{6}|[\da-fA-F]{3})")
(defn hex?
"Returns true if x is a hexadecimal color."
[x]
(boolean (and (string? x) (re-matches hex-re x))))
(defn hex->rgb
"Convert a hexadecimal color to an RGB color map."
[s]
(when-let [[_ hex] (re-matches hex-re s)]
(let [hex (if (= 3 (count hex))
(apply str (mapcat #(list % %) hex))
hex)]
(->> (re-seq #"[\da-fA-F]{2}" hex)
(map #(util/string->int % 16))
(rgb)))))
(defn rgb->hex
"Convert an RGB color map to a hexadecimal color."
[{r :red g :green b :blue}]
(letfn [(hex-part [v]
(-> (util/format "%2s" (util/int->string v 16))
(string/replace " " "0")))]
(apply str "#" (map hex-part [r g b]))))
(defn trim-one [x]
(if (< 1 x) 1 x))
(defn rgb->hsl
"Convert an RGB color map to an HSL color map."
[{:keys [red green blue] :as color}]
(if (hsl? color)
color
(let [[r g b] (map #(/ % 255) [red green blue])
mx (max r g b)
mn (min r g b)
d (- mx mn)
h (condp = mx
mn 0
r (* 60 (/ (- g b) d))
g (+ (* 60 (/ (- b r) d)) 120)
b (+ (* 60 (/ (- r g) d)) 240))
l (trim-one (/ (+ mx mn) 2))
s (trim-one
(cond
(= mx mn) 0
(< l 0.5) (/ d (* 2 l))
:else (/ d (- 2 (* 2 l)))))]
(hsl (mod h 360) (* 100 s) (* 100 l)))))
(declare hue->rgb)
;; SEE: http://www.w3.org/TR/css3-color/#hsl-color.
(defn hsl->rgb
"Convert an HSL color map to an RGB color map."
[{:keys [hue saturation lightness] :as color}]
(if (rgb? color)
color
(let [h (/ hue 360.0)
s (/ saturation 100.0)
l (/ lightness 100.0)
m2 (if (<= l 0.5)
(* l (inc s))
(- (+ l s) (* l s)))
m1 (- (* 2 l) m2)
[r g b] (map #(Math/round (* % 0xff))
[(hue->rgb m1 m2 (+ h (/ 1.0 3)))
(hue->rgb m1 m2 h)
(hue->rgb m1 m2 (- h (/ 1.0 3)))])]
(rgb [r g b]))))
(defn- hue->rgb
[m1 m2 h]
(let [h (cond
(< h 0) (inc h)
(> h 1) (dec h)
:else h)]
(cond
(< (* 6 h) 1) (+ m1 (* (- m2 m1) h 6))
(< (* 2 h) 1) m2
(< (* 3 h) 2) (+ m1 (* (- m2 m1) (- (/ 2.0 3) h) 6))
:else m1)))
(defn hsl->hex
"Convert an HSL color map to a hexadecimal string."
[color]
(-> color hsl->rgb rgb->hex))
(defn hex->hsl
"Convert a hexadecimal color to an HSL color."
[color]
(-> color hex->rgb rgb->hsl))
(def percent-clip
(partial util/clip 0 100))
(def rgb-clip
(partial util/clip 0 255))
(defn as-hex
"Convert a color to a hexadecimal string."
[x]
(cond
(hex? x) x
(rgb? x) (rgb->hex x)
(hsl? x) (hsl->hex x)
:else (throw (ex-info (str "Can't convert " x " to a color.") {}))))
(defn as-rgb
"Convert a color to a RGB."
[x]
(cond
(rgb? x) x
(hsl? x) (hsl->rgb x)
(hex? x) (hex->rgb x)
(number? x) (rgb (map rgb-clip [x x x]))
:else (throw (ex-info (str "Can't convert " x " to a color.") {}))))
(defn as-hsl
"Convert a color to a HSL."
[x]
(cond
(hsl? x) x
(rgb? x) (rgb->hsl x)
(hex? x) (hex->hsl x)
(number? x) (hsl [x (percent-clip x) (percent-clip x)])
:else (throw (ex-info (str "Can't convert " x " to a color.") {}))))
(defn- restrict-rgb
[m]
(select-keys m [:red :green :blue]))
(defn- make-color-operation
[op]
(fn color-op
([a] a)
([a b]
(let [o (comp rgb-clip op)
a (restrict-rgb (as-rgb a))
b (restrict-rgb (as-rgb b))]
(as-color (merge-with o a b))))
([a b & more]
(reduce color-op (color-op a b) more))))
#?(:clj
(defmacro ^:private defcolor-operation [name operator]
`(def ~name (make-color-operation ~operator))))
(defcolor-operation
^{:doc "Add the RGB components of two or more colors."
:arglists '([a] [a b] [a b & more])}
color+ +)
(defcolor-operation
^{:doc "Subtract the RGB components of two or more colors."
:arglists '([a] [a b] [a b & more])}
color- -)
(defcolor-operation
^{:doc "Multiply the RGB components of two or more colors."
:arglists '([a] [a b] [a b & more])}
color* *)
(defcolor-operation
^{:doc "Multiply the RGB components of two or more colors."
:arglists '([a] [a b] [a b & more])}
color-div /)
(defn- update-color [color field f v]
(let [v (or (:magnitude v) v)]
(update-in (as-hsl color) [field] f v)))
(defn rotate-hue
"Rotates the hue value of a given color by amount."
[color amount]
(update-color color :hue (comp #(mod % 360) +) amount))
(defn saturate
"Increase the saturation value of a given color by amount."
[color amount]
(update-color color :saturation (comp percent-clip +) amount))
(defn desaturate
"Decrease the saturation value of a given color by amount."
[color amount]
(update-color color :saturation (comp percent-clip -) amount))
(defn lighten
"Increase the lightness value a given color by amount."
[color amount]
(update-color color :lightness (comp percent-clip +) amount))
(defn darken
"Decrease the lightness value a given color by amount."
[color amount]
(update-color color :lightness (comp percent-clip -) amount))
(defn invert
"Return the inversion of a color."
[color]
(as-color (merge-with - {:red 255 :green 255 :blue 255} (as-rgb color))))
(defn mix
"Mix two or more colors by averaging their RGB channels."
([color-1 color-2]
(let [c1 (restrict-rgb (as-rgb color-1))
c2 (restrict-rgb (as-rgb color-2))]
(as-color (merge-with util/average c1 c2))))
([color-1 color-2 & more]
(reduce mix (mix color-1 color-2) more)))
;;;; Color wheel functions.
(defn complement
"Return the complement of a color."
[color]
(rotate-hue color 180))
(defn- hue-rotations
([color & amounts]
(map (partial rotate-hue color) amounts)))
(defn analogous
"Given a color return a triple of colors which are 0, 30, and 60
degrees clockwise from it. If a second falsy argument is passed the
returned values will be in a counter-clockwise direction."
([color]
(analogous color true))
([color clockwise?]
(let [sign (if clockwise? + -)]
(hue-rotations color 0 (sign 30) (sign 60)))))
(defn triad
"Given a color return a triple of colors which are equidistance apart
on the color wheel."
[color]
(hue-rotations color 0 120 240))
(defn split-complement
"Given a color return a triple of the color and the two colors on
either side of it's complement."
([color]
(split-complement color 130))
([color distance-from-complement]
(let [d (util/clip 1 179 distance-from-complement)]
(hue-rotations color 0 d (- d)))))
(defn tetrad
"Given a color return a quadruple of four colors which are
equidistance on the color wheel (ie. a pair of complements). An
optional angle may be given for color of the second complement in the
pair (this defaults to 90 when only color is passed)."
([color]
(tetrad color 90))
([color angle]
(let [a (util/clip 1 90 (Math/abs (:magnitude angle angle)))
color-2 (rotate-hue color a)]
[(rotate-hue color 0)
(complement color)
color-2
(complement color-2)])))
(defn shades
"Given a color return a list of shades from lightest to darkest by
a step. By default the step is 10. White and black are excluded from
the returned list."
([color]
(shades color 10))
([color step]
(let [c (as-hsl color)]
(for [i (range 1 (Math/floor (/ 100.0 step)))]
(assoc c :lightness (* i step))))))
;; ---------------------------------------------------------------------
;; CSS color name conversion
(def color-name->hex
{:aquamarine "#7fffd4"
:aliceblue "#f0f8ff"
:antiquewhite "#faebd7"
:aqua "#00ffff"
:azure "#f0ffff"
:beige "#f5f5dc"
:bisque "#ffe4c4"
:black "#000000"
:blanchedalmond "#ffebcd"
:blue "#0000ff"
:blueviolet "#8a2be2"
:brown "#a52a2a"
:burlywood "#deb887"
:cadetblue "#5f9ea0"
:chartreuse "#7fff00"
:chocolate "#d2691e"
:coral "#ff7f50"
:cornflowerblue "#6495ed"
:cornsilk "#fff8dc"
:crimson "#dc143c"
:cyan "#00ffff"
:darkblue "#00008b"
:darkcyan "#008b8b"
:darkgoldenrod "#b8860b"
:darkgray "#a9a9a9"
:darkgreen "#006400"
:darkgrey "#a9a9a9"
:darkkhaki "#bdb76b"
:darkmagenta "#8b008b"
:darkolivegreen "#556b2f"
:darkorange "#ff8c00"
:darkorchid "#9932cc"
:darkred "#8b0000"
:darksalmon "#e9967a"
:darkseagreen "#8fbc8f"
:darkslateblue "#483d8b"
:darkslategray "#2f4f4f"
:darkslategrey "#2f4f4f"
:darkturquoise "#00ced1"
:darkviolet "#9400d3"
:deeppink "#ff1493"
:deepskyblue "#00bfff"
:dimgray "#696969"
:dimgrey "#696969"
:dodgerblue "#1e90ff"
:firebrick "#b22222"
:floralwhite "#fffaf0"
:forestgreen "#228b22"
:fuchsia "#ff00ff"
:gainsboro "#dcdcdc"
:ghostwhite "#f8f8ff"
:gold "#ffd700"
:goldenrod "#daa520"
:gray "#808080"
:green "#008000"
:greenyellow "#adff2f"
:honeydew "#f0fff0"
:hotpink "#ff69b4"
:indianred "#cd5c5c"
:indigo "#4b0082"
:ivory "#fffff0"
:khaki "#f0e68c"
:lavender "#e6e6fa"
:lavenderblush "#fff0f5"
:lawngreen "#7cfc00"
:lemonchiffon "#fffacd"
:lightblue "#add8e6"
:lightcoral "#f08080"
:lightcyan "#e0ffff"
:lightgoldenrodyellow "#fafad2"
:lightgray "#d3d3d3"
:lightgreen "#90ee90"
:lightgrey "#d3d3d3"
:lightpink "#ffb6c1"
:lightsalmon "#ffa07a"
:lightseagreen "#20b2aa"
:lightskyblue "#87cefa"
:lightslategray "#778899"
:lightslategrey "#778899"
:lightsteelblue "#b0c4de"
:lightyellow "#ffffe0"
:lime "#00ff00"
:limegreen "#32cd32"
:linen "#faf0e6"
:magenta "#ff00ff"
:maroon "#800000"
:mediumaquamarine "#66cdaa"
:mediumblue "#0000cd"
:mediumorchid "#ba55d3"
:mediumpurple "#9370db"
:mediumseagreen "#3cb371"
:mediumslateblue "#7b68ee"
:mediumspringgreen "#00fa9a"
:mediumturquoise "#48d1cc"
:mediumvioletred "#c71585"
:midnightblue "#191970"
:mintcream "#f5fffa"
:mistyrose "#ffe4e1"
:moccasin "#ffe4b5"
:navajowhite "#ffdead"
:navy "#000080"
:oldlace "#fdf5e6"
:olive "#808000"
:olivedrab "#6b8e23"
:orange "#ffa500"
:orangered "#ff4500"
:orchid "#da70d6"
:palegoldenrod "#eee8aa"
:palegreen "#98fb98"
:paleturquoise "#afeeee"
:palevioletred "#db7093"
:papayawhip "#ffefd5"
:peachpuff "#ffdab9"
:peru "#cd853f"
:pink "#ffc0cb"
:plum "#dda0dd"
:powderblue "#b0e0e6"
:purple "#800080"
:red "#ff0000"
:rosybrown "#bc8f8f"
:royalblue "#4169e1"
:saddlebrown "#8b4513"
:salmon "#fa8072"
:sandybrown "#f4a460"
:seagreen "#2e8b57"
:seashell "#fff5ee"
:sienna "#a0522d"
:silver "#c0c0c0"
:skyblue "#87ceeb"
:slateblue "#6a5acd"
:slategray "#708090"
:slategrey "#708090"
:snow "#fffafa"
:springgreen "#00ff7f"
:steelblue "#4682b4"
:tan "#d2b48c"
:teal "#008080"
:thistle "#d8bfd8"
:tomato "#ff6347"
:turquoise "#40e0d0"
:violet "#ee82ee"
:wheat "#f5deb3"
:white "#ffffff"
:whitesmoke "#f5f5f5"
:yellow "#ffff00"
:yellowgreen "#9acd32"})
(defn- ex-info-color-name
"Helper function for from-name. Returns an instance of ExceptionInfo
for unknown colors."
[n]
(ex-info
(str "Unknown color " (pr-str n) " see (:expected (ex-data e)) for a list of color names")
{:given n
:expected (set (keys color-name->hex))}))
(def
^{:private true
:doc "Helper function for from-name."}
color-name->color
(memoize (fn [k] (color-name->hex k))))
(defn from-name
"Given a CSS color name n return an instance of CSSColor."
[n]
(if-let [h (color-name->color (keyword n))]
h
(throw (ex-info-color-name n))))
(defn- scale-color-value
[value amount]
(+ value (if (pos? amount)
(* (- 100 value) (/ amount 100))
(/ (* value amount) 100))))
(defn scale-lightness
"Scale the lightness of a color by amount"
[color amount]
(update-color color :lightness scale-color-value amount))
(defn scale-saturation
"Scale the saturation of a color by amount"
[color amount]
(update-color color :saturation scale-color-value amount))
(defn- decrown-hex [hex]
(string/replace hex #"^#" ""))
(defn- crown-hex [hex]
(if (re-find #"^#" hex)
hex
(str "#" hex)))
(defn- expand-hex
"(expand-hex \"#abc\") -> \"aabbcc\"
(expand-hex \"333333\") -> \"333333\""
[hex]
(as-> (decrown-hex hex) _
(cond
(= 3 (count _)) (string/join (mapcat vector _ _))
(= 1 (count _)) (string/join (repeat 6 _))
:else _)))
(defn- hex->long
"(hex->long \"#abc\") -> 11189196"
[hex]
(-> hex
(string/replace #"^#" "")
(expand-hex)
#?(:clj (Long/parseLong 16)
:cljs (js/parseInt 16))))
(defn- long->hex
"(long->hex 11189196) -> \"aabbcc\""
[long]
#?(:clj (Integer/toHexString long)
:cljs (.toString long 16)))
(defn weighted-mix
"`weight` is number 0 to 100 (%).
At 0, it weighs color-1 at 100%.
At 100, it weighs color-2 at 100%.
Returns hex string."
[color-1 color-2 weight]
(let [[weight-1 weight-2] (map #(/ % 100) [(- 100 weight) weight])
[long-1 long-2] (map (comp hex->long as-hex)
[color-1 color-2])]
(-> (+ (* long-1 weight-1) (* long-2 weight-2))
(long->hex)
(expand-hex)
(crown-hex))))

View File

@ -0,0 +1,753 @@
(ns mranderson047.garden.v1v3v3.garden.compiler
"Functions for compiling Clojure data structures to CSS."
(:require
[clojure.string :as string]
#?(:clj [mranderson047.garden.v1v3v3.garden.color :as color]
:cljs [mranderson047.garden.v1v3v3.garden.color :as color :refer [CSSColor]])
[mranderson047.garden.v1v3v3.garden.compression :as compression]
[mranderson047.garden.v1v3v3.garden.selectors :as selectors]
[mranderson047.garden.v1v3v3.garden.units :as units]
[mranderson047.garden.v1v3v3.garden.util :as util]
#?(:cljs
[mranderson047.garden.v1v3v3.garden.types :refer [CSSUnit CSSFunction CSSAtRule]]))
#?(:cljs
(:require-macros
[mranderson047.garden.v1v3v3.garden.compiler :refer [with-media-query-context with-selector-context]]))
#?(:clj
(:import (mranderson047.garden.v1v3v3.garden.types CSSUnit CSSFunction CSSAtRule)
(mranderson047.garden.v1v3v3.garden.color CSSColor))))
;; ---------------------------------------------------------------------
;; Compiler flags
(def
^{:dynamic true
:private true
:doc "The current compiler flags."}
*flags*
{;; When set to `true` the compiled stylesheet will be "pretty
;; printed." This would be equivalent to setting
;; `{:ouput-style => :expanded}` in Sass. When set to `false`
;; the compiled stylesheet will be compressed with the YUI
;; compressor.
:pretty-print? true
;; A sequence of files to prepend to the output file.
:preamble []
;; Location to save a stylesheet after compiling.
:output-to nil
;; A list of vendor prefixes to prepend to things like
;; `@keyframes`, properties within declarations containing the
;; `^:prefix` meta data, and properties defined in `:auto-prefix`.
:vendors []
;; A set of properties to automatically prefix with `:vendors`.
:auto-prefix #{}
;; `@media-query` specific configuration.
:media-expressions {;; May either be `:merge` or `:default`. When
;; set to `:merge` nested media queries will
;; have their expressions merged with their
;; parent's.
:nesting-behavior :default}})
(def
^{:private true
:doc "Retun a function to call when rendering a media expression.
The returned function accepts two arguments: the media
expression being evaluated and the current media expression context.
Both arguments are maps. This is used to provide semantics for nested
media queries."}
media-expression-behavior
{:merge (fn [expr context] (merge context expr))
:default (fn [expr _] expr)})
(def
^{:dynamic true
:private true
:doc "The current parent selector context."}
*selector-context* nil)
(def
^{:dynamic true
:private true
:doc "The current media query context."}
*media-query-context* nil)
;; ---------------------------------------------------------------------
;; Utilities
(defmacro with-selector-context
[selector-context & body]
`(binding [*selector-context* ~selector-context]
(do ~@body)))
(defmacro with-media-query-context
[selector-context & body]
`(binding [*media-query-context* ~selector-context]
(do ~@body)))
(defn- vendors
"Return the current list of browser vendors specified in `*flags*`."
[]
(seq (:vendors *flags*)))
(defn- auto-prefixed-properties
"Return the current list of auto-prefixed properties specified in `*flags*`."
[]
(set (map name (:auto-prefix *flags*))))
(defn- auto-prefix?
[property]
(contains? (auto-prefixed-properties) property))
(defn- top-level-expression? [x]
(or (util/rule? x)
(util/at-import? x)
(util/at-media? x)
(util/at-keyframes? x)))
(defn- divide-vec
"Return a vector of [(filter pred coll) (remove pred coll)]."
[pred coll]
((juxt filter remove) pred coll))
#?(:clj
(defn- save-stylesheet
"Save a stylesheet to disk."
[path stylesheet]
(spit path stylesheet)))
;; =====================================================================
;; Expansion
;; The expansion process ensures that before a stylesheet is rendered
;; it is in a format that can be easily digested. That is, it produces
;; a new data structure which is a list of only one level.
;; This intermediate process between input and compilation separates
;; concerns between parsing data structures and compiling them to CSS.
;; All data types that implement `IExpandable` should produce a list.
(defprotocol IExpandable
(expand [this]
"Return a list containing the expanded form of `this`."))
;; ---------------------------------------------------------------------
;; List expansion
(defn- expand-seqs
"Like flatten but only affects seqs."
[coll]
(mapcat
(fn [x]
(if (seq? x)
(expand-seqs x)
(list x)))
coll))
;; ---------------------------------------------------------------------
;; Declaration expansion
(defn expand-declaration-1
[declaration]
{:pre [(map? declaration)]}
(let [prefix #(util/as-str %1 "-" %2)]
(reduce
(fn [m [k v]]
(if (util/hash-map? v)
(reduce
(fn [m1 [k1 v1]]
(assoc m1 (prefix k k1) v1))
m
(expand-declaration-1 v))
(assoc m (util/to-str k) v)))
(empty declaration)
declaration)))
(defn- expand-declaration
[declaration]
(if (empty? declaration)
declaration
(with-meta (expand-declaration-1 declaration) (meta declaration))))
;; ---------------------------------------------------------------------
;; Rule expansion
(def
^{:private true
:doc "Matches a single \"&\" or \"&\" follow by one or more
non-whitespace characters."}
parent-selector-re
#"^&(?:\S+)?$")
(defn- extract-reference
"Extract the selector portion of a parent selector reference."
[selector]
(when-let [reference (->> (last selector)
(util/to-str)
(re-find parent-selector-re))]
(apply str (rest reference))))
(defn- expand-selector-reference
[selector]
(if-let [reference (extract-reference selector)]
(let [parent (butlast selector)]
(concat (butlast parent)
(-> (last parent)
(util/as-str reference)
(list))))
selector))
(defn- expand-selector [selector parent]
(let [selector (map selectors/css-selector selector)
selector (if (seq parent)
(->> (util/cartesian-product parent selector)
(map flatten))
(map list selector))]
(map expand-selector-reference selector)))
(defn- expand-rule
[rule]
(let [[selector children] (split-with selectors/selector? rule)
selector (expand-selector selector *selector-context*)
children (expand children)
[declarations xs] (divide-vec util/declaration? children)
ys (with-selector-context
(if (seq selector)
selector
*selector-context*)
(doall (mapcat expand xs)))]
(->> (mapcat expand declarations)
(conj [selector])
(conj ys))))
;; ---------------------------------------------------------------------
;; At-rule expansion
(defmulti ^:private expand-at-rule :identifier)
(defmethod expand-at-rule :default
[at-rule]
(list at-rule))
;; @keyframes expansion
(defmethod expand-at-rule :keyframes
[{:keys [value]}]
(let [{:keys [identifier frames]} value]
(->> {:identifier (util/to-str identifier)
:frames (mapcat expand frames)}
(CSSAtRule. :keyframes)
(list))))
;; @media expansion
(defn- expand-media-query-expression [expression]
(if-let [f (->> [:media-expressions :nesting-behavior]
(get-in *flags*)
(media-expression-behavior))]
(f expression *media-query-context*)
expression))
(defmethod expand-at-rule :media
[{:keys [value]}]
(let [{:keys [media-queries rules]} value
media-queries (expand-media-query-expression media-queries)
xs (with-media-query-context media-queries (doall (mapcat expand (expand rules))))
;; Though media-queries may be nested, they may not be nested
;; at compile time. Here we make sure this is the case.
[subqueries rules] (divide-vec util/at-media? xs)]
(cons
(CSSAtRule. :media {:media-queries media-queries
:rules rules})
subqueries)))
;; ---------------------------------------------------------------------
;; Stylesheet expansion
(defn- expand-stylesheet [xs]
(->> (expand xs)
(map expand)
(apply concat)))
(extend-protocol IExpandable
#?(:clj clojure.lang.ISeq
:cljs IndexedSeq)
(expand [this] (expand-seqs this))
#?(:cljs LazySeq)
#?(:cljs (expand [this] (expand-seqs this)))
#?(:cljs RSeq)
#?(:cljs(expand [this] (expand-seqs this)))
#?(:cljs NodeSeq)
#?(:cljs (expand [this] (expand-seqs this)))
#?(:cljs ArrayNodeSeq)
#?(:cljs (expand [this] (expand-seqs this)))
#?(:cljs Cons)
#?(:cljs (
expand [this] (expand-seqs this)))
#?(:cljs ChunkedCons)
#?(:cljs (expand [this] (expand-seqs this)))
#?(:cljs ChunkedSeq)
(expand [this] (expand-seqs this))
#?(:cljs PersistentArrayMapSeq)
#?(:cljs (expand [this] (expand-seqs this)))
#?(:cljs List)
#?(:cljs (expand [this] (expand-seqs this)))
#?(:clj clojure.lang.IPersistentVector
:cljs PersistentVector)
(expand [this] (expand-rule this))
#?(:cljs Subvec)
#?(:cljs (expand [this] (expand-rule this)))
#?(:cljs BlackNode)
#?(:cljs (expand [this] (expand-rule this)))
#?(:cljs RedNode)
#?(:cljs (expand [this] (expand-rule this)))
#?(:clj clojure.lang.IPersistentMap
:cljs PersistentArrayMap)
(expand [this] (list (expand-declaration this)))
#?(:cljs PersistentHashMap)
#?(:cljs (expand [this] (list (expand-declaration this))))
#?(:cljs PersistentTreeMap)
#?(:cljs (expand [this] (list (expand-declaration this))))
#?(:clj Object
:cljs default)
(expand [this] (list this))
CSSFunction
(expand [this] (list this))
CSSAtRule
(expand [this] (expand-at-rule this))
CSSColor
(expand [this] (list this))
nil
(expand [this] nil))
;; ---------------------------------------------------------------------
;; Rendering
(defprotocol CSSRenderer
(render-css [this]
"Convert a Clojure data type in to a string of CSS."))
;; ---------------------------------------------------------------------
;; Punctuation
(def ^:private comma ", ")
(def ^:private colon ": ")
(def ^:private semicolon ";")
(def ^:private l-brace " {\n")
(def ^:private r-brace "\n}")
(def ^:private l-brace-1 " {\n\n")
(def ^:private r-brace-1 "\n\n}")
(def ^:private rule-sep "\n\n")
(def ^:private indent " ")
(defn- space-separated-list
"Return a space separated list of values."
([xs]
(space-separated-list render-css xs))
([f xs]
(string/join " " (map f xs))))
(defn- comma-separated-list
"Return a comma separated list of values. Subsequences are joined with
spaces."
([xs]
(comma-separated-list render-css xs))
([f xs]
(let [ys (for [x xs]
(if (sequential? x)
(space-separated-list f x)
(f x)))]
(string/join comma ys))))
(defn- rule-join [xs]
(string/join rule-sep xs))
(def
^{:private true
:doc "Match the start of a line if the characters immediately
after it are spaces or used in a CSS id (#), class (.), or tag name."}
indent-loc-re
#?(:clj
#"(?m)(?=[\sA-z#.}-]+)^")
#?(:cljs
(js/RegExp. "(?=[ A-Za-z#.}-]+)^" "gm")))
(defn- indent-str [s]
#?(:clj
(string/replace s indent-loc-re indent))
#?(:cljs
(.replace s indent-loc-re indent)))
;; ---------------------------------------------------------------------
;; Declaration rendering
(defn- render-value
"Render the value portion of a declaration."
[x]
(if (util/at-keyframes? x)
(util/to-str (get-in x [:value :identifier]))
(render-css x)))
(defn- render-property-and-value
[[prop val]]
(if (set? val)
(->> (interleave (repeat prop) val)
(partition 2)
(map render-property-and-value)
(string/join "\n"))
(let [val (if (sequential? val)
(comma-separated-list render-value val)
(render-value val))]
(util/as-str prop colon val semicolon))))
(defn- add-blocks
"For each block in `declaration`, add sequence of blocks
returned from calling `f` on the block."
[f declaration]
(mapcat #(cons % (f %)) declaration))
(defn- prefixed-blocks
"Sequence of blocks with their properties prefixed by
each vendor in `vendors`."
[vendors [p v]]
(for [vendor vendors]
[(util/vendor-prefix vendor (name p)) v]))
(defn- prefix-all-properties
"Add prefixes to all blocks in `declaration` using
vendor prefixes in `vendors`."
[vendors declaration]
(add-blocks (partial prefixed-blocks vendors) declaration))
(defn- prefix-auto-properties
"Add prefixes to all blocks in `declaration` when property
is in the `:auto-prefix` set."
[vendors declaration]
(add-blocks
(fn [block]
(let [[p _] block]
(when (auto-prefix? (name p))
(prefixed-blocks vendors block))))
declaration))
(defn- prefix-declaration
"Prefix properties within a `declaration` if `{:prefix true}` is
set in its meta, or if a property is in the `:auto-prefix` set."
[declaration]
(let [vendors (or (:vendors (meta declaration)) (vendors))
prefix-fn (if (:prefix (meta declaration))
prefix-all-properties
prefix-auto-properties)]
(prefix-fn vendors declaration)))
(defn- render-declaration
[declaration]
(->> (prefix-declaration declaration)
(map render-property-and-value)
(string/join "\n")))
;; ---------------------------------------------------------------------
;; Rule rendering
(defn- render-selector
[selector]
(comma-separated-list selector))
(defn- render-rule
"Convert a vector to a CSS rule string. The vector is expected to be
fully expanded."
[[selector declarations :as rule]]
(when (and (seq rule) (every? seq rule))
(str (render-selector selector)
l-brace
(->> (map render-css declarations)
(string/join "\n")
(indent-str))
r-brace)))
;; ---------------------------------------------------------------------
;; Media query rendering
(defn- render-media-expr-part
"Render the individual components of a media expression."
[[k v]]
(let [[sk sv] (map render-value [k v])]
(cond
(true? v) sk
(false? v) (str "not " sk)
(= "only" sv) (str "only " sk)
:else (if (and v (seq sv))
(str "(" sk colon sv ")")
(str "(" sk ")")))))
(defn- render-media-expr
"Make a media query expession from one or more maps. Keys are not
validated but values have the following semantics:
`true` as in `{:screen true}` == \"screen\"
`false` as in `{:screen false}` == \"not screen\"
`:only` as in `{:screen :only} == \"only screen\""
[expr]
(if (sequential? expr)
(->> (map render-media-expr expr)
(comma-separated-list))
(->> (map render-media-expr-part expr)
(string/join " and "))))
;; ---------------------------------------------------------------------
;; Garden type rendering
(defn- render-unit
"Render a CSSUnit."
[css-unit]
(let [{:keys [magnitude unit]} css-unit
magnitude #?(:cljs magnitude)
#?(:clj (if (ratio? magnitude)
(float magnitude)
magnitude))]
(str magnitude (name unit))))
(defn- render-function
"Render a CSS function."
[css-function]
(let [{:keys [function args]} css-function
args (if (sequential? args)
(comma-separated-list args)
(util/to-str args))]
(util/format "%s(%s)" (util/to-str function) args)))
(defn ^:private render-color [c]
(if-let [a (:alpha c)]
(let [{:keys [hue saturation lightness]} (color/as-hsl c)
[s l] (map units/percent [saturation lightness])]
(util/format "hsla(%s)" (comma-separated-list [hue s l a])))
(color/as-hex c)))
;; ---------------------------------------------------------------------
;; At-rule rendering
(defmulti ^:private render-at-rule
"Render a CSS at-rule"
:identifier)
(defmethod render-at-rule :default [_] nil)
;; @import
(defmethod render-at-rule :import
[{:keys [value]}]
(let [{:keys [url media-queries]} value
url (if (string? url)
(util/wrap-quotes url)
(render-css url))
queries (when media-queries
(render-media-expr media-queries))]
(str "@import "
(if queries (str url " " queries) url)
semicolon)))
;; @keyframes
(defmethod render-at-rule :keyframes
[{:keys [value]}]
(let [{:keys [identifier frames]} value]
(when (seq frames)
(let [body (str (util/to-str identifier)
l-brace-1
(->> (map render-css frames)
(rule-join)
(indent-str))
r-brace-1)
prefix (fn [vendor]
(str "@" (util/vendor-prefix vendor "keyframes ")))]
(->> (map prefix (vendors))
(cons "@keyframes ")
(map #(str % body))
(rule-join))))))
;; @media
(defmethod render-at-rule :media
[{:keys [value]}]
(let [{:keys [media-queries rules]} value]
(when (seq rules)
(str "@media "
(render-media-expr media-queries)
l-brace-1
(-> (map render-css rules)
(rule-join)
(indent-str))
r-brace-1))))
;; ---------------------------------------------------------------------
;; CSSRenderer implementation
(extend-protocol CSSRenderer
#?(:clj clojure.lang.ISeq
:cljs IndexedSeq)
(render-css [this] (map render-css this))
#?(:cljs LazySeq)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs RSeq)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs NodeSeq)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs ArrayNodeSeq)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs Cons)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs ChunkedCons)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs ChunkedSeq)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs PersistentArrayMapSeq)
#?(:cljs (render-css [this] (map render-css this)))
#?(:cljs List)
#?(:cljs (render-css [this] (map render-css this)))
#?(:clj clojure.lang.IPersistentVector
:cljs PersistentVector)
(render-css [this] (render-rule this))
#?(:cljs Subvec)
#?(:cljs (render-css [this] (render-rule this)))
#?(:cljs BlackNode)
#?(:cljs (render-css [this] (render-rule this)))
#?(:cljs RedNode)
#?(:cljs (render-css [this] (render-rule this)))
#?(:clj clojure.lang.IPersistentMap
:cljs PersistentArrayMap)
(render-css [this] (render-declaration this))
#?(:cljs PersistentHashMap)
#?(:cljs (render-css [this] (render-declaration this)))
#?(:cljs PersistentTreeMap)
#?(:cljs (render-css [this] (render-declaration this)))
#?(:clj clojure.lang.Ratio)
#?(:clj (render-css [this] (str (float this))))
#?(:cljs number)
#?(:cljs (render-css [this] (str this)))
#?(:clj clojure.lang.Keyword
:cljs Keyword)
(render-css [this] (name this))
CSSUnit
(render-css [this] (render-unit this))
CSSFunction
(render-css [this] (render-function this))
CSSAtRule
(render-css [this] (render-at-rule this))
#?(:clj CSSColor
:cljs color/CSSColor)
(render-css [this] (render-color this))
#?(:clj Object
:cljs default)
(render-css [this] (str this))
nil
(render-css [this] ""))
;; ---------------------------------------------------------------------
;; Compilation
(defn compile-style
"Convert a sequence of maps into CSS for use with the HTML style
attribute."
[ms]
(->> (filter util/declaration? ms)
(reduce merge)
(expand)
(render-css)
(first)))
(defn- do-compile
"Return a string of CSS."
[flags rules]
(binding [*flags* flags]
(->> (expand-stylesheet rules)
(filter top-level-expression?)
(map render-css)
(remove nil?)
(rule-join))))
(defn- do-preamble
"Prefix stylesheet with files in preamble. Not available in
ClojureScript."
[{:keys [preamble]} stylesheet]
#?(:clj
(string/join "\n" (conj (mapv slurp preamble) stylesheet)))
#?(:cljs
stylesheet))
(defn- do-compression
"Compress CSS if the pretty-print(?) flag is true."
[{:keys [pretty-print? pretty-print]} stylesheet]
;; Also accept pretty-print like CLJS.
(if (or pretty-print? pretty-print)
stylesheet
(compression/compress-stylesheet stylesheet)))
(defn- do-output-to
"Write contents of stylesheet to disk."
[{:keys [output-to]} stylesheet]
#?(:clj
(when output-to
(save-stylesheet output-to stylesheet)
(println "Wrote:" output-to)))
stylesheet)
(defn compile-css
"Convert any number of Clojure data structures to CSS."
[flags & rules]
(let [[flags rules] (if (and (util/hash-map? flags)
(some (set (keys flags)) (keys *flags*)))
[(merge *flags* flags) rules]
[*flags* (cons flags rules)])]
(->> (do-compile flags rules)
(do-preamble flags)
(do-compression flags)
(do-output-to flags))))

View File

@ -0,0 +1,97 @@
(ns mranderson047.garden.v1v3v3.garden.compression
"Stylesheet compression utilities."
#?(:clj
(:import (java.io StringReader StringWriter)
(com.yahoo.platform.yui.compressor CssCompressor))))
;; ---------------------------------------------------------------------
;; Clojure
;; Clojure stylesheet compression leverages the YUI Compressor as it
;; provides a performant and excellent solution to CSS compression.
#?(:clj
(defn compress-stylesheet
"Compress a stylesheet with the YUI CSSCompressor. Set
line-break-position to -1 for no line breaks, 0 for a line break
after each rule, and n > 0 for a line break after at most n
columns. Defaults to no -1"
([stylesheet]
(compress-stylesheet stylesheet -1))
([^String stylesheet line-break-position]
(with-open [reader (StringReader. stylesheet)
writer (StringWriter.)]
(doto (CssCompressor. reader)
(.compress writer line-break-position))
(str writer)))))
;; ---------------------------------------------------------------------
;; ClojureScript
;; ClojureScript stylesheet compression uses a simple tokenizer and
;; loop/recur to construct a new string of minified CSS.
#?(:cljs
(defn- token-fn
"Return a function which when given a string will return a map
containing the chunk of text matched by re, it's size, and tag."
[[tag re]]
(fn [s]
(when-let [chunk (re-find re s)]
{:tag tag
:chunk chunk
:size (count chunk)}))))
#?(:cljs
(defn- tokenizer
"Given an arbitrary number of [tag regex] pairs, return a function
which when given a string s will return the first matching token of s.
Token precedence is determined by the order of the pairs. The first
and last pairs have the highest and lowest precedence respectively."
[& tags+regexes]
(let [fs (map token-fn tags+regexes)]
(fn [s]
(some #(% s) fs)))))
#?(:cljs
(def
^{:private true
:doc "Tokenizer used during stylesheet compression."}
stylesheet-tokenizer
(tokenizer
;; String literals
[:string #"^\"(?:\\.|[^\"])*\""]
;; Delimiters
[:r-brace #"^\s*\{\s*"]
[:l-brace #"^;?\s*}"]
[:r-paren #"^\s*\(\s*"]
[:l-paren #"^\s*\)"]
[:comma #"^,\s*"]
[:colon #"^:\s*"]
[:semicolon #"^;"]
;; White space
[:space+ #"^ +"]
[:white-space+ #"^\s+"]
;; Everything else
[:any #"^."])))
#?(:cljs
(defn compress-stylesheet
"Compress a string of CSS using a basic compressor."
[stylesheet]
(loop [s1 stylesheet s2 ""]
(if-let [{:keys [tag chunk size]} (stylesheet-tokenizer s1)]
(recur (subs s1 size)
(str s2 (case tag
:string chunk
:r-brace "{"
:l-brace "}"
:r-paren "("
:l-paren ")"
:comma ","
:semi-comma ";"
:colon ":"
:space+ " "
:white-space+ ""
chunk)))
s2))))

View File

@ -0,0 +1,17 @@
(ns mranderson047.garden.v1v3v3.garden.core
"Convert Clojure data structures to CSS."
(:require [mranderson047.garden.v1v3v3.garden.compiler :as compiler]))
(defn ^String css
"Convert a variable number of Clojure data structure to a string of
CSS. The first argument may be a list of flags for the compiler."
{:arglists '([rules] [flags? rules])}
[& rules]
(apply compiler/compile-css rules))
(defn ^String style
"Convert a variable number of maps into a string of CSS for use with
the HTML `style` attribute."
[& maps]
(compiler/compile-style maps))

View File

@ -0,0 +1,119 @@
(ns mranderson047.garden.v1v3v3.garden.def
(:require [mranderson047.garden.v1v3v3.garden.types]
[mranderson047.garden.v1v3v3.garden.util :as util]
[mranderson047.garden.v1v3v3.garden.core])
(:import garden.types.CSSFunction
garden.types.CSSAtRule))
(defmacro defstyles
"Convenience macro equivalent to `(def name (list styles*))`."
[name & styles]
`(def ~name (list ~@styles)))
(defmacro defstylesheet
"Convenience macro equivalent to `(def name (css opts? styles*))`."
[name & styles]
`(def ~name (mranderson047.garden.v1v3v3.garden.core/css ~@styles)))
(defmacro defrule
"Define a function for creating rules. If only the `name` argument is
provided the rule generating function will default to using it as the
primary selector.
Ex.
(defrule a)
;; => #'user/a
(a {:text-decoration \"none\"})
;; => [:a {:text-decoration \"none\"}]
Ex.
(defrule sub-headings :h4 :h5 :h6)
;; => #'user/sub-headings
(sub-headings {:font-weight \"normal\"})
;; => [:h4 :h5 :h6 {:font-weight \"normal\"}]"
[sym & selectors]
(let [rule (if (seq selectors)
`(vec '~selectors)
[(keyword sym)])
[_ sym spec] (macroexpand `(defn ~sym [~'& ~'children]
(into ~rule ~'children)))]
`(def ~sym ~spec)))
(defmacro ^{:arglists '([name] [name docstring? & fn-tail])}
defcssfn
"Define a function for creating custom CSS functions. The generated
function will automatically create an instance of
`garden.types.CSSFunction` of which the `:args` field will be set
to whatever the return value of the original function is. The
`:function` field will be set to `(str name)`.
If only the `name` argument is provided the returned function will
accept any number of arguments.
Ex.
(defcssfn url)
;; => #'user/url
(url \"http://fonts.googleapis.com/css?family=Lato\")
;; => #garden.types.CSSFunction{:function \"url\", :args \"http://fonts.googleapis.com/css?family=Lato\"}
(css (url \"http://fonts.googleapis.com/css?family=Lato\"))
;; => url(http://fonts.googleapis.com/css?family=Lato)
Ex.
(defcssfn attr
([name] name)
([name type-or-unit]
[[name type-or-unit]])
([name type-or-unit fallback]
[name [type-or-unit fallback]]))
;; => #'user/attr
(attr :vertical :length)
;; => #garden.types.CSSFunction{:function \"url\", :args [:vertical :length]}
(css (attr :vertical :length))
;; => \"attr(vertical length)\"
(attr :end-of-quote :string :inherit)
;; => #garden.types.CSSFunction{:function \"url\", :args [:end-of-quote [:string :inherit]]}
(css (attr :end-of-quote :string :inherit))
;; => \"attr(end-of-quote string, inherit)\""
([sym]
(let [[_ sym fn-tail] (macroexpand
`(defn ~sym [& ~'args]
(CSSFunction. ~(str sym) ~'args)))]
`(def ~sym ~fn-tail)))
([sym & fn-tail]
(let [[_ sym [_ & fn-spec]] (macroexpand `(defn ~sym ~@fn-tail))
cssfn-name (str sym)]
`(def ~sym
(fn [& args#]
(CSSFunction. ~cssfn-name (apply (fn ~@fn-spec) args#)))))))
(defmacro defkeyframes
"Define a CSS @keyframes animation.
Ex.
(defkeyframes my-animation
[:from
{:background \"red\"}]
[:to
{:background \"yellow\"}])
(css {:vendors [\"webkit\"]}
my-animation ;; Include the animation in the stylesheet.
[:div
^:prefix ;; Use vendor prefixing (optional).
{:animation [[my-animation \"5s\"]]}])"
[sym & frames]
(let [value {:identifier `(str '~sym)
:frames `(list ~@frames)}
obj `(CSSAtRule. :keyframes ~value)]
`(def ~sym ~obj)))

View File

@ -0,0 +1,27 @@
(ns mranderson047.garden.v1v3v3.garden.media
"Utility functions for working with media queries.")
;; See: http://www.w3.org/TR/css3-mediaqueries/#media1
(def media-features
#{:all
:aspect-ratio :min-aspect-ratio :max-aspect-ratio
:braille
:color :min-color :max-color
:color-index :min-color-index :max-color-index
:device-height :min-device-height :max-device-height
:device-width :min-device-width :max-device-width
:embossed
:grid
:handheld
:height :min-height :max-height
:monochrome :min-monochrome :max-monochrome
:orientation
:print
:projection
:resolution :min-resolution :max-resolution
:scan
:screen
:speech
:tty
:tv
:width :min-width :max-width})

View File

@ -0,0 +1,31 @@
(ns mranderson047.garden.v1v3v3.garden.repl
"Method definitions for `print-method` with Garden types."
(:require [mranderson047.garden.v1v3v3.garden.compiler :as compiler]
[mranderson047.garden.v1v3v3.garden.util :as util]
[mranderson047.garden.v1v3v3.garden.types]
[mranderson047.garden.v1v3v3.garden.color]
[mranderson047.garden.v1v3v3.garden.selectors :as selectors])
(:import (mranderson047.garden.v1v3v3.garden.types CSSUnit
CSSFunction
CSSAtRule)
(mranderson047.garden.v1v3v3.garden.color CSSColor)
(mranderson047.garden.v1v3v3.garden.selectors CSSSelector)))
(defmethod print-method CSSUnit [css-unit writer]
(.write writer (compiler/render-css css-unit)))
(defmethod print-method CSSFunction [css-function writer]
(.write writer (compiler/render-css css-function)))
(defmethod print-method CSSColor [color writer]
(.write writer (compiler/render-css color)))
(defmethod print-method CSSAtRule [css-at-rule writer]
(let [f (if (or (util/at-keyframes? css-at-rule)
(util/at-media? css-at-rule))
compiler/compile-css
compiler/render-css)]
(.write writer (f css-at-rule))))
(defmethod print-method CSSSelector [css-selector writer]
(.write writer (selectors/css-selector css-selector)))

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,79 @@
(ns mranderson047.garden.v1v3v3.garden.stylesheet
"Utility functions for CSS properties, directives and functions."
(:require [mranderson047.garden.v1v3v3.garden.util :as util]
[mranderson047.garden.v1v3v3.garden.color :as color]
[mranderson047.garden.v1v3v3.garden.types :as t])
#?(:clj
(:import garden.types.CSSFunction
garden.types.CSSAtRule)))
;;;; ## Stylesheet helpers
(defn rule
"Create a rule function for the given selector. The `selector`
argument must be valid selector (ie. a keyword, string, or symbol).
Additional arguments may consist of extra selectors or
declarations.
The returned function accepts any number of arguments which represent
the rule's children.
Ex.
(let [text-field (rule \"[type=\"text\"])]
(text-field {:border [\"1px\" :solid \"black\"]}))
;; => [\"[type=\"text\"] {:boder [\"1px\" :solid \"black\"]}]"
[selector & more]
(if-not (or (keyword? selector)
(string? selector)
(symbol? selector))
(throw (ex-info
"Selector must be either a keyword, string, or symbol." {}))
(fn [& children]
(into (apply vector selector more) children))))
(defn cssfn [fn-name]
(fn [& args]
(t/CSSFunction. fn-name args)))
;;;; ## At-rules
(defn- at-rule [identifier value]
(t/CSSAtRule. identifier value))
(defn at-font-face
"Create a CSS @font-face rule."
[& font-properties]
["@font-face" font-properties])
(defn at-import
"Create a CSS @import rule."
([url]
(at-rule :import {:url url
:media-queries nil}))
([url & media-queries]
(at-rule :import {:url url
:media-queries media-queries})))
(defn at-media
"Create a CSS @media rule."
[media-queries & rules]
(at-rule :media {:media-queries media-queries
:rules rules}))
(defn at-keyframes
"Create a CSS @keyframes rule."
[identifier & frames]
(at-rule :keyframes {:identifier identifier
:frames frames}))
;;;; ## Functions
(defn rgb
"Create a color from RGB values."
[r g b]
(color/rgb [r g b]))
(defn hsl
"Create a color from HSL values."
[h s l]
(color/hsl [h s l]))

View File

@ -0,0 +1,8 @@
(ns mranderson047.garden.v1v3v3.garden.types
"Internal types used by Garden.")
(defrecord CSSUnit [unit magnitude])
(defrecord CSSFunction [function args])
(defrecord CSSAtRule [identifier value])

View File

@ -0,0 +1,327 @@
(ns mranderson047.garden.v1v3v3.garden.units
"Functions and macros for working with CSS units."
(:refer-clojure :exclude [rem])
#?@(:clj
[(:require
[mranderson047.garden.v1v3v3.garden.types :as types]
[mranderson047.garden.v1v3v3.garden.util :as util])
(:import
[mranderson047.garden.v1v3v3.garden.types CSSUnit])])
#?@(:cljs
[(:require
[cljs.reader :refer [read-string]]
[mranderson047.garden.v1v3v3.garden.types :as types :refer [CSSUnit]]
[mranderson047.garden.v1v3v3.garden.util :as util])
(:require-macros
[mranderson047.garden.v1v3v3.garden.units :refer [defunit]])]))
;;;; ## Unit families
(def length-units
#{:in :cm :pc :mm :pt :px (keyword "%")})
(def angular-units
#{:deg :grad :rad :turn})
(def time-units
#{:s :ms})
(def frequency-units
#{:Hz :kHz})
(def resolution-units
#{:dpi :dpcm :dppx})
;;;; ## Unit predicates
(defn unit?
"True if x is of type CSSUnit."
[x]
(instance? CSSUnit x))
(defn length?
[x]
(and (unit? x)
(contains? length-units (:unit x))))
(defn angle?
[x]
(and (unit? x)
(contains? angular-units (:unit x))))
(defn time?
[x]
(and (unit? x)
(contains? time-units (:unit x))))
(defn frequency?
[x]
(and (unit? x)
(contains? frequency-units (:unit x))))
(defn resolution?
[x]
(and (unit? x)
(contains? resolution-units (:unit x))))
;;;; ## Unit conversion
(def ^{:private true
:doc "Map associating CSS unit types to their conversion values."}
conversions
{;; Absolute units
:cm {:cm 1
:mm 10
:pc 2.36220473
:pt 28.3464567
:px 37.795275591}
:in {:cm 2.54
:in 1
:mm 25.4
:pc 6
:pt 72
:px 96}
:mm {:mm 1
:pt 2.83464567
:px 3.7795275591}
:pc {:mm 4.23333333
:pc 1
:pt 12
:px 16}
:pt {:pt 1
:px 1.3333333333}
:px {:px 1}
(keyword "%") {(keyword "%") 1}
;; Relative untis
:em {:em 1}
:rem {:rem 1}
;; Angular units
:deg {:deg 1
:grad 1.111111111
:rad 0.0174532925
:turn 0.002777778}
:grad {:grad 1
:rad 63.661977237
:turn 0.0025}
:rad {:rad 1
:turn 0.159154943}
:turn {:turn 1}
;; Time units
:s {:ms 1000
:s 1}
:ms {:ms 1}
;; Frequency units
:Hz {:Hz 1
:kHz 0.001}
:kHz {:kHz 1}})
(defn- convertable?
"True if unit is a key of convertable-units, false otherwise."
[unit]
(contains? conversions unit))
(defn- convert
"Convert a Unit with :unit left to a Unit with :unit right if possible."
[{m :magnitude left :unit} right]
(if (every? convertable? [left right])
(let [v1 (get-in conversions [left right])
v2 (get-in conversions [right left])]
(cond
v1
(CSSUnit. right (* v1 m))
v2
(CSSUnit. right (/ m v2))
;; Both units are convertible but no conversion between them exists.
:else
(throw
(ex-info
(util/format "Can't convert %s to %s" (name left) (name right)) {}))))
;; Display the inconvertible unit.
(let [x (first (drop-while convertable? [left right]))]
(throw (ex-info (str "Inconvertible unit " (name x)) {})))))
;;;; ## Unit helpers
(def ^{:doc "Regular expression for matching a CSS unit. The magnitude
and unit are captured."
:private true}
unit-re
#"([+-]?\d+(?:\.?\d+)?)(p[xtc]|in|[cm]m|%|r?em|ex|ch|v(?:[wh]|m(?:in|ax))|deg|g?rad|turn|m?s|k?Hz|dp(?:i|cm|px))")
(defn read-unit
"Read a `CSSUnit` object from the string `s`."
[s]
(when-let [[_ magnitude unit] (re-matches unit-re s)]
(let [unit (keyword unit)
magnitude (if magnitude (read-string magnitude) 0)]
(CSSUnit. unit magnitude))))
(defn make-unit-predicate
"Creates a function for verifying the given unit type."
[unit]
(fn [x] (and (unit? x) (= (:unit x) unit))))
(defn make-unit-fn
"Creates a function for creating and converting `CSSUnit`s for the
given unit. If a number n is passed the function it will produce a
new `CSSUnit` record with a the magnitude set to n. If a `CSSUnit`
is passed the function will attempt to convert it."
[unit]
(fn [x]
(cond
(number? x)
(CSSUnit. unit x)
(unit? x)
(if (and (= (unit x) unit))
x
(convert x unit))
:else
(let [;; Does `.getName` even work in CLJS? -- @noprompt
ex-message (util/format "Unable to convert from %s to %s"
(.getName type)
(name unit))
;; TODO: This needs to be populated with more helpful
;; data.
ex-data {:given {:type type
:unit unit}}]
(throw
(ex-info ex-message ex-data))))))
(defn make-unit-adder
"Create a addition function for adding Units."
[unit]
(let [u (make-unit-fn unit)]
(fn u+
([] (u 0))
([x] (u x))
([x y]
(let [{m1 :magnitude} (u x)
{m2 :magnitude} (u y)]
(u (+ m1 m2))))
([x y & more]
(reduce u+ (u+ x y) more)))))
(defn make-unit-subtractor
"Create a subtraction function for subtracting Units."
[unit]
(let [u (make-unit-fn unit)]
(fn u-
([x] (u (- x)))
([x y]
(let [{m1 :magnitude} (u x)
{m2 :magnitude} (u y)]
(u (- m1 m2))))
([x y & more]
(reduce u- (u- x y) more)))))
(defn make-unit-multiplier
"Create a multiplication function for multiplying Units."
[unit]
(let [u (make-unit-fn unit)]
(fn u*
([] (u 1))
([x] (u x))
([x y]
(let [{m1 :magnitude} (u x)
{m2 :magnitude} (u y)]
(u (* m1 m2))))
([x y & more]
(reduce u* (u* x y) more)))))
(defn make-unit-divider
"Create a division function for dividing Units."
[unit]
(let [u (make-unit-fn unit)]
(fn ud
([x] (u (/ 1 x)))
([x y]
(let [{m1 :magnitude} (u x)
{m2 :magnitude} (u y)]
(u (/ m1 m2))))
([x y & more]
(reduce ud (ud x y) more)))))
#?(:clj
(defmacro defunit
"Create a suite of functions for unit creation, conversion,
validation, and arithmetic."
([name]
`(defunit ~name ~name))
([name unit]
(let [k (keyword unit)
append #(symbol (str name %))]
`(do
(def ~name (make-unit-fn ~k))
(def ~(append \?) (make-unit-predicate ~k))
(def ~(append \+) (make-unit-adder ~k))
(def ~(append \-) (make-unit-subtractor ~k))
(def ~(append \*) (make-unit-multiplier ~k))
(def ~(append "-div") (make-unit-divider ~k)))))))
(comment
;; This:
(defunit px)
;; Is equivalent to:
(def px (make-unit-fn :px))
(def px? (make-unit-predicate :px))
(def px+ (make-unit-adder :px))
(def px- (make-unit-subtractor :px))
(def px* (make-unit-multiplier :px))
(def px-div (make-unit-divider :px)))
;; # Predefined units
;; Absolute units
(defunit cm)
(defunit mm)
(defunit in)
(defunit px)
(defunit pt)
(defunit pc)
(defunit percent "%")
;; Font-relative units
(defunit em)
(defunit ex)
(defunit ch)
(defunit rem)
;; Viewport-percentage lengths
(defunit vw)
(defunit vh)
(defunit vmin)
(defunit vmax)
;; Angles
(defunit deg)
(defunit grad)
(defunit rad)
(defunit turn)
;; Times
(defunit s)
(defunit ms)
;; Frequencies
(defunit Hz)
(defunit kHz)
;; Resolutions
(defunit dpi)
(defunit dpcm)
(defunit dppx)

View File

@ -0,0 +1,179 @@
(ns mranderson047.garden.v1v3v3.garden.util
"Utility functions used by Garden."
(:require
[clojure.string :as str]
[mranderson047.garden.v1v3v3.garden.types :as t]
#?@(:cljs
[[goog.string]
[goog.string.format]]))
#?(:clj
(:refer-clojure :exclude [format]))
#?(:clj
(:import garden.types.CSSAtRule)))
;; ---------------------------------------------------------------------
;; String utilities
#?(:cljs
(defn format
"Formats a string using goog.string.format."
[fmt & args]
(apply goog.string/format fmt args)))
;; To avoid the pain of #?cljs :refer.
#?(:clj
(def format #'clojure.core/format))
(defprotocol ToString
(^String to-str [this] "Convert a value into a string."))
(extend-protocol ToString
#?(:clj clojure.lang.Keyword)
#?(:cljs Keyword)
(to-str [this] (name this))
#?(:clj Object)
#?(:cljs default)
(to-str [this] (str this))
nil (to-str [this] ""))
(defn ^String as-str
"Convert a variable number of values into strings."
[& args]
(apply str (map to-str args)))
(defn string->int
"Convert a string to an integer with optional base."
[s & [radix]]
(let [radix (or radix 10)]
#?(:clj
(Integer/parseInt ^String s ^Long radix))
#?(:cljs
(js/parseInt s radix))))
(defn int->string
"Convert an integer to a string with optional base."
[i & [radix]]
(let [radix (or radix 10)]
#?(:clj
(Integer/toString ^Long i ^Long radix))
#?(:cljs
(.toString i radix))))
(defn space-join
"Return a space separated list of values."
[xs]
(str/join " " (map to-str xs)))
(defn comma-join
"Return a comma separated list of values. Subsequences are joined with
spaces."
[xs]
(let [ys (for [x xs]
(if (sequential? x)
(space-join x)
(to-str x)))]
(str/join ", " ys)))
(defn wrap-quotes
"Wrap a string with double quotes."
[s]
(str \" s \"))
;; ---------------------------------------------------------------------
;; Predicates
(defn hash-map?
"True if `(map? x)` and `x` does not satisfy `clojure.lang.IRecord`."
[x]
(and (map? x) (not (record? x))))
(def
^{:doc "Alias to `vector?`."}
rule? vector?)
(def
^{:doc "Alias to `hash-map?`."}
declaration? hash-map?)
(defn at-rule?
[x]
(instance? #?(:clj CSSAtRule) #?(:cljs t/CSSAtRule) x))
(defn at-media?
"True if `x` is a CSS `@media` rule."
[x]
(and (at-rule? x) (= (:identifier x) :media)))
(defn at-keyframes?
"True if `x` is a CSS `@keyframes` rule."
[x]
(and (at-rule? x) (= (:identifier x) :keyframes)))
(defn at-import?
"True if `x` is a CSS `@import` rule."
[x]
(and (at-rule? x) (= (:identifier x) :import)))
(defn prefix
"Attach a CSS style prefix to s."
[p s]
(let [p (to-str p)]
(if (= \- (last p))
(str p s)
(str p \- s))))
(defn vendor-prefix
"Attach a CSS vendor prefix to s."
[p s]
(let [p (to-str p)]
(if (= \- (first p))
(prefix p s)
(prefix (str \- p) s))))
;; ---------------------------------------------------------------------
;; Math utilities
(defn natural?
"True if n is a natural number."
[n]
(and (integer? n) (pos? n)))
(defn between?
"True if n is a number between a and b."
[n a b]
(let [bottom (min a b)
top (max a b)]
(and (>= n bottom) (<= n top))))
(defn clip
"Return a number such that n is no less than a and no more than b."
[a b n]
(let [[a b] (if (<= a b) [a b] [b a])]
(max a (min b n))))
(defn average
"Return the average of two or more numbers."
[n m & more]
(/ (apply + n m more) (+ 2.0 (count more))))
;; Taken from clojure.math.combinatorics.
(defn cartesian-product
"All the ways to take one item from each sequence."
[& seqs]
(let [v-original-seqs (vec seqs)
step
(fn step [v-seqs]
(let [increment
(fn [v-seqs]
(loop [i (dec (count v-seqs)), v-seqs v-seqs]
(if (= i -1) nil
(if-let [rst (next (v-seqs i))]
(assoc v-seqs i rst)
(recur (dec i) (assoc v-seqs i (v-original-seqs i)))))))]
(when v-seqs
(cons (map first v-seqs)
(lazy-seq (step (increment v-seqs)))))))]
(when (every? seq seqs)
(lazy-seq (step v-original-seqs)))))