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.
Write
<p><(post :body)></p>
instead of
<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 ()
, so
<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).
Use raw
function to prevent escaping: <(raw "<br/>")>
.
Use 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.
Result of "><"
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 (fn
construct.
Example:
(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.
Options :file-name
and :file-path
(both String) are in place for better stack traces.
(fleet-ns root-ns root-path filters)
Treats 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.
Example:
(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: posts
, posts-html
and posts-html-fleet
.
This is useful for cases where you have posts.html.fleet
and posts.json.fleet
, so you may access distinct templates as posts-html
and posts-json
,
while and if you have only one posts.html.fleet
you could call it posts
conviniently.
— Template function will take one or two arguments: first named same as shortest function name for file (posts
in previous example) and second named data
.
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 data
.
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, :fleet
or :all
.
— Function should have Boolean type and one File argument.
— String filter definition treated as *.string.fleet
mask, e.g. "js"
mask will match update.js.fleet
.
— Regex filter matches whole filename, e.g. #".*.html"
will match posts.html
.
— :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 \<(
and \<"
only outside embedded clojure code, \">
and \)>
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 (post_dedicated.fleet
):
<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>
Clojure:
(def post-page (fleet [post] (slurp "post_dedicated.fleet")))
(post-page p)
(footer)
Low-level:
(def footer (fleet "<p>© <(year (now))> Flamefork</p>"))
High-level:
Directory tree
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.