public
Last active

Fast, simple, powerful templating in Clojure

  • Download Gist
gistfile1.clj
Clojure
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81
;; Call the renderer-fn macro with a template and it returns a function optimized to render it.
;; This happens at compile-time.
;; At run-time, you call this function with the parameters that will be interpolated into the template,
;; typically (but not limited to) a map.
;;
;; Useful in i18n for variable interpolation, for example. I'm using this to add internationalization
;; support to https://github.com/xavi/noir-auth-app
 
;; See usage at the end.
 
(require '[clojure.walk :as walk])
 
(defn replace-symbol [form old-sym-name new-sym-name]
(walk/postwalk #(if (and (symbol? %) (= (name %) (name old-sym-name)))
(symbol new-sym-name)
%)
form))
 
; Returns an optimized expression implementing the template specified in the
; first parameter (a sequence), replacing the % in the interpolation
; expressions with the symbol name specified in the second parameter.
;
; (compile-template '("hello " (:name %) "!") 'm)
; =>
; (str "hello " (:name m) "!")
;
(defn compile-template [ss sym-name]
(let [ss (->> ss
; The (not= ...) allows the interpolation expression to be
; simply % instead of something like (:name %)
(map #(if (and (symbol? %) (not= (name %) "%")) (eval %) %))
; For all elements that are not strings, replaces any % with
; the sym-name specified as a parameter.
(map #(if (string? %)
%
(replace-symbol % "%" sym-name)))
; http://clojuredocs.org/clojure_core/clojure.core/partition-by
(partition-by string?)
; Concatenates the strings on each sublist:
; (("hello " "Clojurian ") ((:name %) (:surname %)))
; =>
; (("hello Clojurian ") ((:name %) (:surname %)))
(map #(if (string? (first %)) (list (apply str %)) %))
; (("hello Clojurian ") ((:name %) (:surname %)))
; =>
; ("hello Clojurian " (:name %) (:surname %))
; http://clojuredocs.org/clojure_core/clojure.core/flatten
; http://stackoverflow.com/questions/5232350/clojure-semi-flattening-a-nested-sequence
(apply concat))]
(if (= (count ss) 1) (first ss) (cons 'str ss))))
 
(defmacro renderer-fn
[& ss]
(let [m (gensym "m")]
`(fn [& [~m]] ~(compile-template ss (name m)))))
 
 
;; ----- USAGE -----
 
(def app-name "Demo App")
(def f (renderer-fn "Hello " (:name %) ", "
"welcome to " app-name "! "
"You can also break long lines for code readability "
"without sacrificing performance."))
 
; f is now something like (use macroexpand on the renderer-fn macro to see it):
;
; (fn* ([& p__102]
; (clojure.core/let [[m101] p__102]
; (str "Hello " (:name m101)
; ", welcome to Demo App! You can also break long lines for readability without sacrificing performance."))))
 
; It can be used like this:
(f {:name "Xavi"})
; =>
; "Hello Xavi, welcome to Demo App! You can also break long lines for readability without sacrificing performance."
 
; and it's probably the fastest templating that you can get in Clojure
(time (f {:name "Xavi"}))
; => "Elapsed time: 0.068 msecs"
; in my MBP (2.26 GHz Intel Core 2 Duo)

Please sign in to comment on this gist.

Something went wrong with that request. Please try again.