Skip to content

Instantly share code, notes, and snippets.

@wmorgan
Created July 5, 2012 16:18
Show Gist options
  • Save wmorgan/3054620 to your computer and use it in GitHub Desktop.
Save wmorgan/3054620 to your computer and use it in GitHub Desktop.
Mustache templating for Clojure
(ns potato.core
(:use [slingshot.slingshot :only [throw+ try+]])
(:use [clojure.string :only [trim split]]))
;; use this provide template values at write time (i.e. not compile time).
;; "name" will be the name of the template variable. "context", when not nil,
;; will be the value previously returned by *template-value* for the enclosing
;; section.
(defn ^:dynamic *template-value* [name context])
;; use this to provide the content of a partial, give a name. this will be
;; called at compile time, not at write time.
(defn ^:dynamic *partial-value* [name])
;;; now, the guts:
;; verify that a string ends with the expected delimiter, otherwise raise an
;; error
(defn- verify-end-delim [s tok start-index]
(let [end-index (dec (count s))]
(if (= tok (.charAt s end-index))
(.substring s 1 end-index)
(throw+ { :type :parse-error :msg (str "expected " tok " to end " s)
:position (+ start-index end-index) }))))
;; turn a string into a single lexeme
(defn- lex-item [s start-index]
;(println (str "; lex-item called with " s " and " start-index))
(condp = (.charAt s 0)
\# (list :section (.substring s 1))
\^ (list :inverted-section (.substring s 1))
\/ (list :end-section (.substring s 1))
\! (list :comment)
\> (list :partial (trim (.substring s 1)))
\{ (list :unescaped-value (trim (verify-end-delim s \} start-index)))
\& (list :unescaped-value (trim (.substring s 1)))
\= (let [together (verify-end-delim s \= start-index)
[left right :as both] (split together #"\s+")]
(if (= 2 (count both))
(list :change-delimiter left right)
(throw+ { :type :parse-error
:msg (str "invalid format for delimiter spec: " together)
:position start-index
})))
(list :value s)))
;; find the corresponding end delimiter. has a little extra ugly logic to
;; distinguish between "}}" and "}}}"
(defn- find-end-delimiter [s end-delim starting-point]
;(println "; find-end-delimiter called with [" s "] and " end-delim " and " starting-point)
(let [pos (.indexOf s end-delim starting-point)
len (count end-delim)]
(when (= -1 pos)
(throw+ { :type :parse-error :msg (str "missing " end-delim) :position starting-point }))
(if (and (< (+ pos len) (count s))
(= (str "}" end-delim) (.substring s pos (+ pos (count end-delim) 1))))
(+ pos 1)
pos)))
;; lex a mustache template
(defn lex [s]
(loop [s s
start-delim "{{"
end-delim "}}"
start-index 0
acc ()]
;(println "; lex called with [" s "] and start-index " start-index)
(let [start-delim-si (.indexOf s start-delim)]
(if (= -1 start-delim-si)
;; no more mustache items
(if (empty? s) acc (cons (list :string s) acc))
;; have more mustache items
(let [start-delim-ei (+ start-delim-si (count start-delim))
prefix (.substring s 0 start-delim-si)
end-delim-si (find-end-delimiter s end-delim start-delim-ei)
end-delim-ei (+ end-delim-si (count end-delim))
token (trim (.substring s start-delim-ei end-delim-si))
lexeme (lex-item token (+ start-index start-delim-ei))
remaining (.substring s end-delim-ei)
[new-start-delim new-end-delim new-acc]
;; do we need to change our delimiter?
(if (= :change-delimiter (first lexeme))
;; yep, use the new delimiters
(let [[token start end] lexeme]
[start end (conj acc (list :string prefix))])
;; nope, use the original delimiters
[start-delim end-delim (conj acc (list :string prefix) lexeme)])]
(recur remaining new-start-delim new-end-delim (+ start-index end-delim-ei)
new-acc))))))
(defn compile-mustache [s])
;; parse the mustache template given the lexeme seq. nests sections, cleans up
;; strings, skips comments, etc. returns a tree and a seq of unparsed lexemes.
(defn- parse [lexemes until-section]
;(println (str "; parse called with " (first lexemes) " and " until-section))
(if (empty? lexemes)
(if (nil? until-section)
{ :tree () :lexemes () }
;; otherwise, we didn't get a close section -- parse error
(throw+ { :type :parse-error
:msg (str "unterminated section " until-section) }))
;; here, we have some lexemes. we'll peek at two of them.
(let [[ltype ldata & lmore :as lexeme] (first lexemes)
lexemes-rest (rest lexemes)
[ntype ndata & nmore] (first lexemes-rest)]
(cond
;; skip comments
(= :comment ltype) (parse lexemes-rest until-section)
;; skip empty strings
(and (= :string ltype) (empty? ldata)) (parse lexemes-rest until-section)
;; merge consecutive strings
(and (= :string ltype) (= :string ntype))
(parse (cons (list :string (str ldata ndata)) (rest lexemes-rest)) until-section)
;; compile partials
(= :partial ltype)
(parse (concat (compile-mustache (*partial-value* ldata)) lexemes-rest) until-section)
;; handle start sections or inverted sections
(or (= :section ltype) (= :inverted-section ltype))
(let [sub-result (parse lexemes-rest ldata)]
;(println (str "> came back with " (sub-result :tree) " and " (sub-result :lexemes)))
(let [next-result (parse (sub-result :lexemes) until-section)]
{ :tree (cons (concat (list ltype ldata) (sub-result :tree)) (next-result :tree))
:lexemes (next-result :lexemes) }))
;; handle end-sections
(= :end-section ltype)
(if (= ldata until-section) ; end section!
;; empty tree, and here are the rest of the lexemes
{ :tree () :lexemes lexemes-rest }
;; otherwise you are popping something you didn't want
(throw+ { :type :parse-error
:msg (str "cross-nested section " until-section " -- was expecting " ldata) }))
;; otherwise, just add it to the tree and continue
:else
(let [result (parse lexemes-rest until-section)]
{ :tree (cons lexeme (result :tree)) :lexemes (result :lexemes) })))))
;; call me to compile!
(defn compile-mustache [s]
;(println "; compile-mustache called with [" s "]")
(let [result (parse (reverse (lex s)) nil)]
(result :tree)))
(defn mustache-to-string [compiled-template & context])
;; escape HTML characters
(defn- escape-string [s]
(.replace (.replace (.replace (str s) "&" "&amp;") "<" "&lt") ">" "&gt"))
;; build the string for a particular section
(defn- fill-section [name template context]
;(println "; fill-section called with " name " and " context " and " template)
(let [value (*template-value* name context)]
(cond
(nil? value) ""
(seq? value) (apply str (map #(mustache-to-string template %) value))
(fn? value) (value template context)
:else (mustache-to-string template value))))
;; build the string for a particular inverted-section
(defn- fill-inverted-section [name template context]
;(println "; fill-inverted-section called with " name " and " context " and " template)
(let [value (*template-value* name context)]
(if (or (nil? value) (empty? value))
(mustache-to-string template nil)
"")))
;; the actual work of turning a compiled mustache template into a string.
;; calls *template-value* as necessary.
(defn mustache-to-string [compiled-template & context]
;(println "; mustache-to-string called with " compiled-template " and " context)
(loop [compiled-template compiled-template
acc ""]
(if (empty? compiled-template)
acc
(let [[type data & more] (first compiled-template)
context (first context)
new-acc (str acc
(condp = type
:string data
:unescaped-value (*template-value* data context)
:section (fill-section data more context)
:inverted-section (fill-inverted-section data more context)
:value (escape-string (*template-value* data context))))]
(recur (rest compiled-template) new-acc)))))
;;;;;;;; examples
(defn ^:dynamic *items*)
(def example-items1 (list {:name "small" :size 1} {:name "medium" :size 3} {:name "large" :size 5}))
(def example-items-empty ())
(defn example-template-value1 [v context]
(println "; example-template-value1 called with " v " and " context)
(cond
(map? context) (context (keyword v))
(nil? context)
(condp = v
"items" *items*
"count" (count *items*)
"title" "my list of nice & great stuff"
"UNKNOWN")
:else (:throw+ { :type :template-value-error :msg (str "unknown context " context) })))
(defn example-template-value2 [v context]
(if (nil? context) (str "called with " v) (str "called with " v " / " context)))
(defn example1 []
(binding [*template-value* example-template-value1
*items* example-items1]
(mustache-to-string (compile-mustache "{{title}}\nhere are the {{count}} things:\n{{#items}}- {{name}} has size {{ size }}\n{{/items}}{{^items}}\nno items!\n{{/items}}\nthe end!"))))
(defn example2 []
(binding [*template-value* example-template-value1
*items* example-items-empty]
(mustache-to-string (compile-mustache "{{title}}\nhere are the {{count}} things:\n{{#items}}- {{name}} has size {{ size }}\n{{/items}}{{^items}}\nno items!\n{{/items}}\nthe end!"))))
(defn example3 []
(binding [*template-value* example-template-value1
*items* example-items1]
(mustache-to-string (compile-mustache "one {{!two}} {{! three}} four"))))
(defn example4 []
(binding [*template-value* (fn [v c] ({ "company" "<b>clojure, inc</b>" } v))]
(mustache-to-string (compile-mustache "{{company}}\n{{{company}}}\n"))))
(defn example5 []
(binding [*template-value* example-template-value1
*items* example-items1
*partial-value* (fn [n] (when (= n "bob") "- {{name}} / {{size}}"))]
(mustache-to-string (compile-mustache "here are the {{count}} things:\n{{#items}} {{> bob}}\n{{/items}}\nbye!"))))
(defn example6 []
(binding [*template-value* (fn [v c] ({ "name" "joe" } v))]
(mustache-to-string (compile-mustache "{{name}}{{=<% %>=}}<% name %><%={{ }}=%>{{name}}"))))
(defn example7 []
(binding [*template-value* (fn [v c]
(fn [compiled-template context] (str
"[" v ": "
(mustache-to-string compiled-template context)
"]")))]
(mustache-to-string (compile-mustache "{{#joe}}{{#bob}}hello{{/bob}}{{/joe}}"))))
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment