Skip to content

Instantly share code, notes, and snippets.

@xavi
Created September 15, 2012 18:57
Show Gist options
  • Save xavi/3729307 to your computer and use it in GitHub Desktop.
Save xavi/3729307 to your computer and use it in GitHub Desktop.
Fast, simple, powerful templating in Clojure
;; 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)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment