12. Reading the SASS DSL and Generating CSS with Clojure Zippers

There is a fantastic DSL for generating CSS called SASS. In general the SASS DSL is only available for Ruby programmers. In this chapter we’ll take some steps towards our own SASS DSL in Clojure.

Assumptions

In this chapter we assume that you are aware of CSS as it is used for laying out HTML pages in the browser.

Benefits

The benefit of this chapter is a richer understanding of how zippers can be used in parsing a DSL. In the future you can use this to build your own DSL.

Outline—Features of SASS

Tables 12.1 and 12.2 provide an example of what SASS can do.

Image

Table 12.1 Nesting

Image

Table 12.2 Constants

The benefits of SASS are reuse and manageability of your CSS, particularly for larger sites, as you start to scale but want to retain consistency and control.

Let’s take a look at our larger process. As you can see in Figure 12.1, our aim is to take the SASS DSL input, convert it into a Clojure data structure, apply some transformations, and then output the CSS.

Image

Figure 12.1 The larger process

We’ll do this in the following sections:

1. Set up a basic flat data structure.

2. Nest the data structure according to original SASS tree.

3. Apply a transformation—colon-suffix selectors.

4. Apply a transformation—change nesting to flatter, two-level style.

5. Generate CSS.

6. Apply a transformation—populate simple constants.

The Recipe—Code

Follow these steps to create the recipe:

1. Create a new Leiningen project sass-dsl in your projects directory, and then change to that directory:

lein new sass-dsl
cd sass-dsl

2. Ensure that the file project.clj has the following contents:

(defproject sass_dsl "0.1.0-SNAPSHOT"
  :dependencies [[org.clojure/clojure "1.7.0-beta2"]]
  :target-path "target/%s"
  :profiles {:uberjar {:aot :all}})

3. Ensure that the file src/sass_dsl/core.clj has the following contents:

(ns sass-dsl.core
  (:require [clojure.test :refer :all]
            [sass-dsl.parse :as parse]
            [sass-dsl.render :as render]
            [sass-dsl.transform :as transform]))

(defn sass-to-css-basic
  "Given a basic set of SASS, transform it to CSS."
  [sass-val]
  (-> sass-val
      parse/transform-to-sass-nested-tree
      transform/flatten-colon-suffixes
      transform/sass-de-nest-flattened-colons-vec-zip
      render/output-css))

(defn sass-to-css
  "Given some css with constants defined, transform it to CSS."
  [sass-val]
  (let [tree (parse/transform-to-sass-nested-tree sass-val)]
    (-> tree
        (transform/replaced-constants-structure-vec-zip-vz
        (transform/extract-constants tree {}))
        transform/strip-constant-declarations
        render/output-css)))

4. Ensure that the file test/sass_dsl/core_test.clj has the following contents:

(ns sass-dsl.core-test
  (:require [clojure.test :refer :all]
            [sass-dsl.core :as core]
            [sass-dsl.common-test :as common-test]))

(deftest basic-css-test
  (testing "Ensure we get css output"
    (is (.contains
         (core/sass-to-css-basic common-test/basic-css)
         "table.hl { "

(deftest basic-css-constants-test
  (testing "Ensure we get CSS."
    (is (.contains
         (core/sass-to-css common-test/basic-css-constants)
         ".content-navigation { "))))

5. Ensure that the file src/sass_dsl/common.clj has the following contents:

(ns sass-dsl.common
  (:require [clojure.zip :as zip]))

(defn is-element
  "Is there a CSS element at this zipper node?"
  [loc]
  {:pre  [(not (nil? loc))]}
  (not (coll? (zip/node loc))))

(defn get-parent
  "Retrieve the parent to this CSS node."
  [loc]
  {:pre  [(not (nil? loc))]}
  (zip/left (zip/up loc)))

(defn is-first-element
  "Is this the first element in this zipper structure?"
  [loc]
  (nil? (zip/left loc)))

(defn is-last-element
  "Is this is a tree node in this zipper structure?"
  [loc]
  {:pre [(not (nil? loc))]}
  ;note that nil punning is required here
  (nil? (zip/right loc)))

(defn has-no-colon
  "Is this lacking a colon? ie is this not a value row?"
  [loc]
  (not (.contains (zip/node loc) ":")))

(defn- has-right-element
  "Is there an element to the right?"
  [loc]
  (zip/right loc))

(defn is-selector
  "Is this a selector?"
  [loc]
  {:pre  [(not (nil? loc))]}
  (if (and
       (is-element loc)
       (has-right-element loc))
    (zip/branch? (zip/right loc))))

6. Ensure that the file test/sass_dsl/common_test.clj has the following contents:

(ns sass-dsl.common-test)

(def basic-css "table.hl
  margin: 2em 0
  td.ln
    text-align: right

li
  font:
    family: serif
    weight: bold
    size: 1.2em")

(def basic-css-constants
  "$blue: #3bbfce
  $margin: 16px

.content-navigation
  border-color: $blue
  color: darken($blue, 9%)

.border
  padding: $margin / 2
  margin: $margin / 2
  border-color: $blue")

7. Ensure that the file src/sass_dsl/parse.clj has the following contents:

(ns sass-dsl.parse
  (:require [clojure.zip :as zip]
            [clojure.string :refer [split] :as str]))

(defn get-lines
  "Tokenise the input into lines."
  [input-str]
  {:pre  [(= (class input-str) java.lang.String)]}
  (split input-str #" +"))

(defn count-space-indent
  "Count the number of spaces used to indent."
  [input-string]
  (count (take-while #{space} input-string)))

(defn has-indent
  "Does this line have an indent?"
  [loc]
  (pos? (count-space-indent (zip/node  loc))))

(defn drop-one-indent
  "Shuffle this down an indent by stripping two leading spaces."
  [input-string]
  ; Precondition here that the two chars are spaces
  {:pre [(and (= (.charAt input-string 0) space) (= (.charAt input-string 1)
space))]}
  (subs input-string 2))

(defn is-left-branch
  "Is there a branch to the left?"
  [loc]
  ;Precondition that node is not nil
  {:pre [(not (nil? loc))]}
  (if (zip/left loc)
    (zip/branch? (zip/left loc))))

(defn push-down-indents
  "Given a particular location in the tree - turn leading spaces into tree
nodes."
  [loc]
  (if (zip/end? loc)
    (zip/root loc)
    (if (has-indent loc)
      (let [reduced-indent-node (drop-one-indent (zip/node loc))]
        (if (is-left-branch loc)
          (recur (-> loc
                     zip/left
                     (zip/append-child reduced-indent-node)
                     zip/right
                     zip/remove))
          (recur (-> loc
                     (zip/insert-left [])
                     zip/remove
                     (zip/append-child reduced-indent-node)))))
      (recur (zip/next loc)))))

(defn transform-to-sass-nested-tree-without-zipper
  "Given a sass string return a nested vector."
  [sass-str]
  {:pre  [(= (class sass-str) java.lang.String)]}
  (push-down-indents (zip/vector-zip (get-lines sass-str))))

(defn transform-to-sass-nested-tree
  "Given a sass string, return a zipper."
  [sass-str]
  {:pre  [(= (class sass-str) java.lang.String)]
   :post [(not (nil? %))]}
  (zip/vector-zip (transform-to-sass-nested-tree-without-zipper sass-str)))

8. Ensure that the file test/sass_dsl/parse_test.clj has the following contents:

(ns sass-dsl.parse-test
  (:require [clojure.test :refer :all]
            [sass-dsl.parse :as parse]
            [sass-dsl.common-test :as common-test]))

(deftest get-lines-test
  (testing "Ensure we can splits the lines into a sequence."
    (is (= (count
         (parse/get-lines common-test/basic-css))
         9))))

(deftest push-down-test
  (testing "Ensure push-down-indents returns a value."
    (is (=
         (parse/transform-to-sass-nested-tree-without-zipper
         common-test/basic-css)
         ["table.hl" ["margin: 2em 0" "td.ln" ["text-align: right"]] "li"
["font:" ["family: serif" "weight: bold" "size: 1.2em"]]]))))

9. Ensure that the file src/sass_dsl/transform.clj has the following contents:

(ns sass-dsl.transform
  (:require [clojure.zip :as zip]
            [clojure.string :refer [join split triml] :as str]
            [sass-dsl.common :as common]))

(defn has-colon-suffix
  "Does this element end in a colon - ie is it potentially an attribute key?"
  [elem]
  (= : (last elem)))

(defn drop-colon-suffix
  "Remove the colon suffix from this string."
  [input-string]
  (join (drop-last input-string)))

(defn ins-replacement
  "Insert a replacement."
  [loc reps]
  (if (empty? reps)
    loc
    (recur
     (zip/insert-right loc (first reps))
     (rest reps))))

(defn get-c-s-suffix-replacements
  "Replace the sass suffix for css and prefix it to everything in the
collection."
  [cs coll]
  (let [cs-minus-suffix (drop-colon-suffix cs)]
    (map #(str cs-minus-suffix "-" %) coll)))

(defn flatten-colon-suffixes
  "If this has a colon suffix - then prepend it to the attribute key of the
children."
  [loc]
  {:pre  [(not (nil? loc))]}
  (let [is-c-s (has-colon-suffix (zip/node loc))
        c-s-children (if is-c-s (zip/node (zip/next loc)))
        c-s-replacements (if is-c-s (get-c-s-suffix-replacements (zip/node
loc) (zip/node (zip/next loc))))]
    (if (zip/end? loc)
      (zip/root loc)
      (recur
       (zip/next
        (if is-c-s
          (-> loc
              zip/next
              zip/remove
              (ins-replacement c-s-replacements)
              zip/remove)
          loc))))))

(defn is-parent-selector
  "Is the parent at the zipper location a selector?"
  [loc]
  {:pre  [(not (nil? loc))]}
  (if (and
       (common/is-element loc)
       (common/get-parent loc))
    (common/is-selector (common/get-parent loc))))

(defn parent-name
  "Get the name of the parent at the zipper location."
  [loc]
  {:pre  [(not (nil? loc))]}
  (if (and
       (common/is-element loc)
       (common/get-parent loc))
    (zip/node (common/get-parent loc))))

(defn is-selector-and-parent-is-selector
  "Is the element at the zipper location a selector and does it have a
selector parent?"
  [loc]
  (and
   (common/is-selector loc)
   (is-parent-selector loc)))

(defn zip-remove-selector-and-children
  "Given a zipper location, remove the selector and its children."
  [loc]
  (-> loc
      zip/right
      zip/remove
      zip/remove))

(defn pull-selector-and-child-up-one
  "Given a zipper location - remove it and put it back one level up with a
new name."
  [loc new-node-name]
  (zip/insert-right
   (zip/insert-right
    (if (common/is-first-element loc) ;handle case where two selectors
chained
      (zip-remove-selector-and-children loc)
      (zip/up
       (zip-remove-selector-and-children loc)))
    (zip/node (zip/right loc)))
   new-node-name))

(defn sass-de-nest
  "Flatten out selectors and attributes, prefixing the parent names."
  [loc]
  {:pre  [(not (nil? loc))]}
  (let [is-element (common/is-element loc)
        is-selector (if is-element (common/is-selector loc))
        has-parent (common/get-parent loc)
        parent-is-selector (is-parent-selector loc)
        parent-name (parent-name loc)
        is-selector-and-parent-is-selector
        (is-selector-and-parent-is-selector
        loc)
        node-name (if is-element (zip/node loc))
        new-node-name (if (and is-element has-parent)
                        (if is-selector-and-parent-is-selector (str
parent-name " " node-name)))]
    (if (zip/end? loc)
      (zip/root loc)
      (if is-selector-and-parent-is-selector
        (recur
         (pull-selector-and-child-up-one loc new-node-name))
        (recur (zip/next loc))))))

(defn sass-de-nest-flattened-colons-vec-zip
  "Wrap the sass-de-nest in a vector zipper."
  [colon-vec]
  (zip/vector-zip (sass-de-nest (zip/vector-zip colon-vec))))

(defn is-constant-declaration
  "Is this a sass constant declaration?"
  [tree-node]
  (if (string? tree-node)
    (= $ (first (take 1 (clojure.string/triml tree-node))))))

(defn get-map-pair
  "Given a space separated string return a map key value pair."
  [input-dec]
  (let [[the-key the-val] (split (triml input-dec) #" ")]
    {(drop-colon-suffix the-key) the-val}))

(defn extract-constants
  "Given a zipper location - return the sass constant declarations as a map."
  [loc previous-c-d-map]
  (let [is-c-d (is-constant-declaration (zip/node loc))
        c-d-pair (if is-c-d (get-map-pair (zip/node loc)))
        c-d-map (if is-c-d (conj c-d-pair previous-c-d-map))
        curr-map (if is-c-d c-d-map previous-c-d-map)]
    (if (zip/end? loc)
      curr-map
      (recur
       (zip/next loc) curr-map))))

(defn strip-constant-declarations
  "Given a zipper location - remove the sass constant declarations."
  [loc]
  (let [next-loc (zip/next loc)
        is-c-d (is-constant-declaration (zip/node next-loc))]
    (if (zip/end? loc)
      (zip/root loc)
      (recur
       (if is-c-d
         (if (empty? (zip/node (zip/remove next-loc)))
           (zip/remove (zip/remove next-loc))
           (zip/remove next-loc))
         next-loc)))))

(defn is-constant-reference
  "Given a zipper location - see if this refers to a sass constant."
  [loc]
  (if (and (string? loc)
           (not (is-constant-declaration loc)))
    (.contains  loc "$")))

(defn replace-const-references
  "Given a string of references to constants, replace them with the values
from the constant declarations in the map."
  [start-string my-map]
  (letfn [(replace-values [current-string [reference value]]
            (if (.contains current-string reference)
              (str/replace current-string reference value)
              current-string))]
    (reduce replace-values start-string my-map)))

(defn replaced-constants-structure
  "Given a zipper location and a replacement map for constant references,
step through and replace them."
  [loc const-refs-map]
  (let [is-c-r (is-constant-reference (zip/node  loc))]
    (if (zip/end? loc)
      (zip/root loc)
      (recur
       (zip/next
        (if is-c-r
          (zip/replace loc
                       (replace-const-references (zip/node loc)
const-refs-map))
          loc)) const-refs-map))))

(defn replaced-constants-structure-vec-zip
  "Constant reference replacement - with a zipper for the input location."
  [loc const-refs-map]
  (replaced-constants-structure (zip/vector-zip loc) const-refs-map))

(defn replaced-constants-structure-vec-zip-vz
  "Constant reference replacement - with a zipper for the return output."
  [loc const-refs-map]
  (zip/vector-zip (replaced-constants-structure-vec-zip loc const-refs-map)))

10. Ensure that the file test/sass_dsl/transform_test.clj has the following contents:

(ns sass-dsl.transform-test
  (:require [clojure.test :refer :all]
            [sass-dsl.parse :as parse]
            [sass-dsl.transform :as transform]
            [sass-dsl.common-test :as common-test]
            [sass-dsl.parse-test :as parse-test]
            [sass-dsl.common-test :as common-test]))

(def sass-simpler-nesting-flattened-colons
  "Given a nested vector of sass declarations - flatten the parents down to
prefixes."
  (transform/flatten-colon-suffixes (parse/transform-to-sass-nested-tree
common-test/basic-css)))

(deftest flatten-parents
  (testing "Ensure a nested vector of sass declarations is flattened the
parents down to prefixes."
    (is (=
         sass-simpler-nesting-flattened-colons
         ["table.hl" ["margin: 2em 0" "td.ln" ["text-align: right"]] "li"
["font-size: 1.2em" "font-weight: bold" "font-family: serif"]]))))

;referred to in render-test
(def sass-denested-simpler-nesting-flattened-colons
  "wrap the sass-de-nest in a vector zipper"
  (transform/sass-de-nest-flattened-colons-vec-zip
sass-simpler-nesting-flattened-colons))

(deftest flatten-colon-suffixes-test
  (testing "Ensure colon suffixes are flat."
    (is (=
         sass-denested-simpler-nesting-flattened-colons
         [["table.hl" ["margin: 2em 0"] "table.hl td.ln" ["text-align:
right"] "li" ["font-size: 1.2em" "font-weight: bold" "font-family: serif"]]
nil]))))

(def sass-constants-nested-tree
  "Given a sass string return a nested vector."
  (parse/transform-to-sass-nested-tree common-test/basic-css-constants))

(def constants-map (transform/extract-constants sass-constants-nested-tree
{}))

(deftest sass-constants-test
  (testing "Ensure we get sass constants."
    (is (=
         constants-map
         {"$blue" "#3bbfce", "$margin" "16px"}))))

(def sass-constants-stripped-nested-tree
(transform/strip-constant-declarations
sass-constants-nested-tree))

(deftest remove-constants-test
  (testing "Ensure we stripped the constant declarations."
    (is (=
         sass-constants-stripped-nested-tree
         [".content-navigation" ["border-color: $blue"
"color: darken($blue, 9%)"] ".border" ["padding: $margin / 2"
"margin: $margin / 2" "border-color: $blue"]]))))

(deftest remove-constants
  (testing "Ensure we stripped the constant declarations."
    (is (=
         (transform/replace-const-references "border-color: $blue" {"$blue"
"#3bbfce" "$margin" "16px"})
         "border-color: #3bbfce"))))

(deftest replaced-constants-structure
  (testing "Ensure we replaced the constants structure."
    (is (=
         (transform/replaced-constants-structure-vec-zip
          sass-constants-stripped-nested-tree
          constants-map)
         [".content-navigation" ["border-color: #3bbfce" "color:
darken(#3bbfce, 9%)"] ".border" ["padding: 16px / 2" "margin: 16px / 2"
"border-color: #3bbfce"]]))))

11. Ensure that the file src/sass_dsl/render.clj has the following contents:

(ns sass-dsl.render
  (:require [clojure.zip :as zip]
            [sass-dsl.common :as common]))

(defn output-css
([loc acc]
   (let [is-element (common/is-element loc)
         has-parent (common/get-parent loc)
         is-loc-last-element (common/is-last-element loc)
         is-loc-selector (common/has-no-colon loc)
         prefix (if (not is-loc-selector) " ")
         suffix (if is-loc-selector
                  " { "
                  (if is-loc-last-element
                    "; } "
                    "; "))
         final-line (if is-element
                      (str prefix (zip/node loc) suffix))]
     (if (zip/end? loc)
       acc
       (recur
        (zip/next loc)
        (str acc
             final-line)))))
  ([loc] (output-css loc "")))

12. Ensure that the file test/sass_dsl/render_test.clj it has the following contents:

(ns sass-dsl.render-test
  (:require [clojure.test :refer :all]
            [sass-dsl.render :as render]
            [sass-dsl.transform :as transform]
            [sass-dsl.transform-test :as transform-test]))

(deftest basic-css-render-test
  (testing "Ensure we get CSS output."
    (is (=
         (render/output-css
transform-test/sass-denested-simpler-nesting-flattened-colons
"")
         "table.hl {   margin: 2em 0; } table.hl td.ln {   text-align:
right; } li {   font-size: 1.2em;   font-weight: bold;   font-family:
serif; } "))))

(deftest constant-replacement-render-test
  (testing "Ensure we get CSS."
    (is (=
         (render/output-css
          (transform/replaced-constants-structure-vec-zip-vz
           transform-test/sass-constants-stripped-nested-tree
           transform-test/constants-map)
          "")
         ".content-navigation {   border-color: #3bbfce;   color:
darken(#3bbfce, 9%); } .border {   padding: 16px / 2;   margin: 16px /
2;   border-color: #3bbfce; } "))))

Testing the Solution

In a command prompt in the project directory, run the following:

lein test

You should expect to see the following results:

lein test sass-dsl.common-test

lein test sass-dsl.core-test

lein test sass-dsl.parse-test

lein test sass-dsl.render-test

lein test sass-dsl.transform-test

Ran 12 tests containing 12 assertions.
0 failures, 0 errors.

If not, check the code above to see if something is incorrect.

Notes on the Recipe

This is an outline of the code, as the structure is built up.

The first thing we’ll look at is the test get-lines-test in the namespace parse-test, which tests the function get-lines in the namespace parse. This acts on the SASS contained in basic-css in the namespace common-test.

You can see from the test expectations that our original SASS string has been split into a vector and that it has maintained its indentation information.

The next thing we’ll look at is the test push-down-test in the namespace parse-test, which tests the function transform-to-sass-nested-tree-without-zipper in the namespace parse. This acts on the SASS contained in basic-css in the namespace common-test.

This nests the data structure according to the original SASS tree. In its present form the data structure is an un-nested vector with the tree structure preserved in the indentation spaces at the start of some string elements.

Our goal is to change this representation to a nested vector to represent this tree structure and to strip out the space prefixes. For example:

["a" "  b" "  c"]

We would expect this to change to

["a" ["b" "c"]]

This is done by using the function count-space-prefix to get the number of spaces at the start of a string. This then uses the function has-indent to work out if a zipper location has an indented string. Why not a string input, and what’s this zipper thing? The zipper location allows us to refer to a particular location on the tree and to apply transformations to it without impacting the original tree. You can think of it as a read-only cursor that returns a reference to the new tree.

Then it uses the function left-is-branch to see if the element to the left of the current location is a branch (a collection of child nodes). We’ll use this when traversing elements to be nested to see if there is already a set of child nodes to push the element down to.

This next function drop-one-indent will be used for stripping the indents when we nest elements.

Now we’ll go on to the core of the operation of pushing down nested elements. The next function is a little hard to understand if you haven’t got the zipper traversal idiom in your head. At its simplest, you can traverse a zipper like this:

(loop [loc zipped-vector]
  (if (zip/end? loc)
    (zip/root loc)
     (recur (zip/next loc))))

This will step through each of the elements without doing anything. If you wanted to see what a particular element was, then you might do it like this.

So now we move on to our actual implementation of the push-down. This basically has three choices:

Image Has indent and nested node to the left.

Image Has indent and no nested node to the left.

Image Doesn’t have an indent.

This can be seen in the two ifs.

Then we do a read, remove, and insert. This is illustrated in Table 12.3. Assume the location is pointing at the "   b" element.

Image

Table 12.3 Read, Remove, and Insert

OK, so bringing together the if decision tree and our removal and insert code, we have the push-down-indents function.

You can see that the original tree structure from the SASS code indents is now captured in the nested levels of the vector.

Now we’re going to apply a transformation to do colon-suffix selectors. You’ll see an opportunity in the data structure above: font: is not a selector but a colon-suffix element shorthand to expand down to its children. So this

["font:" ["family: serif" "weight: bold" "size: 1.2em"]]

becomes this:

["font-family: serif" " font-weight: bold" " font-size: 1.2em"]

To make that even simpler, the following structure

["a:" ["b" "c"]]

would become

 ["a-b" "a-c"]

So let’s get started. First, to see if a given string has a colon suffix, we use the function has-colon-suffix.

Next we provide the ability to strip the suffix from the end of a string by using the function drop-colon-suffix.

Next we add the ability to insert the “replacement children” (i.e., the new font-family and font-weight elements) into a particular location provided using the function insert-replacement.

Next we use a function to generate the new colon-suffix children, get-c-s-suffix-replacements.

Now we’ll put it all together. At a high level this function acts out: “If this is a colon-suffix element, then remove this element and the children and insert a bunch of colon-suffix replacements.” The function get-c-s-suffix-replacements is illustrated in Table 12.4, assuming the location is at element "a: ".

Image

Table 12.4 An Illustration of the Function get-c-s-suffix-replacements

Now we’ll focus on the function flatten-colon suffixes in the namespace transform. In particular, we’ll look at the corresponding test flatten-colon-suffixes-test in the namespace transform-test.

You can see the test expectation:

["table.hl"
 ["margin: 2em 0" "td.ln" ["text-align: right"]]
 "li"
 ["font-size: 1.2em" "font-weight: bold" "font-family: serif"]]

You can see that the font: colon-suffix element has been removed, and its children replaced with the font- prefix.

You’ll notice that our data structure still has three levels of nesting. This reflects the original SASS tree structure. You’ll also notice that the CSS that gets generated only has two levels of nesting. We’ll address this issue in the next section.

Now we’re going to apply a transformation to change the nesting to a flatter, two-level style. You’ll notice that when we went from the initial SASS tree structure to the CSS tree structure, we went from three levels of nesting to two.

Image

You’ll also notice that the second-level selector td.ln has been augmented by the previous select table.h1. We can do a simpler representation of this transformation in a Clojure data structure:

Image

So now we’ll dig into it:

First, we’ll work out whether a particular location on a zipper traversal is an element and not a branch (collection of child nodes) or a non-location (e.g., right of the right-most element in a collection). To do this, we use the function is-element in the common namespace.

Next we’ll use the function has-right-element to work out if a particular location has an element to the right; that is, we ask the question, “Is this element the last in a collection?” We’ll use this to work out if an element is a selector.

Next we’ll combine the previous two functions to work out if a particular element is a selector. To do this, we use the function is-selector in the common namespace.

Next we’ll use a function parent-exists to see if a particular location has a parent.

Next we’ll use the previous functions to confirm that the parent is a selector. For this we use the function is-parent-selector in the transform namespace.

Next we’ll use the function parent-name to get the name of the parent selector.

Now we’ll identify is an element is a selector that has a parent selector (the whole use case of this section). We do this using the function is-selector-and-parent-is-selector in the transform namespace.

Now given a particular location, we’ll remove the selector and its children. We can illustrate this transformation as the following, assuming the location is pointing at the "c":

Image

This is implemented using the function zip-remove-selector-and-children in the transform namespace.

Now we’ll use two functions to identify if a location is the first element in a branch (collection). The functions are is-first-element and is-last-element in the common namespace.

This next function does the heavy lifting of this section. Visually we want to do this transformation:

Image

In our implementation there is an if that handles the case where a selector and children were the only elements in a collection. We can explain the logic behind this if with a table:

Image

Now that we understand the if logic, we can explain the behavior of our zipper traversal inside the pull-selector-and-child-up-one function. We’ll just do the simple case. We’ll assume the location is pointing to the "c" element.

Image

Now let’s pull it all together. Our sass-de-nest function traverses the tree and where an element is a selector with a parent selector, it pulls the child selector and its nodes up a level.

We’ll look at the function sass-de-nest-flattened-colons-vec-zip in the transform namespace. This is tested by flatten-colon-suffixes-test in the transform-test namespace. You can see in the test expectation we have:

["table.hl"
 ["margin: 2em 0"]
 "table.hl td.ln"
 ["text-align: right"]
 "li"
 ["font-size: 1.2em" "font-weight: bold" "font-family: serif"]]

You can see our three-level structure has come down to a two-level structure. Our next step will be to generate a result—the actual CSS.

Now we’re going to generate the CSS. Ok, so we’ve traversed trees and transformed them until you’ve probably gone stir crazy. Now let’s get a result out.

First we’ll redefine our concept of a selector, from a string that is followed by a collection to a string that doesn’t have a colon in it. The function we’ll use is has-no-colon in the common namespace.

Next we’ll use a function to generate the CSS. Compared to all the other work we’ve been doing, this is relatively simple (mainly because we did a lot of preparation work to get the data structure into good shape).

The key line is this one, with the prefix, value, and suffix:

        final-line (if is-element
                     (str prefix (zip/node loc) suffix)) ]

You can see this in the parse namespace in the function output-css.

We can summarize what is going on with a table (see Table 12.5) and a simple dataset like ["a" ["b"]]:

Image

Table 12.5 Summary of Function

The only other trick is that we’re returning a string instead of a zipped list. This means we need to pass the string in an accumulator parameter, and so when we reach the end of the zipper, we need to return the accumulated string.

We can see this in the test basic-css-render-test in the render-test namespace. You can see the test expectation:

table.hl {
  margin: 2em 0;
}

table.hl td.ln {
  text-align: right;
}

li {
  font-size: 1.2em;
  font-weight: bold;
  font-family: serif;
}

Fantastic! We’ve used the original nesting example from the start of the chapter. We’re on the road to implementing our own DSL.

Let’s do one more thing. Let’s add the ability to define constants to get some reuse and provide high-level control.

Now we’re going to apply a transformation by populating some simple constants. Our goal is to implement the mapping of constants into our data structure. At a high level we want to do what is shown in Table 12.6.

Image

Table 12.6 Constants

We’ve already gone through a series of functions to generate some CSS. We need to splice this into our existing process. We’ll do it like this:

Image

So in our data structure, we want to do a transformation from this:

["$a: 12" "b" ["c: $a"]]

to this:

["b" ["c: 12"]]

We’ll start off by using a new string for our new starting SASS example. You can see the following in the common-test namespace:

(def basic-css-constants
  "$blue: #3bbfce
$margin: 16px

.content-navigation
  border-color: $blue
  color: darken($blue, 9%)

.border
  padding: $margin / 2
  margin: $margin / 2
  border-color: $blue")

Now we’ll use a function to determine if a string represents a constant declaration. You can see the function is-constant-declaration in the namespace parse.

Now, given a string that represents a constant declaration, we’ll compose that into a key-value pair that can be stored in a map. For example,

Image

This is implemented using the function get-map-pair in the transform namespace.

Next we’ll traverse the tree and build a map of all the constant declarations. The function extract-constants does the following steps:

1. Is this a constant declaration?

2. If yes, extract out the pair.

3. If yes, build a map of the pair.

4. If yes, add the new map on to the accumulator map.

This combines some of our previous functions for taking a string, splitting it, and turning it into a nested vector. Then this loads up our SASS string with constants into a zipped vector.

The function extract-constants is tested using sass-constants-test in the transform-test namespace. This has the following test expectation:

{"$margin" "16px", "$blue" "#3bbfce"}

Great! You can see we’ve extracted the constant declarations.

Next we’ll remove the constant declarations from the nested vector. The key part of our function strip-constant-declarations is these two lines:

(if is-c-d
  (zip/remove (zip/next loc))

They say, “If this next location is a constant declaration, remove it!”

So let’s see the test. This is tested using remove-constants-test in the transform-test namespace. This has the following test expectation:

[".content-navigation"
 ["border-color: $blue" "color: darken($blue, 9%)"]
 ".border"
 ["padding: $margin / 2" "margin: $margin / 2" "border-color: $blue"]]

Great. We’ve stripped out the constant declarations. Now we need to start putting the constants back in where they are referred to. To do that we need to identify the points that are constant references. We’ll do that with our function is-const-reference. This basically asks the question, “Is this not a constant reference and does it contain a dollar sign?”

Now we’ll use a function to replace the constant references in a given string with the values in our map. Our function replace-const-references walks the map, with the function, “If this key is in my string, replace it with the value.”

We test this using remove-constants in the transform-test namespace. This has the test expectation:

"border-color: #3bbfce"

Now we’ll pull all of that together to walk the tree and replace the constant references. The key lines of the function replaced-constants-structure in the transform namespace are:

(if is-c-r
  (zip/replace loc
    (replace-const-references
      (zip/node loc)
      const-refs-map))
   loc))

This says that if it is a constant reference, replace the references in the string with those from the map, then replace the old string at this place with the new one. Here is the rest of the function.

We can test this using replaced-constants-structure in the transform-test namespace. This has the following test expectation:

 [".content-navigation"
 ["border-color: #3bbfce" "color: darken(#3bbfce, 9%)"]
 ".border"
 ["padding: 16px / 2" "margin: 16px / 2" "border-color: #3bbfce"]]

Great. We can see that our constant references have been replaced by our constant declarations that were passed into the map.

Now let’s generate some CSS. We’ll look at the function sass-to-css in the core namespace. This has the following internals:

(let [tree (parse/transform-to-sass-nested-tree sass-val)]
    (-> tree
        (transform/replaced-constants-structure-vec-zip-vz  (transform/extract-
constants tree {}))
        transform/strip-constant-declarations
        render/output-css)))

You can see this has a parse, a transform, and a render phase encapsulated in namespaces.

Now we’ll look at the test basic-css-constants-test in the core-test namespace. You can see this has the following test expectation:

.content-navigation {
  border-color: #3bbfce;
  color: darken(#3bbfce, 9%);
}

.border {
  padding: 16px / 2;
  margin: 16px / 2;
  border-color: #3bbfce;
}

Now you can see the constants have been replaced, but this is still not valid CSS. The SASS function references like darken(#3bbfce, 9%) and the mathematical operations like 16px / 2 still exist. Solving these is quite similar to the transformations with zippers we’ve already done. In fact, this chapter is long enough, and you wouldn’t learn anything new from a DSL point of view by doing it. So we’ll leave it as an exercise to do on your own.

Conclusion

We have implemented part of the SASS DSL from Ruby as an internal DSL in Clojure using zippers to do tree transformations.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.133.126.199