Skip to content

Latest commit

 

History

History
400 lines (327 loc) · 15 KB

7-11_enlive.asciidoc

File metadata and controls

400 lines (327 loc) · 15 KB

Templating HTML with Enlive

by Luke VanderHart

Problem

You want to create HTML dynamically based on a template, without using traditional mixed code or DSL-style templating.

Solution

Use Enlive, a Clojure library that takes a selector-based approach to templating HTML. Unlike other template frameworks like PHP, ERB, and JSP, it doesn’t mix code and text. And unlike systems like Haml or Hiccup, it doesn’t use specialized DSLs. Instead, templates are plain old HTML files, and Enlive uses Clojure code to target specific areas for replacement or duplication based on incoming data.

To follow along with this recipe, start a REPL using lein-try:

$ lein try enlive

To begin, create a file post.html to serve as an Enlive template:

<html>
  <head><title>Page Title</title></head>
  <body>
    <h1>Page Title</h1>
    <h3>By <span class="author">Mickey Mouse</span></h3>
    <div class="post-body">
      Lorem ipsum etc...
    </div>
  </body>
</html>
Note
Place this file in the resources/ directory, if you’re using Enlive in the context of a project.

The following Clojure code defines an Enlive template based on the contents of post.html:

(require '[net.cgrand.enlive-html :as html])

;; Define the template
(html/deftemplate post-page "post.html"
  [post]
  [:title] (html/content (:title post))
  [:h1] (html/content (:title post))
  [:span.author] (html/content (:author post))
  [:div.post-body] (html/content (:body post)))

;; Some sample data
(def sample-post {:author "Luke VanderHart"
                  :title "Why Clojure Rocks"
                  :body "Functional programming!"})

To apply the template to the data, invoke the function defined by deftemplate. Since it returns a sequence of strings, in most applications you’ll probably want to concatenate the results into a single string:

(reduce str (post-page sample-post))
Here’s the formatted output:
<html>
  <head><title>Why Clojure Rocks</title></head>
  <body>
    <h1>Why Clojure Rocks</h1>
    <h3>By <span class="author">Luke VanderHart</span></h3>
    </h3><div class="post-body">Functional programming!</div>
  </body>
</html>

See the following discussion section for a detailed explanation of the deftemplate macro and what is actually happening in this code.

Repeating elements

The preceding code simply replaces the values of certain nodes in the emitted HTML. In real scenarios, another common task is to repeat certain items from input HTML, one repetition for each item in the input data. For this task, Enlive provides snippets, which are selections from an input HTML that can then be repeated as many times as desired in the output of another template:

(def sample-post-list
  [{:author "Luke VanderHart"
    :title "Why Clojure Rocks"
    :body "Functional programming!"}
   {:author "Ryan Neufeld"
    :title "Clojure Community Management"
    :body "Programmers are like..."}
   {:author "Rich Hickey"
    :title "Programming"
    :body "You're doing it completely wrong."}])

(html/defsnippet post-snippet "post.html"
  {[:h1] [[:div.post-body (html/nth-of-type 1)]]}
  [post]
  [:h1] (html/content (:title post))
  [:span.author] (html/content (:author post))
  [:div.post-body] (html/content (:body post)))

(html/deftemplate all-posts-page "post.html"
  [post-list]
  [:title] (html/content "All Posts")
  [:body] (html/content (map post-snippet post-list)))

Invoking the defined all-posts-page function now returns an HTML page populated with all three sample posts:

(reduce str (all-posts-page sample-post-list))
Here’s the formatted output:
<html>
  <head><title>All Posts</title></head>
  <body>
    <h1>Why Clojure Rocks</h1>
    <h3>By <span class="author">Luke VanderHart</span></h3>
    <div class="post-body">Functional programming!</div>
    <h1>Clojure Community Management</h1>
    <h3>By <span class="author">Ryan Neufeld</span></h3>
    <div class="post-body">Programmers are like...</div>
    <h1>Programming</h1>
    <h3>By <span class="author">Rich Hickey</span></h3>
    <div class="post-body">You're doing it completely wrong.</div>
  </body>
</html>

In this example, the defsnippet macro defines a snippet over a range of elements in the input HTML, from the <h1> element to the <div class="post-body">.

Then, the deftemplate for all-posts-page uses the result of mapping post-snippet over the content of the body element. Since there are three posts in the sample input data, the snippet is evaluated three times, and there are three posts output in the resulting HTML.

Discussion

Enlive can be slightly difficult to get the hang of, compared to some other libraries. There are several contributing factors to this:

  • It has a more novel conceptual approach than other templating systems (although it bears a lot of similarity to some other non-Clojure templating techniques, such as XSLT).

  • It utilizes functional programming techniques to the fullest, including liberal use of higher-order functions.

  • It’s a large library, capable of many things. The subset of features required to accomplish a particular task is not always evident.

In general, the best way to get past these issues and experience the power and flexibility that Enlive can provide is to understand all the different parts individually, and what they do. Then, composing them into useful templating systems becomes more manageable.

Enlive and the DOM

First of all, it is important to understand that Enlive does not operate on HTML text directly. Instead, it first parses the HTML into a Clojure data structure representing the DOM (Document Object Model). For example, the HTML fragment:

<div id="foo">
  <span class="bar">Hello!</span>
</div>

would be parsed into the Clojure data:

{:tag :html,
  :attrs nil,
  :content
  ({:tag :body,
    :attrs nil,
    :content
    ({:tag :div,
      :attrs {:id "foo"},
      :content
      ({:tag :span, :attrs {:class "bar"}, :content ("Hello!")})})})}

This is more verbose, but it is easier to manipulate from Clojure. You won’t necessarily have to deal with these data structures directly, but be aware that anywhere Enlive says it operates on an element or a node, it means the Clojure data structure for the element, not the HTML string.

Templates

The most important element of these examples is the deftemplate macro. deftemplate takes a symbol as a name, a classpath-relative path to an HTML file, an argument list, and a series of selector and transform function pairs. It emits a function, bound to the same name and of the specified arguments, which, when called, will return the resulting HTML as a sequence of strings.

An Enlive selector is a Clojure data structure that identifies a specific node in the input HTML file. They are similar to CSS selectors in operation, although somewhat more capable. In the example in the solution, [:title] selects each <title> element, [:span.author] each <span> with class="author", etc. More selector forms are described in the following subsection.

A template transform function takes an Enlive node and returns a modified node. Our example uses Enlive’s content utility function, which returns a function that swaps the contents of a node with the value given as its argument.

The return value is not itself a string, but a sequence of strings, each one a small fragment of HTML code. This allows the underlying data structure to be transformed to a string representation lazily. For simplicity, our example uses the string concatenation function str to reduce the result of all-posts-page , but this is actually not optimally performant. To build a string most efficiently, use the Java StringBuilder class, which uses mutable state to build up a String object with the best possible performance. Alternatively, forego the use of strings altogether and pipe the result seq of the template function directly into an output Writer, which most web application libraries (including Ring) can use as the body of an HTTP response (the most common destination for templated HTML).

Selectors

Enlive selectors are data structures that identify one or more HTML nodes. They describe a pattern of data—​if the pattern matches any nodes in the HTML data structure, the selector will select those nodes. A selector may select one, many, or zero nodes from a given HTML document, depending on how many matches the pattern has.

The full reference for valid selector forms is quite complex, and beyond the scope of this recipe. See the formal selector specification for complete documentation.

The following selector patterns should be sufficient to get you started:

[:div]

Selects all <div> element nodes.

[:div.sidebar]

Selects all <div> element nodes with a CSS class of "sidebar".

[:div#summary]

Selects the <div> element with an HTML ID of "summary".

[:p :span]

Selects all <span> elements that are descendants of <p> elements.

[:div.menu :ul :li :span]

Selects only <span> elements inside an <li> element inside a <ul> element inside a <div> element with a CSS style of "menu".

[[:div (nth-child 2)]]

Selects all <div> elements that are the second children of their parent elements. The double square brackets are not a typo—​the inner vector is used to denote a logical and condition. In this case, the matched element must be a <div>, and the nth-child predicate must hold true.

Other predicates besides nth-child are available, as well as the ability to define custom predicates. See the Enlive documentation for more details.

Finally, there is a special type of selector called a range selector that is not specified by a vector, but rather by a map literal (in curly braces). The range selector contains two other selectors and inclusively matches all the nodes between the two matched nodes, in document order. The starting node is in key position in the map literal and the ending node is in value position, so the selector {[:.foo] [:.bar]} will match all nodes between nodes with an ID of "foo" and an ID of "bar".

The example in the solution uses a range selector in the defsnippet form to select all the nodes that are part of the same logical blog post, even though they aren’t wrapped in a common parent element.

Snippets

A snippet is similar to a template, in that it produces a function based on a base HTML file. However, snippets have two major differences from templates:

  1. Rather than always rendering the entire HTML file like a template does, snippets render only a portion of the input HTML. The portion to be rendered is specified by an Enlive selector passed as the third argument to the defsnippet macro, right after the name and the path to the HTML file.

  2. The return values of the emitted functions are Enlive data structures rather than HTML strings. This means that the results of rendering a snippet can be returned directly from the transform function of a template or another snippet. This is where Enlive starts to show its power; snippets can be recycled and reused extensively and in different combinations.

Other than these differences, the defsnippet form is identical to deftemplate, and after the selector, the rest of the arguments are the same—​an argument vector and a series of selector and transform function pairs.

Using Enlive for scraping

Because of its emphasis on selectors and use of plain, unannotated HTML files, Enlive is extremely useful not just for templating and producing HTML, but also for parsing and scraping data from HTML from any source.

To use Enlive to extract data from HTML, you must first parse the HTML file into an Enlive data structure. To do this, invoke the net.cgrand.enlive-html/html-resource function on the HTML file. You may specify the file as a java.net.URL, a java.io.File, or a string indicating a classpath-relative path. The function will return the parsed Enlive data structure representing the HTML DOM.

Then, you can use the net.cgrand.enlive-html/select function to apply a selector to the DOM and extract specific data. Given a node and a selector, select will return only the matched nodes. You can then use the net.cgrand.enlive-html/text function to retrieve the text content of a node.

For example, the following function will return a sequence of the most recent n comic titles in the XKCD archives:

(defn comic-titles
  [n]
  (let [dom (html/html-resource
             (java.net.URL. "http://xkcd.com/archive"))
        title-nodes (html/select dom [:#middleContainer :a])
        titles (map html/text title-nodes)]
    (take n titles)))

(comic-titles 5)
;; -> ("Oort Cloud" "Git Commit" "New Study"
       "Telescope Names" "Job Interview")
When to use Enlive

As an HTML templating system, Enlive has two primary value propositions over its alternatives in the Clojure ecosystem.

First, the templates are pure HTML. This makes it much easier to work with HTML designers: they can hand their HTML mockups directly to a developer without having to deal with inline markup code, and developers can use them directly without manually slicing them (outside of code, that is). Furthermore, the templates themselves can be viewed in a browser statically, meaning they can serve as their own wireframes. This eliminates the burden of keeping a web project’s visual prototypes in sync with the code.

Secondly, because it uses real Clojure functions and data structures instead of a custom DSL, Enlive exposes the full power of the Clojure language. There are very few situations where you should feel limited by Enlive’s capabilities, since it is always possible to extend it using only standard Clojure functions and macros, operating on familiar persistent, immutable data structures.

See Also