Skip to content
jcromartie edited this page Sep 13, 2010 · 26 revisions

(Original version of this tutorial – Adrian Cuthbertson 2009-04-23 – http://clojure.googlegroups.com/web/enlive-tut1.txt).

Introduction

Enlive is a library for applying clojure based transformations to html documents.
The basic procedure is that you define a template with rules, each having a CSS like selector which selects certain nodes from a parsed html document, and a directive for applying a transformation to the selected node/s in order to create a new output html document.

The Enlive project is hosted at http://github.com/cgrand/enlive.
The project is under active development, so check the latest master branch to work with at the site.

To install…

    git clone git://github.com/cgrand/enlive.git

The src directory and lib/tagsoup-1.2.jar are needed on your classpath to use Enlive.

This tutorial conducts a repl session to introduce Enlive’s main features.

Html Source

Lets start with a simple example. Say we have a file t1.html containing…

<html>
  <body>
    <h1></h1>
  </body>
</html>

Exploring what we can do from the repl – start your repl making sure the classpath includes clojure.jar, clojure-contrib.jar, enlive/src, envlive/lib/tagsoup-1.2.jar. Then set your namespace as follows…

    user=> (ns x (:use net.cgrand.enlive-html))

This loads enlive-html and sets the ns to x. So, x now contains clojure/core and enlive.

    x=> (html-resource "t1.html")
    ({:tag :html, :attrs nil, :content [{:tag :body, :attrs nil, :content [{:tag :h1, :attrs nil, :content nil}]}]})

From this we see that the html source is parsed from the file into a hash map of nodes representing the structure of the document. Note that html-resource allows you to load source from files, classpath resources, input streams, readers, URL’s etc. Check the source for details.

Here’s another example…

x=> (html-resource (java.io.StringReader. "<html><body><h1></h1></body></html>"))
({:tag :html, :attrs nil, :content [{:tag :body, :attrs nil, :content [{:tag :h1, :attrs nil, :content nil}]}]})

In the deftemplate macro shown below, the html source is determined from the source parameter type, e.g a String implies a file name/path, a Reader implies a Reader source, etc.

Defining Enlive Clojure Templates

Next step is to define a template that will be used to transform the html source…

x=> (deftemplate t1 "t1.html" [] [:html] (content "wargh"))

This says define a template called t1 which transforms source file “t1.html”. We won’t pass any parameters at this stage (hence []). This is followed by the rules for transformation. These comprise one or more selector/directive pairs. In this case, select a node with the tag :html (from the parsed tree described above) and replace it’s content with “wargh”.

x=> t1
#<core$comp__3776$fn__3778 clojure.core$comp__3776$fn__3778@e73e0>

So, t1 has been defined (compiled) as a function

Performing the Transformation

To perform the transformation, call the defined template function (passing parameters if any)…

x=> (apply str (t1))
"<html>wargh</html>"

Why did we use (apply str… ?

x=> (t1)
("<html>" "wargh" "</html>")

Just calling the function performs the transformation, but returns a list of tokens which need to be concatenated into the final output string.

Maybe we actually wanted to set the heading rather than simply replace the :html’s full contents…

x=> (deftemplate t1 "t1.html" [hdr] [:h1] (content hdr))
x=> (apply str (t1 "First Enlive App"))
"<html><body><h1>First Enlive App</h1></body></html>"

That’s more likely what we might do, but the two examples illustrate the basic ideas.

A More Elaborate Example

First we need a more elaborate html file (t2.html)…

<html>
  <body>
    <h1>Sample Header</h1>
    <div id="d1">
      <p class="d"></p>
    </div>
    <div id="d2">
      <ul class="l">
        <li>an item</li>
      </ul>
    </div>
  </body>
</html>

Our goal is to create a Clojure program which will transform this html by…

  • Replacing the sample header
  • Adding a paragraph to div#d1
  • Adding some items to the ul in div#d2

Now the deftemplate…

x=> (deftemplate t2 "t2.html" [hdr para-txt li-itms]
      [:h1] (content hdr)
      [:div#d1 :p] (content para-txt)
      [:div :ul.l :li] (clone-for [item li-itms] (content item)))

x=> (apply str (t2 "Second Enlive App" "Some paragraph stuff" ["one" "two" "three"]))
"<html><body><h1>Second Enlive App</h1><div id=\"d1\"><p class=\"d\">Some paragraph stuff</p></div><div id=\"d2\">
<ul class=\"l\"><li>one</li><li>two</li><li>three</li></ul></div></body></html>"

Note: The html file must exist before the deftemplate is executed as the compilation (see above) includes parsing the file.

That should get you started with Enlive.

At a Lower Level

The selectors are similar to CSS selectors. Refer to the Enlive project site for a table showing Enlive’s selectors and the equivalent CSS selector.

It’s quite useful while developing with Enlive to try things out at a lower level. The at and sniptest macros can be used for this…

Using at

    (def t2-nd (first (html-resource "t2.html")))

That just loads the html and t2-nd now contains the parsed node…

    x=> t2-nd
    {:tag :html, :attrs nil, :content [{:tag :body, :attrs nil, :content [{:tag :h1, :attrs nil, 
     :content ["Sample Header"]} {:tag :div, :attrs {:id "d1"}, :content [{:tag :p, :attrs {
     :class "d"}, :content nil}]} {:tag :div, :attrs {:id "d2"}, :content [{:tag :ul, :attrs {:class "l"},
     :content nil}]}]}]}

Now you can use at to experiment with selectors and transformation directives…

    x=> (at t2-nd [:div :ul.l] (content (to-li ["one" "two"])))
    ({:tag :html, :attrs nil, :content [{:tag :body, :attrs nil, :content [{:tag :h1, :attrs nil,
     :content ["Sample Header"]} {:tag :div, :attrs {:id "d1"}, :content [{:tag :p, :attrs {:class "d"},
     :content []}]} {:tag :div, :attrs {:id "d2"}, :content [{:tag :ul, :attrs {:class "l"},
     :content ({:tag :li, :attrs nil, :content "one"} {:tag :li, :attrs nil, :content "two"})}]}]}]})

Using sniptest

x=> (sniptest "<html><body><span>Hello </span>" 
      [:span] (append "World"))
"<html><body><span>Hello World</span></body></html>"

Transformation Directives

So, every rule has a selector on the left and a transformation directive on the right. The transformation directive is a function that takes one argument (the selected node) and returns a node or sequence of nodes.

Here’s an example…

    x=> (at t2-nd [:div :ul.l] (fn [nd] (prn nd) {:tag :huh :attrs {:wah "foo"} :content nil}))
    ({:tag :ul, :attrs {:class "l"}, :content []}
     {:tag :html, :attrs nil, :content [{:tag :body, :attrs nil, :content [{:tag :h1, :attrs nil, :content ["Sample Header"]} {:tag :div, :attrs {:id "d1"}, :content [{:tag :p, :attrs {:class "d"}, :content []}]} {:tag :div, :attrs {:id "d2"}, :content [{:tag :huh, :attrs {:wah "foo"}, :content nil}]}]}]})

There we’re just printing out the selected node, but you could use it in applying the transformation. We’re returning the replacement node as a :huh tag with a :wah attribute with value “foo”.

All that (content x) does is define a function that assocs x into the :content of the passed node.
Here’s it’s definition…

    (defn content
     "Replaces the content of the node. Values can be nodes or nested collection of nodes." 
     [& values]
      #(assoc % :content (flatten values)))

There are a number of other transformation helper functions in addition to content – wrap, set-attr, remove-attr, etc. Check the source for more info.

This should give you an idea of how powerful Enlive really is and how you could develop sophisticated libraries of selection/transformation functions.

We’ll cover snippets and other more advanced topics in other tutorials.

Summary

Enlive is a powerful html transformation library. It is NOT a templating system in the traditional sense of the word (where code is embedded in html files, e.g JSP, etc).

It is easy to use at a high-level – straight html file transformations, but also has the lower-level features which will allow you to develop very advanced web/content generation applications.

This is functional web development at its best.