Templating System for Clojure
- Template is function of its arguments.
- HTML is better for HTML than some host language DSL (just cause HTML is DSL).
- DOM manipulation tools and XSLT are good for transforming, not for templating (yes, opinionated).
- Clojure is good :)
- HTML isn't the only language that needs templating.
<p><%= (escape-html (post :body)) %></p>
Read on for more goodness.
...just because (star)fleet consists of many spaceships.
<()> is almost equivalent to Clojure's
<h1><(body)></h1> in Fleet is nearly the same as
(str "<h1>" (body) "</h1>") in Clojure.
The only difference is that
(body) output gets escaped (e.g. html-encoded to prevent XSS).
raw function to prevent escaping:
str function to place value
<(str posts-count)> instead of calling a function.
This is almost all we need, with one issue: writing something like
<(raw (for [p posts] (str "<li class=\"post\">" (p :title) "</li>")))>
is too ugly, and defining
<li class="post"><(p :title)></li> as separate template
can be overkill in many cases. So there should be the good way of embedding strings and anonymous templates.
The previous example could be rewritten using Slipway as
<(for [p posts] "> <li class="post"><(p :title)></li> <")>
This example has two points worth mentioning.
"><" construction processing is an expression of String type.
Strings in Slipway considered
raw by default.
Next case is something like this:
<(raw (map (fn [post] (str "<li class=\"post\">" (post :title) "</li>")) posts))>
With Slipway it can be replaced with
<(map (fn [post] "> <li class="post"><(post :title)></li> <") posts)>
Need to mention that all this supports lexical scoping and other Clojure features just like reference (previous) expression.
(fleet [& args] template-str options)
Creates anonymous function from
template-str using provided
options map. Intended to use just like
(def footer (fleet "<p>© <(year (now))> Your Company</p>")) (println (footer)) (def header (fleet [title] "<head><title><(str title)></title></head>")) (println (header "Main Page"))
Main option is
:escaping. It can be function of one String argument or keyword specifying one of predefined functions:
:bypass — default, no escaping;
:xml — XML (or HTML) rules;
:str — Java-compatible string escaping;
:clj-str — Clojure string escaping (
\n is allowed);
:regex — Escaping of Regex special symbols.
:file-path (both String) are in place for better stack traces.
(fleet-ns root-ns root-path filters)
root-path as root of template directory tree, maps it to namespace with prefix
root-ns., creates template functions
for each file in it with name and samespace according to relative path.
(fleet-ns view "path/to/view_dir" [:fleet :xml])
Template functions are created by the following rules:
— Several equal functions will be created for each file. E.g. file
posts.html.fleet will produce 3 functions:
This is useful for cases where you have
posts.json.fleet, so you may access distinct templates as
while and if you have only one
posts.html.fleet you could call it
— Template function will take one or two arguments: first named same as shortest function name for file (
posts in previous example) and second named
When it's called with one arguments both symbols (fn-name and data) are bound to same value of this argument.
When it's called with no arguments both symbols (fn-name and data) are bound to nil.
This is also for convinience: you could use name appropriate to usage: e.g. if your template renders post, you could use
post param name,
and if template renders some complex data you could use
Also you can mix&match, for example
post as main rendered entity and
data as some render options.
Filters argument is vector of
file-filter escaping-fn pairs used to filter which files to process and with which escaping function.
File filters could be defined as function, string, regex,
— Function should have Boolean type and one File argument.
— String filter definition treated as
*.string.fleet mask, e.g.
"js" mask will match
— Regex filter matches whole filename, e.g.
#".*.html" will match
:fleet filter is treated as "others". If it is set all
*.fleet files will be processed.
:all means, literally, all.
If you need to insert Fleet constructions into text you can escape them using backslash.
You only need escaping to remove ambiguity, so use
\<" only outside embedded clojure code,
\)> only inside embedded clojure code.
This is not intended to work out-of-box, only to show some bits of a language / system.
Template file (
<head> <title><(post :title)></title> <(stylesheet :main)> <(raw "<script>alert('Hello!')</script>")> </head> <body> <p><(str notice)></p> <p>Spaceship \<()> is landing.</p> <( ; Begin of post )> <(inside-frame (let [p post] "> Author: <(p :author)><br/> Date: <(p :date)><br/> <"))> <p><(post :body)></p> <ul> <(for [tag (post :tags] "> <li><(str tag)></li> <")> </ul> <( ; End of post )> <(footer)> </body> </html>
(def post-page (fleet [post] (slurp "post_dedicated.fleet"))) (post-page p) (footer)
(def footer (fleet "<p>© <(year (now))> Flamefork</p>"))
root_dir/ first_subdir/ file_a.html.fleet file_b.html.fleet second_subdir/ file_c.html.fleet
will be treated and processed by
(fleet-ns templates "path/to/root_dir" [:fleet :xml]) as functions
templates.first-subdir/file-a templates.first-subdir/file-b templates.second-subdir/file-c
and (for example) first function will be like
(defn file-a ([file-a data] ...) ([file-a] (recur file-a file-a))) ( (recur nil nil)))
Use 0.9.x for Clojure 1.2, 1.3
Use 0.10.x for Clojure 1.4+
- update Fleet with latest Clojure goodness [in progress]
- support ClojureScript
Copyright (c) 2010 Ilia Ablamonov, released under the MIT license.