diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..83a1e37 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +lib +pom.xml +*.jar diff --git a/README.markdown b/README.markdown new file mode 100644 index 0000000..4426b55 --- /dev/null +++ b/README.markdown @@ -0,0 +1,136 @@ +# gaka 0.1.0 +by [Brian Carper](http://briancarper.net) + +Gaka is a CSS-generating library for Clojure inspired partly by +[Sass](http://sass-lang.com/) and similar to +[Hiccup](http://github.com/weavejester/hiccup). + +## Features + +* Simple +* Indented output +* Selector nesting +* "Mixins" + +## Purpose + +CSS syntax is verbose, with lots of curly braces and semi-colons. Writing CSS +as s-expressions is a way to ensure you have proper syntax, because the program +handles the syntax for you. And it lets you write CSS very quickly using an +editor that's good at manipulating s-expressions. + +CSS has a lot of repetition in selectors. `body #content div a {...}` etc. +You can remove most of this verbosity via nesting. S-expressions are a great +way to express that nesting. (Sass uses indentation for the same purpose.) + +CSS rules in Gaka are just vectors of keywords and strings and numbers, which +means you can easily generate and manipulate them programatically. + +## Example + +Rules are vectors, where the first element is a selector and the rest are +either key/value pairs, or sub-rules. + + user> (require '(gaka [core :as gaka])) + nil + user> (def rules [:div#foo + :margin "0px" + [:span.bar + :color "black" + :font-weight "bold" + [:a:hover + :text-decoration "none"]]]) + #'user/rules + user> (println (gaka/css rules)) + div#foo { + margin: 0px;} + + div#foo span.bar { + color: black; + font-weight: bold;} + + div#foo span.bar a:hover { + text-decoration: none;} + + + nil + user> (binding [gaka/*print-indent* false] + (println (gaka/css rules))) + div#foo { + margin: 0px;} + + div#foo span.bar { + color: black; + font-weight: bold;} + + div#foo span.bar a:hover { + text-decoration: none;} + + + nil + user> (gaka/save-css "foo.css" rules) + nil + +Anything in a seq (e.g. a list) will be flattened into the surrounding context, +which lets you have "mixins". + + user> (def standard-attrs (list :margin 0 :padding 0 :font-size "12px")) + #'user/standard-attrs + user> (println (gaka/css [:div standard-attrs :color "red"])) + div { + margin: 0; + padding: 0; + font-size: 12px; + color: red;} + user> (defn color [x] (list :color x)) + #'user/color + user> (println (gaka/css [:div (color "red")])) + div { + color: red;} + +If you want a fancy selector or attribute that doesn't work as a keyword, use a +string. + + user> (println (gaka/css ["input[type=text]" :font-family "\"Bitstream Vera Sans\", monospace"])) + input[type=text] { + font-family: "Bitstream Vera Sans", monospace;} + +An easy way to compile your CSS to a file and make sure it's always up-to-date +is to throw a `save-css` call at the bottom of your source file. + + (ns my-site.css + (:require (gaka [core :as gaka) + + (def rules [...]) + + (save-css "public/css/style.css" rules) + +Now every time you re-compile this file (for example, `C-c C-k` in +Slime/Emacs), a static CSS file in `public/css` will be generated or updated. +This is the prefered way to serve CSS files for a web app (to avoid +re-compiling your CSS on every request, which is probably pointless). + +That's about it. + +## Limitations + +Gaka currently outputs less-than-optimal CSS under certain circumstances and +errs on the side of verbosity to preserve correctness. + +Gaka doesn't validate your CSS or check your spelling. + +Gaka makes no attempt to be fast. You should compile your CSS, save it to a +file and serve it statically. + +I wrote this in one afternoon while eating a tasty ham and turky sandwich. +Bugs are likely. + +## Install + +To fetch from CLojars (via Leningen) put this in your project.clj: + + [gaka "0.1.0"] + +## License + +Eclipse Public License 1.0, see http://opensource.org/licenses/eclipse-1.0.php. diff --git a/project.clj b/project.clj new file mode 100644 index 0000000..7201276 --- /dev/null +++ b/project.clj @@ -0,0 +1,5 @@ +(defproject gaka/gaka "0.1.0" + :description "A CSS-generating library for Clojure" + :dependencies [[org.clojure/clojure "1.2.0-master-SNAPSHOT"] + [org.clojure/clojure-contrib "1.2.0-SNAPSHOT"]] + :dev-dependencies [[swank-clojure "1.2.1"]]) diff --git a/src/gaka/core.clj b/src/gaka/core.clj new file mode 100644 index 0000000..afde566 --- /dev/null +++ b/src/gaka/core.clj @@ -0,0 +1,54 @@ +(ns gaka.core + (:require (clojure.contrib [string :as s]) + (clojure.java [io :as io]))) + +(def *context* []) +(def *print-indent* true) + +(defn make-rule [selector keyvals] + {:selector selector + :keyvals keyvals}) + +(defn indent [n] + (when *print-indent* + (s/repeat n " "))) + +(defn render-val [x] + (cond (number? x) (str x) + :else (name x))) + +(defn render-keyval [n [key val]] + (when-not val + (throw (IllegalArgumentException. (str "Missing value for key " (pr-str key) ".")))) + (let [indent (indent n)] + (str indent (name key) ": " (render-val val) ";"))) + +(defn render-rule [{:keys [selector keyvals]}] + (let [indent (indent (dec (count selector)))] + (str indent (s/join " " (map name selector)) " {\n" + (s/join "\n" (map #(render-keyval (count selector) %) + (partition-all 2 keyvals))) + "}\n\n"))) + +(declare compile-rule) +(defn compile* [rules [selector & xs]] + (reduce (fn [rules selector] + (binding [*context* (conj *context* selector)] + (let [subrules (filter vector? xs) + keyvals (flatten (remove vector? xs)) + rules (conj rules (make-rule *context* keyvals))] + (reduce (fn [rs x] + (compile* rs x)) + rules subrules)))) + rules (s/split #"\s*,\s*" (name selector)))) + +(defn css [& rules] + (let [rules (filter (complement empty?) rules)] + (if-not (seq rules) + "" + (let [rules (reduce compile* [] rules)] + (s/map-str render-rule rules))))) + +(defn save-css [filename & rules] + (with-open [out (io/writer filename)] + (.write out (apply css rules)))) diff --git a/test/gaka/core_test.clj b/test/gaka/core_test.clj new file mode 100644 index 0000000..0f629c6 --- /dev/null +++ b/test/gaka/core_test.clj @@ -0,0 +1,62 @@ +(ns gaka.core-test + (:use [gaka.core] :reload-all) + (:use [clojure.test])) + +(defmacro =? [& body] + `(are [x# y#] (= x# y#) + ~@body)) + +(deftest test-compile* + (=? (compile* [] [:a]) + [{:selector ["a"] + :keyvals []}] + + (compile* [] [:a :color :red]) + [{:selector ["a"] + :keyvals [:color :red]}] + + (compile* [] [:a [:img :border :none]]) + [{:selector ["a"] + :keyvals []} + {:selector ["a" "img"] + :keyvals [:border :none]}] + + (compile* [] [:div [:a [:img :border :none]]]) + [{:selector ["div"] + :keyvals []} + {:selector ["div" "a"] + :keyvals []} + {:selector ["div" "a" "img"] + :keyvals [:border :none]}] + + (compile* [] [:div (list :border :none)]) + [{:selector ["div"] + :keyvals [:border :none]}] + + (compile* [] [:div ["a, img" :border :none]]) + [{:selector ["div"] + :keyvals []} + {:selector ["div" "a"] + :keyvals [:border :none]} + {:selector ["div" "img"] + :keyvals [:border :none]}])) + +(deftest test-render-rule + (=? (render-rule {:selector ["a"] :keyvals [:color :red]}) + "a {\n color: red;}\n\n" + + (render-rule {:selector ["a"] :keyvals [:color :red :border :none]}) + "a {\n color: red;\n border: none;}\n\n" + + (render-rule {:selector ["a" "img"] :keyvals [:border :none]}) + " a img {\n border: none;}\n\n")) + +(deftest test-css + (=? (css nil) + "" + + (css [:a :color :red [:img :border :none]]) + "a {\n color: red;}\n\n a img {\n border: none;}\n\n" + + (css [:a :color :red [:img :border :none] :font-style :italic]) + "a {\n color: red;\n font-style: italic;}\n\n a img {\n border: none;}\n\n"))