Skip to content

Table and Layout Tutorial, Part 3: Simple Transformations

srid edited this page Aug 29, 2011 · 12 revisions

Part 1: The Goal
Part 2: Resources and Selectors
Part 3: Simple Transformations
Part 4: Duplicating Elements and Nested Transformations
Part 5: Frozen Transformations, Including Snippets and Templates

(Comments to Brian Marick, please.)

In this part, you'll learn how to transform one tree structure of Enlive nodes into another. We'll start with this part of the tutorial layout HTML's node:

jcrit.server=> (pprint (select layout [:div#wrapper]))
({:tag :div,
  :attrs {:id "wrapper"},
  :content ("\n       " {:type :comment, :data "body"} "\n    ")})

The simplest transformation does nothing:

jcrit.server=> (pprint (transform layout [:div#wrapper] identity))
({:tag :html,
  :attrs nil,
  :content
  ("\n"
   {:tag :head,
    :attrs nil,
    :content
   ...
     {:tag :div,
      :attrs {:id "wrapper"},
      :content ("\n       " {:type :comment, :data "body"} "\n    ")}
     "\n")}
   "\n\n")})

transform takes three arguments. The first is an entire tree or sequence of trees. The second is a selector that picks some subtrees to transform. The third is a function that takes a subtree and returns a new one. Notice that the return value is the entire transformed input, not merely the transformed subtrees.

As a next step, let's assume some earlier use of html-resource gave us this list of nodes:

(def page-content '({:tag :p, :content ("Hi, mom!")}))

Here's a transform that inserts that into the nodes derived from the tutorial layout HTML, putting it inside the <div id="wrapper">:

jcrit.server=> (pprint (transform layout
                                  [:div#wrapper]
                                  (fn [a-selected-node]
                                    (assoc a-selected-node :content page-content))))
({:tag :html,
 ...
     {:tag :div,
      :attrs {:id "wrapper"},
      :content ({:tag :p, :content ("Hi, mom!")})}
     "\n")}
   "\n\n")})

That third argument is a lot of characters to type to mean "replace the content", so Enlive provides an abbreviation:

jcrit.server=> (pprint (transform layout
                                  [:#wrapper]
                                  (content page-content)))
({:tag :html,
 ...
     {:tag :div,
      :attrs {:id "wrapper"},
      :content ({:tag :p, :content ("Hi, mom!")})}
     "\n")}
   "\n\n")})

It's the same result. Note well that the content function, like the other transforming functions you'll see shortly, returns another function. Think of content as an abbreviation for make-content-setter.

You can give multiple arguments to content. Moreover, if any of the arguments has been wrapped up in a sequential, you needn't bother unwrapping them:

jcrit.server=> (pprint (transform layout 
                                  [:#wrapper] 
                                  (content "Say it!" 
                                           [[[page-content]]]  ; <== wrapped
                                           "Say it again!"
                                           page-content)))
     ...
     {:tag :div,
      :attrs {:id "wrapper"},
      :content
      ("Say it!"
       {:tag :p, :content ("Hi, mom!")}  ; <== unwrapped
       "Say it again!"
       {:tag :p, :content ("Hi, mom!")})}
     ...

The tutorial layout HTML has a <script id="jquery_code"> tag that should be transformed to contain a particular page's jQuery code. That code's provided as a vector of strings:

(def jquery-content ["$('input.true_name').first().focus();"
                     "if (a < b) foo();"])

To insert it into the right place, this suffices:

jcrit.server=> (pprint (transform layout [:script#jquery_code]
                                  (content "\njQuery(function() { \n"
                                           jquery-content
                                           "\n});\n")))
   ...
     {:tag :script,
      :attrs {:type "text/javascript", :id "jquery_code"},
      :content
      ("\njQuery(function() { \n"
       "$('input.true_name').first().focus();"
       "if (a < b) foo();"
       "\n});\n")}
     "\n")}
   ...

Notice that the string input is not subject to HTML escapes. (The < stays an <; it doesn't become an &lt.)

emit*

The previous sentence doesn't really provide the right assurance about HTML escapes. transform produces a huge pile of nodes–how do we know that escaping doesn't happen after those nodes are converted into the string to be sent to the browser in an HTML Response?

Nodes are converted to strings with emit*. Note the plural: the output is a sequence of strings, not a single joined string. So here's the way to see the final result:

jcrit.server=> ;; first, stash the transformed value
jcrit.server=> (def transformed (transform layout 
                                           [:script#jquery_code]
                                           (content "\njQuery(function() { \n"
                                                    jquery-content
                                                    "\n});\n")))
#'jcrit.server/transformed
jcrit.server=> (print (apply str (emit* transformed)))
<!DOCTYPE html>
<html>
<head>
    <title>Critter4Us</title>
    <link type="text/css" rel="stylesheet" href="/css/reset.css" />
    <script type="text/javascript" src="/js/jquery.js"></script>
    <script type="text/javascript" src="/js/c4.js"></script>
    <script type="text/javascript" id="jquery_code">
jQuery(function() { 
$('input.true_name').first().focus();if (a < b) foo();
});
</script>
...

at

We now have the tools to transform the tutorial layout HTML nodes into their final form: first, substitute the page contents, then add the jQuery code:

jcrit.server=> (pprint (-> layout
                           (transform [:#wrapper] (content page-content))
                           (transform [:#jquery_code] 
                                      (content "\njQuery(function() { \n"
                                               jquery-content
                                               "\n});\n"))))
({:tag :html,
 ...
     {:tag :script,
      :attrs {:type "text/javascript", :id "jquery_code"},
      :content
      ("\njQuery(function() { \n"
       "$('input.true_name').first().focus();"
       "if (a < b) foo();"
       "\n});\n")}
  ...
     {:tag :div,
      :attrs {:id "wrapper"},
      :content ({:tag :p, :content ("Hi, mom!")})}
     "\n")}
   "\n\n")})

That works, but it's not wildly appealing. The at macro lets it look better:

jcrit.server=> (pprint
                 (at layout 
                     [:#wrapper] 
                     (content page-content)

                     [:#jquery_code]
                     (content "\njQuery(function() { \n"
                              jquery-content
                              "\n});")))
({:tag :html,
  ...
     {:tag :script,
      :attrs {:type "text/javascript", :id "jquery_code"},
      :content
      ("\njQuery(function() { \n"
       "$('input.true_name').first().focus();"
       "if (a < b) foo();"
       "\n});")}
  ...
     {:tag :div,
      :attrs {:id "wrapper"},
      :content ({:tag :p, :content ("Hi, mom!")})}
     "\n")}
   "\n\n")})

Note: Don't assume the transformations will be made in a particular order.

Other content transformations

The original tutorial layout HTML had an important comment inside the wrapper <div>:

    <div id="wrapper">
       <!--body-->
    </div>

We replaced it. How could we retain it? With append:

jcrit.server=> (pprint 
                 (transform layout [:#wrapper] 
                            (append page-content)))
({:tag :html,
     {:tag :div,
      :attrs {:id "wrapper"},
      :content
      ("\n       "
       {:type :comment, :data "body"}   ; <<== Still there.
       "\n    "
       {:tag :p, :content ("Hi, mom!")})}
     "\n")}
   "\n\n")})

append adds to the end of the selected element's content. prepend adds to the beginning.

(Note: I'm not trying to document all the transformations. See the core documentation for the complete list. Alternately, look in the source. By the end of this tutorial, I hope the terminology and mechanisms used in the transformation functions will be clear.)

Whole-tag transformations

You can substitute a new element for an old one, replacing the tag as well as the contents:

jcrit.server=> (pprint (at layout 
                           [:head] (substitute page-content)
                           [:body] (substitute page-content)))
({:tag :html,
  :attrs nil,
  :content
  ("\n"
   {:tag :p, :content ("Hi, mom!")}
   "\n"
   {:tag :p, :content ("Hi, mom!")}
   "\n\n")})

Another transformation, after, adds its arguments just after the selected element. It doesn't change the selected element's content, so (for example) you'd use after to add a new <td> element to a table row. before adds its arguments before the selected element.

You can wrap selected nodes in other tags:

jcrit.server=> (pprint (transform layout [:div#wrapper] 
                                  (wrap :div
                                  {:id "superdiv", :class "wasted space"})))
({:tag :html,
 ...
     {:tag :div,                                        ;; <<<= new
      :attrs {:id "superdiv", :class "wasted space"},   ;; <<<= new
      :content                                          ;; <<<= new
      [{:tag :div,
        :attrs {:id "wrapper"},
        :content
        ("\n       " {:type :comment, :data "body"} "\n    ")}]}

You can also unwrap to replace a tag with its content:

jcrit.server=> (pprint (transform layout [:div#wrapper] unwrap))
({:tag :html,
 ...
   {:tag :body,
    :attrs nil,
    :content
    ("\n    "
     "\n       "
     {:type :comment, :data "body"}
     "\n    "
     "\n")}
   "\n\n")})

Notice the unwrap function is given directly. It's unlike other transformation functions, which take arguments and produce new functions that use those arguments. unwrap can stand alone because there's no argument to give it.

Attribute transformations

You can perform a variety of transformations on attributes.

  • Adding one or more attributes
    jcrit.server=> (pprint (transform layout [:div#wrapper]
                                      (set-attr :NEWBIE "FRED", :OLDIE "DAWN")))
    ...
     {:tag :div,
      :attrs {:OLDIE "DAWN", :NEWBIE "FRED", :id "wrapper"},
      :content ("\n       " {:type :comment, :data "body"} "\n    ")}
    ...
  • Deleting one or more attributes
    jcrit.server=> (pprint (transform layout [:div#wrapper]
                                      (remove-attr :id)))
    ...
     {:tag :div,
      :attrs {},
      :content ("\n       " {:type :comment, :data "body"} "\n    ")}
    ...
  • Adding one or more whitespace-separated classes
    jcrit.server=> (pprint (transform layout [:div#wrapper]
                                      (add-class "highlight" "plain-styled")))
    ...
     {:tag :div,
      :attrs {:class "highlight plain-styled", :id "wrapper"},
      :content ("\n       " {:type :comment, :data "body"} "\n    ")}
     "\n")}
    ...
  • Removing one or more classes (no example)

In summary

We now know how to do this:

jcrit.server=> (println 
                 (apply str
                        (emit* 
                          (at (html-resource "jcrit/views/layout.html")
                              [:#wrapper]
                              (content page-content)

                              [:#jquery_code]
                              (content "\njQuery(function() { \n"
                                       jquery-content
                                       "\n});")))))
<!DOCTYPE html>
<html>
<head>
    <title>Critter4Us</title>
    <link type="text/css" rel="stylesheet" href="/css/reset.css" />
    <script type="text/javascript" src="/js/jquery.js"></script>
    <script type="text/javascript" src="/js/c4.js"></script>
    <script type="text/javascript" id="jquery_code">
jQuery(function() { 
$('input.true_name').first().focus();if (a < b) foo();  ;; <<<====
});</script>
</head>
<body>
    <div id="wrapper"><p>Hi, mom!</p></div>             ;; <<<====
</body>

</html>

That's hardly ideal. Both the boilerplate code and global variables should be factored out into a helper function. We'll see how to create it in Part 5. That's straightforward, though. While we're thinking about transformations, let's get down and dirty with a more complex case.

Part 4: Duplicating Elements and Nested Transformations