Skip to content

Latest commit

 

History

History
158 lines (121 loc) · 7.45 KB

README.textile

File metadata and controls

158 lines (121 loc) · 7.45 KB

The new Enlive

Enlive is a selector-based (à la CSS) templating library for Clojure.

An Enlive template has two parts: a HTML file and a deftemplate form somewhere in a clj file.

What’s wrong with the old Enlive?

Premature optimization. It drove me to write a convoluted and limited system. The new Enlive is more regular, more powerful and easier to reason about.

I made it work (old Enlive), I’m making it right (new Enlive) and I’ll make it faster.

What’s new in the new Enlive?

Transformations (the right-hand parts of rules) are now plain old closures. These functions take one arg (the selected node) and return nil, another node or an arbitrarily nested collection of nodes.

Rules are applied top-down: the first rule transforms the whole tree and the resulting tree is passed to the next rules.

Nodes are transformed deep-first, that is: if a selector selects several nodes, descendants are transformed first. Hence, when the transformation is applied to an ancestor, you can “see” the transformed descendants (but you can not see your transformed siblings).

   /B                                                                             /(T B)
  A    if A and B are selected and transformed by T the the resulting tree is (T A      )
   \C                                                                             \C

Templates and snippets

A snippet is a function that returns a seq of nodes, it can be used as a building block for more complex templates.

A template is a function that returns a seq of string — basically it’s a snippet whose output is serialized. Templates return a seq of strings to avoid building the whole string.

Templates and snippets transform a source (specified as a path (to access resources on the classpath), a File, a Reader, an InputStream, an URI, an URL, an element or a seq of nodes).

The at form

The at form is the most important form in Enlive. There are implicit at forms in snippet and template.

  (at a-node
    [:a :selector] a-transformation
    [:another :selector] another-transformation
    ...)

The right-hand value of a rule can be nil. It’s the idiomatic way to remove an element.

Transformations are closures which take one arg (the selected node) and return nil, another node or an arbitrarily nested collection of nodes.

Rules are applied top-down: the first rule transforms the whole tree and the resulting tree is passed to the next rules.

Selectors

Enlive enforces (in select* and transform-loc) that selectors can only match elements.

Syntax

A selector is either a vector or a set of selectors (denoting grouping).

Each element in the vector is either a combinator, a simple selector or a compound selector (union or intersection), here is a summary:

  Enlive                           CSS
  =======================================================
  :>                               >
  :*                               *
  :div                             div
  :div#some-id                     div#some-id
  :div.class1.class2               div.class1.class2
  :*#foo.bar                       *#foo.bar
  :#foo.bar                        #foo.bar
  (attr? :href)                    *[href]
  (attr? :href :title)             *[href][title]
  (attr= :href "foo")              *[href=foo]
  (attr= :href "foo" :title "bar") *[href=foo][title=bar]
  [:a.action (attr? :href)]        a.action[href]
  #{:div :p}                       _no equivalent_
  (attr-has :foo "bar" "baz")      *[foo~=bar][foo~=baz]   
  (attr-starts                     *[href^=foo][title^=bar]
    :href "foo" :title "bar")
  (attr-ends                       *[href$=foo][title$=bar]
    :href "foo" :title "bar")
  (attr-contains                   *[href*=foo][title*=bar]
    :href "foo" :title "bar")
  (attr|= :lang "fr")              *[lang|=fr]
  root                             *:root
  (nth-child 3)                    *:nth-child(3)
  (nth-child 4 2)                  *:nth-child(4n+2)
  (nth-last-child 3)               *:nth-last-child(3)
  (nth-last-child 4 2)             *:nth-last-child(4n+2)
  (nth-of-type 3)                  *:nth-of-type(3)
  (nth-of-type 4 2)                *:nth-of-type(4n+2)
  (nth-last-of-type 3)             *:nth-last-of-type(3)
  (nth-last-of-type 4 2)           *:nth-last-of-type(4n+2)
  first-child                      *:first-child
  last-child                       *:last-child
  first-of-type                    *:first-of-type
  last-of-type                     *:last-of-type
  only-child                       *:only-child
  only-of-type                     *:only-of-type
  empty                            *:empty
  odd                              *:nth-child(odd)
  even                             *:nth-child(even)
  (but :a)                         :not(a)
  (has [:a])                       no equivalent
  [:p (left :h1)]                  h1 + p  
  [:p (lefts :h1)]                 h1 ~ p
  [:p (right :h1)]                 no equivalent  
  [:p (rights :h1)]                no equivalent

Some examples:

  [:div]                                       div
  [:body :script]                              body script
  #{[:ul.outline :> :li] [:ol.outline :> li]}  ul.outline > li, ol.outline > li 
  [#{:ul.outline :ol.outline} :> :li]          ul.outline > li, ol.outline > li
  [[#{:ul :ol} :.outline] :> :li]              ul.outline > li, ol.outline > li

Compilation

At macroexpansion-time in select, snippet and at macros, selectors are compiled to code: (all expansions are edited for clarity)

  net.cgrand.enlive-html=> (compile-selector '[:div])
  (chain descendants-or-self (tag= :div))
  net.cgrand.enlive-html=> (compile-selector '[:body :script])
  (chain descendants-or-self (tag= :body) descendants-or-self (tag= :script))
  net.cgrand.enlive-html=> (compile-selector '[#{:ul.outline :ol.outline} :> :li])
  (chain descendants-or-self (union (intersection (tag= :ol) (has-class "outline")) (intersection (tag= :ul) (has-class "outline"))) (tag= :li))
  net.cgrand.enlive-html=> (compile-selector '[[:div (attr= :title "foobar")]])
  (chain descendants-or-self (intersection (tag= :div) (attr= :title "foobar")))

(compile-selector '[:div]) is equivalent to (macroexpand-1 '(selector [:div])).

at, select, snippet are macros that expect a selector to be passed. If you want to use a value instead of the selector, you have to use at*, select* and snippet* which are the functions behind the macro sugar.

  net.cgrand.enlive-html=> (macroexpand-1 '(at node [:div] (content "It's a div")))
  (at* [node] (selector [:div]) (content "It's a div"))
  net.cgrand.enlive-html=> (macroexpand-1 '(select some-html some-selector))
  (select* some-html (selector some-selector))

The relation between snippet and snippet* is more complex: snippet* doesn’t not take a selector, you have to select your nodes before (through select or select*).

  net.cgrand.enlive-html=> (-> '(snippet a-source [:div#foo] [args] selector1 transformation1) macroexpand-1)
  (snippet* (select (html-resource a-source) [:div#foo]) [args] selector1 transformation1)
  net.cgrand.enlive-html=> (-> '(snippet a-source [:div#foo] [args] selector1 transformation1) macroexpand-1 macroexpand-1)
  (let [nodes__4595__auto__ (select (html-resource a-source) [:div#foo])] 
    (fn [args] 
      (flatmap #(at % selector1 transformation1)) nodes__4595__auto__)))