How do I use snippets in enlive? - clojure

I'm a Rails dev getting my feet wet in Clojure. I'm trying to do something which was very simple with ERB but I can't for the life of me figure it out in enlive.
Say I have a simple layout file for a website in layout.html:
<!DOCTYPE html>
<html>
<head>
</head>
<body>
</body>
</html>
And I have these snippets, for instance, header.html and footer.html and this simple route.
(deftemplate layout "layout.html" [])
(defroutes home-routes
(GET "/" [] layout))
How can I make it so whenever a request goes to "/" it transforms the layout and inserts the header and footer snippets into it?

defsnippet only matches a specific part of your html (which is why it takes a selector as an argument), and transforms it. deftemplate takes the entire html, and transforms it. Also, defsnippet returns a Clojure data structure while deftemplates returns a vector of strings, so a defsnippet is usually used within a deftemplate.
To give you an idea of what the data returned by a snippet (or selector) look like:
(enlive/html-snippet "<div id='foo'><p>Hello there</p></div>")
;=({:tag :div, :attrs {:id "foo"}, :content ({:tag :p, :attrs nil, :content ("Hello there")})})
In your case you want something like:
header.html:
<div id="my-header-root">
...
</div>
Clojure code:
(enlive/defsnippet header "path/to/header.html" [:#my-header-root] []
identity)
(enlive/defsnippet footer "path/to/footer.html" [enlive/root] []
identity)
(enlive/deftemplate layout "layout.html" [header footer]
[:head] (enlive/content header)
[:body] (enlive/append footer))
(defroutes home-routes
(GET "/" [] (layout (header) (footer))
The identity function used in the snippets returns it's argument, which in this case is the data structure selected by the :#my-header-root selector (we don't do any transformation). If you want to include everything in i.e head.html you can use the root selector-step that comes with enlive.
You can view the html generated from a defsnippet using something like this:
(print (apply str (enlive/emit* (my-snippet))))
I also recommend the tutorial: https://github.com/swannodette/enlive-tutorial/
and the one by Brian Marick for some more details of how the defsnippet and deftemplate macros work.
Last tip, you can experiment with selectors and transformations using the sniptest macro that comes with enlive:
(enlive/sniptest "<p>Replace me</p>"
[:p] (enlive/content "Hello world!"))
;= "<p>Hello world!</p>"

There is great answer with examples in the enlive tutorial.
Warning. All source files links seem broken. You need to insert enlive-tutorial/blob/master/ after https://github.com/swannodette/ in all links or just open them directly from tutorial project.

Related

parsing HTML5 with Enlive/Tagsoup/JSoup

HTML5 allows <meta> tags to appear in the body, but Enlive does not seem to support this:
(deftest test-enlive
(testing "enlive"
(let [html-as-string "<!DOCTYPE html><html lang=\"en\"><body><div><meta foo=\"bar\"><span>the content</span></body></html>"
parsed-html (enlive/html-resource (java.io.StringReader. html-as-string))
span (enlive/select parsed-html [ :div :span ])
content (first (map enlive/text span))]
(is (= "the content" content)))))
This test fails, but will pass if you remove the meta tag.
This old thread led me to realize that it was the meta tag that was causing a problem.
I realize that Enlive depends on Tagsoup, but when I switch it out for JSoup (which claims to support HTML5) I get the same result.

Enlive templates - how to append script to end of the page

I am adding custom backbone.js scrips to my pages and it needs to be at the end of page,
I have viewed the solution for adding scripts to the html head as described here : Enlive templates – add to head section
Unfortunately, I need to add scripts to page bottom.
<html>
<head><!-- all the normal css and js scripts--></head>
<body>
<div> <!-- html content that will be used by my scripts --> </div>
<!-- this is where i want to append custom javascripts -->
</body>
</html>
just realized its as simple as a conj :
My solution is non idiomatic and i added a lot of prints to understand what is under the hood ; once i figured out [:body] input to a transformer was a vector, conj became an obvious solution,
(html/sniptest "<html><body><header class=\"some_class\">Header Content</header><footer>sddsd</footer></body></html>"
[:body] (fn [html-source]
(let [conts (:content html-source)
newstuff {:tag :script
:attrs {:src "../javascripts/foundation/foundation2.forms.js"}
:content []}
_ (pprint conts)
_ (print (class conts))
conts2 (conj conts newstuff )]
(assoc html-source :content conts2))))

Cannot call enlive snippet via its name stored in a variable

I'm trying to do something simple with Clojure Enlive lib: I want the top menu of my page to be different based on language selected by the user (English or Japanese). I can't find how to implement this basic feature.
I have a "template/header.html" template with 2 sections (one per language):
<div class="header en-US">...</div>
<div class="header ja-JP">...</div>
I created 2 corresponding snippets in my templating code:
(:require [net.cgrand.enlive-html :as html])
...
(html/defsnippet header-ja-JP "templates/header.html" [:div.header.ja-JP] [])
(html/defsnippet header-en-US "templates/header.html" [:div.header.en-US] [])
Now when I load my main template (index.html) I want to populate my header menu code into my nav tag.
Below, the argument 'lang' value is either "en-US" or "ja-JP":
(defn process-template
[lang]
(let [header-selector (str “header-“ lang)
header-content (#(resolve (symbol header-selector)))]
(apply str
(html/emit*
(html/at
(html/html-resource "templates/index.html")
[:nav] (html/content (header-content)))))))
It throws a java.lang.NullPointerException
If I replace the last line by
[:nav] (html/content (header-ja-JP)))))))
or
[:nav] (html/content (header-en-US)))))))
it works fine (so the snippets work), but I've tried lots of different syntaxes and can't make the dynamic header work based on language.
The above code (let part) seems ok if I execute it manually in REPL, the header-content is JSON object containing the correct content so I don't understand why I get a NullPointerException.
Ideally I think I should have only one snippet taking the language as argument and returning the corresponding HTML section, but didn't manage to do that either.
I'm very new at Clojure so I'm probably missing something obvious here; if someone could point me to what's wrong in the code above or another way to make this work that will be awesome.
Thanks,
Olivier
If we want to do it differently as cgrand suggests, there are many, many ways to accomplish this. I have a hard time imagining how he'd do it with maps, but I'd do it this way:
(defn process-template [lang]
(html/select (html/html-snippet
"<html>
<head></head>
<body>
<nav>
<div class=\"en\"></div>
<div class=\"jp\"></div>
</nav>
</body>
</html>")
[(keyword (str "div." lang))]))
(apply str (html/emit* (process-template "en")))
=> "<div class=\"en\"></div>"
Of course read it from file instead of HTML in a string. The function plucks out the matching HTML node according to a selector.
Your (#(resolve (symbol header-selector))) returns a value.
(resolve (symbol header-selector)) returns a function.
[:nav] (html/content (header-content))))))) calls header-content as a function.
Replace (#(resolve (symbol header-selector))) with (resolve (symbol header-selector)).
I'm fuzzy on what html/at and html/html-resource do so if other problems crop up you might need to cram in clojure.java.io/file in there.

Enlive - Wrap tags around HTML from file

So I have the following HTML in logout.html:
<form id="log_out" name="log_out" action="/log_out" method="post">
<input type="submit"
value="Log Out!">
</input>
</form>
It looks like I need some function to read logout.html as enlive nodes (At least I think wrap takes nodes; I'm not actually sure).
(html/defsnippet nav "templates/nav.html" [:ul]
[]
[:ul] (html/append
(html/wrap :li (html/SOME-FUNCTION-IDK "templates/logout.html"))))
Not sure if this is the best way, but you could define the contents of logout.html as an enlive snippet. A snippet is like a template, but returns nodes, and can also selectively grab parts of the file given a selector (in the example below, the selector is :#log_out, meaning the form element with id="log_out").
(html/defsnippet logout-form "templates/logout.html" [:#log_out]
[])
Then something like:
(html/defsnippet nav "templates/nav.html" [:ul]
[]
[:ul] (html/append
((html/wrap :li) (logout-form))))) ;; untested! ymmv
I ended up having to modify overthink's answer to get it working.
(defn extract-body
"Enlive uses TagSoup to parse HTML. Because it assumes that it's dealing with
something potentially toxic, and tries to detoxify it, it adds <head> and
<body> tags around any HTML you give it. So the DOM returned by html-resource
has these extra tags which end up wrapping the content in the middle of our
webpage. We need to strip these extra tags out."
[html]
(html/at html [#{:html :body}] html/unwrap))
(html/defsnippet logout "templates/logout.html" [html/root] [])
How wrap works is it wraps selected elements in a given tag. So in this case, #log_out is selected and is wrapped with the li tag.
(html/defsnippet nav "templates/nav.html" [html/root]
[]
[:ul] (html/append (extract-body (logout)))
[:#log_out] (html/wrap :li))
It's definitely not as clean as I'd like, but it works.

replace-vars in enlive 1.1.4

I have the following html that is loaded into a template...
Welcome <b id="alert-username">${fullname}</b>
I have the following selector/action in a template...
[:#alert-username] (replace-vars {:fullname (fullname request)})
This is rendered as follows...
Welcome ${fullname}
Which is obviously not what I'm looking for.
However, it works if I do it the hard way...
[:#alert-username] (fn [n] (assoc n :content [(fullname request)]))
which yields...
Welcome Bill
So I know that it's not an issue getting the username from the request because the above code does what it should.
What am I doing wrong here?
The replace-vars transformation does not recurse into child nodes, including text content.
The selector :#alert-username selects the <b id="alert-username"> tag:
{:tag :b, :attrs {:id "alert-username"}, :content ["${fullname}"]}
So, replace-vars, because it is used on the tag, will search the the tag's attributes and ignore the tag's contents.
You can apply the transform to the contents with something like the following:
[:#alert-username] (transform-content
(replace-vars {:fullname (fullname request)}))
But, that will also search any child tag attributes, along with any child text nodes.
Note: The transform-content macro is part of the net.cgrand.enlive-html namespace.
Since you have a tag enclosing the ${fullname}, you could just use the transform
[:#alter-username] (content (fullname request))
and put any text in the tag. The content in of the tag given in the the template will be replaced anyway.