Created
July 5, 2012 16:18
-
-
Save wmorgan/3054620 to your computer and use it in GitHub Desktop.
Mustache templating for Clojure
This file contains hidden or 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
(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) "&" "&") "<" "<") ">" ">")) | |
;; 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