clojureenlive

How does Enlive evaluate its rules / transformations?


I like Enlive, but it got me somewhat confused when I observed the following.

Consider the following Clojure code (also available on github):

(ns enlivetest.core
  (:require [net.cgrand.enlive-html :refer [deftemplate defsnippet] :as html]))

(deftemplate page "index.html"
  [ctx]
  [:.foobar] (html/content (do (println "GENERATING FOOBAR")
                               "===FOOBAR===")))

and this HTML template (resources/index.html) here:

<!DOCTYPE html>
<html>
    <body>
    </body>
</html>

When calling the page template, I'd expect it to ignore the right hand side of its rule (the transformation) completely, as there is no HTML tag that matches the rule's selector :.foobar.

However, as it turns out, the right hand side of the rule does in fact get evaluated:

user=> (require '[enlivetest.core :as c])
nil
user=> (c/page {})
GENERATING FOOBAR
GENERATING FOOBAR
("<!DOCTYPE html>\n" "<" "html" ">" "\n    " "<" "body" ">" "\n    " "</" "body" ">" "\n\n" "</" "html" ">")

(Obviously, it even gets evaluated twice - once for each root HTML element in the template as it seems).

But why is it being evaluated at all, although there is no element matching the selector? Is this correct behaviour? Am I missing something obvious here?

This example uses Enlive 1.1.6, just as its README suggests.

Clarifications are greatly appreciated.

EDIT #1:

As it turns out (thanks to @leetwinski), my assumption of how things work was incorrect:

I was assuming that the deftemplate macro would only evaluate the right hand side of a rule (the transformation part) when the selector of that rule matches an element in the given HTML.

But correct is this:

The right hand side of a rule will always get evaluated during a call to the defined template function (e.g. page) and is expected to evaluate to a function that will in turn evaluate to the desired content (e.g. "===FOOBAR===" in this example) when called. It is this function that will get called only for elements that match the selector.

This means that e.g. html/content evaluates to such a function (and not to the desired content directly).

In order to make things work as I expected originally, I could write it like this:

(deftemplate page "index.html"
  [ctx]
  [:.foobar] #((html/content (do (println "GENERATING FOOBAR")
                                 "===FOOBAR===")) %))

which will result in the following output:

user=> (c/page {})
("<!DOCTYPE html>\n" "<" "html" ">" "\n    " "<" "body" ">" "\n    " "</" "body" ">" "\n\n" "</" "html" ">")

or when adding a <div class="foobar"></div> to the HTML template:

user=> (c/page {})
GENERATING FOOBAR
("<!DOCTYPE html>\n" "<" "html" ">" "\n    " "<" "body" ">" "\n\t\t" "<" "div" " " "class" "=\"" "foobar" "\"" ">" "===FOOBAR===" "</" "div" ">" "\n    " "</" "body" ">" "\n\n" "</" "html" ">")

EDIT #2:

It's been a few weeks, but I'm still struggeling with how this is implemented in Enlive. I see myself wrapping the transformation parts of rules into #((html/content ...) %) over and over again.

Does anybody have an explanation for why Enlive evaluates transformations (at all or even multiple times) even when they are not even relevant for the current rendering process?

I might be overlooking something, as I'm really surprised that this doesn't seem to bother anybody but me.


Solution

  • The reason is the nature of enlive's deftemplate macro:

    it takes pairs of selector-to-function. In yours, the function is generated dynamically here:

    (html/content (do (println "GENERATING FOOBAR") "===FOOBAR==="))
    

    content just creates the function, that would be called in case of the match.

    user> ((html/content "this" "is" "fine") {:content []})
    {:content ("this" "is" "fine")}
    

    content is not a macro, so it should evaluate it's argument. so, what you see, is not the false matching function call, rather it is the call to a generation of the function that would be called in case of the match.

    you could easily see it with macroexpansion of your deftemplate form:

    (def page
     (let*
       [opts__8226__auto__
        (merge (html/ns-options (find-ns 'user)) {})
        source__8227__auto__
        "index.html"]
       (html/register-resource! source__8227__auto__)
       (comp
         html/emit*
         (let*
           [nodes29797
            (map
              html/annotate
              (html/html-resource
                source__8227__auto__
                 opts__8226__auto__))]
           (fn*
             ([ctx]
               (doall
                 (html/flatmap
                   (fn*
                     ([node__8199__auto__]
                        (html/transform
                          (html/as-nodes node__8199__auto__)
                         [:.foobar]
                         (html/content
                           (do
                             (println "GENERATING FOOBAR")
                             "===FOOBAR===")))))
                   nodes29797))))))))
    

    so the correct string in println would be:

    (deftemplate page "index.html"
      [ctx]
      [:.foobar] (html/content (do (println "GENERATING FUNCTION SETTING FOOBAR AS THE NODE CONTENT")
                                   "===FOOBAR===")))
    

    The behaviour you expect could be achieved this way:

    user> 
    (deftemplate page "index.html"
      [ctx]
      [:.foobar] (fn [node] (assoc node :content
                                   (do (println "GENERATING FOOBAR" node)
                                       "===FOOBAR==="))))
    #'ttask.core/page
    
    user> (page {})
    ("<!DOCTYPE html>\n" "<" "html" ">" "\n    " "<" "body" ">" "\n    " "</" "body" ">" "\n\n" "</" "html" ">")
    

    and if you add class "foobar" to body in index.html it would do this (don't forget to re-run deftemplate after changing html):

    user> (page {})
    GENERATING FOOBAR {:tag :body, :attrs {:class foobar}, :content []}
    ("<!DOCTYPE html>\n" "<" "html" ">" "\n    " "<" "body" " " "class" "=\"" "foobar" "\"" ">" "=" "=" "=" "F" "O" "O" "B" "A" "R" "=" "=" "=" "</" "body" ">" "\n\n" "</" "html" ">")