- Notifications
You must be signed in to change notification settings - Fork149
Table and Layout Tutorial, Part 3: Simple Transformations
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 toBrian Marick, please.)
In this part, you'll learn how totransform one tree structure of Enlive nodes into another. We'll start with this part of thetutorial 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 ofhtml-resource gave us this list of nodes:
(defpage-content '({:tag:p,:content ("Hi, mom!")}))
Here's atransform that inserts that into the nodes derived from thetutorial layout HTML, putting it inside the<div>:
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 thecontent function, like the other transforming functions you'll see shortly, returnsanother function. Think ofcontent as an abbreviation formake-content-setter.
You can give multiple arguments tocontent. 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!")})} ...
Thetutorial layout HTML has a<script> tag that should be transformed to contain a particular page's jQuery code. That code's provided as a vector of strings:
(defjquery-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<.)
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 happenafter those nodes are converted into the string to be sent to the browser in an HTML Response?
Nodes are converted to strings withemit*. 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 valuejcrit.server=> (deftransformed (transform layout [:script#jquery_code] (content"\njQuery(function() {\n" jquery-content"\n});\n")))#'jcrit.server/transformedjcrit.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>...
We now have the tools to transform thetutorial 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. Theat 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.
The originaltutorial layout HTML had an important comment inside the wrapper<div>:
<divid="wrapper"><!--body--></div>
We replaced it. How could we retain it? Withappend:
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 thecore 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.)
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 useafter to add a new<td> element to a table row.before adds its arguments before the selected element.
You canwrap 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 alsounwrap 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 theunwrap 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.
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)
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><linktype="text/css"rel="stylesheet"href="/css/reset.css"/><scripttype="text/javascript"src="/js/jquery.js"></script><scripttype="text/javascript"src="/js/c4.js"></script><scripttype="text/javascript"id="jquery_code">jQuery(function(){$('input.true_name').first().focus();if(a<b)foo();;;<<<====});</script></head><body><divid="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 inPart 5. That's straightforward, though. While we're thinking about transformations, let's get down and dirty with a more complex case.