-
Notifications
You must be signed in to change notification settings - Fork 151
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 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 <
.)
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>
...
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.
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.)
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.
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>
<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.