-
-
Save isa/3735531 to your computer and use it in GitHub Desktop.
Fast, simple, powerful templating in Clojure
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
;; 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