├── test-resources └── templates │ └── hello.mustache ├── .gitmodules ├── deps.edn ├── .gitignore ├── test └── cljstache │ ├── runner.cljs │ ├── mustache_spec_test.cljc │ └── core_test.cljc ├── .travis.yml ├── project.clj ├── CHANGES.md ├── COPYING ├── README.md └── src └── cljstache └── core.cljc /test-resources/templates/hello.mustache: -------------------------------------------------------------------------------- 1 | Hello, {{name}} -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "test-resources/spec"] 2 | path = test-resources/spec 3 | url = https://github.com/mustache/spec 4 | -------------------------------------------------------------------------------- /deps.edn: -------------------------------------------------------------------------------- 1 | {:aliases {:dev {:extra-paths ["src" "test" "test-resources"] 2 | :extra-deps {org.clojure/data.json {:mvn/version "0.2.6"}}}}} 3 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | target 2 | .dir-locals.el 3 | pom.xml 4 | *jar 5 | /lib/ 6 | /classes/ 7 | /target/ 8 | .lein-failures 9 | .lein-deps-sum 10 | TAGS 11 | checkouts/* 12 | -------------------------------------------------------------------------------- /test/cljstache/runner.cljs: -------------------------------------------------------------------------------- 1 | (ns cljstache.runner 2 | "A stub namespace to run cljs tests using doo" 3 | (:require [doo.runner :refer-macros [doo-tests]] 4 | [cljstache.core-test] 5 | [cljstache.mustache-spec-test])) 6 | 7 | (doo-tests 'cljstache.core-test 8 | 'cljstache.mustache-spec-test) 9 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: clojure 2 | 3 | script: lein test-$CLJ 4 | 5 | before_install: 6 | - git submodule update --init --recursive 7 | - yes | sudo lein upgrade 8 | 9 | matrix: 10 | include: 11 | - jdk: openjdk11 12 | env: CLJ=clj 13 | - jdk: openjdk11 14 | env: CLJ=cljs 15 | - jdk: oraclejdk11 16 | env: CLJ=clj 17 | - jdk: oraclejdk11 18 | env: CLJ=cljs 19 | 20 | sudo: required 21 | 22 | cache: 23 | directories: 24 | - $HOME/.m2 25 | -------------------------------------------------------------------------------- /project.clj: -------------------------------------------------------------------------------- 1 | (defproject cljstache "2.0.7-SNAPSHOT" 2 | :min-lein-version "2.5.2" 3 | :description "{{ mustache }} for Clojure[Script]" 4 | :url "http://github.com/fotoetienne/cljstache" 5 | :license {:name "GNU Lesser General Public License 2.1" 6 | :url "http://www.gnu.org/licenses/lgpl-2.1.txt" 7 | :distribution :repo} 8 | :jvm-opts ["-Xmx1g"] 9 | 10 | :dependencies [] 11 | 12 | :profiles {:dev {:dependencies [[org.clojure/clojure "1.8.0"] 13 | [org.clojure/data.json "0.2.6"]] 14 | :resource-paths ["test-resources"]} 15 | :1.7 {:dependencies [[org.clojure/clojure "1.7.0"]]} 16 | :1.8 {:dependencies [[org.clojure/clojure "1.8.0"]]} 17 | :1.9 {:dependencies [[org.clojure/clojure "1.9.0"]]} 18 | :1.10 {:dependencies [[org.clojure/clojure "1.10.1"]]} 19 | :cljs {:dependencies [[org.clojure/clojure "1.10.1"] 20 | [org.clojure/clojurescript "1.10.520"]] 21 | :plugins [[lein-cljsbuild "1.1.7"] 22 | [lein-doo "0.1.10"]]}} 23 | 24 | :aliases {"with-clj" ["with-profile" "dev:dev,1.7:dev,1.8:dev,1.9"] 25 | "with-cljs" ["with-profile" "cljs"] 26 | "test-clj" ["with-clj" "test"] 27 | "test-cljs" ["with-cljs" "doo" "nashorn" "test" "once"] 28 | "test-all" ["do" "clean," "test-clj," "test-cljs"] 29 | "deploy" ["do" "clean," "deploy" "clojars"]} 30 | 31 | :jar-exclusions [#"\.swp|\.swo|\.DS_Store"] 32 | 33 | :doo {:paths {:rhino "lein run -m org.mozilla.javascript.tools.shell.Main"}} 34 | 35 | :global-vars {*warn-on-reflection* true} 36 | 37 | :clean-targets [:target-path "out"] 38 | 39 | :cljsbuild {:builds 40 | {:test {:source-paths ["src" "test"] 41 | :compiler {:output-to "target/unit-test.js" 42 | :main 'cljstache.runner 43 | :optimizations :whitespace}}}}) 44 | -------------------------------------------------------------------------------- /CHANGES.md: -------------------------------------------------------------------------------- 1 | 2.1 (2022-11-03) 2 | ============ 3 | * Data passed to render can have string keys 4 | 5 | 6 | 2.0.1 (2017-10-07) 7 | ============ 8 | * Fixed behavior of conditionals to treat strings as atomic 9 | * Update mustache spec to v1.1.3 10 | * Add tags function (Lists all of the tags in a template) 11 | * Update dev/test dependencies 12 | 13 | 2.0.0 (2016) 14 | ============ 15 | * Rename to cljstache 16 | * Add support for Clojure 1.7 - 1.9 17 | * Remove support for Clojure < 1.7 18 | * Add support for ClojureScript 1.8 - 1.9 19 | 20 | 1.5.0 (2014-05-07) 21 | ================== 22 | * Handle path whitespace consistently. 23 | 24 | 1.4.0 (2014-05-05) 25 | ================== 26 | * Support variables containing templates. 27 | * Support all seqable data structures. 28 | * Make the data parameter optional. 29 | * Allow lambda sections to render text. 30 | 31 | 1.3.1 (2012-11-20) 32 | ================== 33 | * Fixed rendering of nested partials. 34 | * Moved development dependencies to the dev profile. 35 | 36 | 1.3.0 (2012-04-05) 37 | ================== 38 | * Move from Maven to Leiningen 2. 39 | * Eliminated reflection warnings. 40 | * Added `(parser/render-resource)`. 41 | 42 | 1.2.0 (2012-03-30) 43 | ================== 44 | * Updated to Clojure 1.3.0. 45 | 46 | 1.1.0 (2012-03-27) 47 | ================== 48 | * Added support for lambdas. 49 | 50 | 1.0.0 (2012-01-28) 51 | ================== 52 | * Made Clostache compliant with the Mustache spec. 53 | * Added support for dotted variable names. 54 | * Added support for implicit iterators. 55 | * Added support for partials. 56 | * Added support for set delimiters. 57 | 58 | 0.6.0 (2011-10-28) 59 | ================== 60 | * Fixed rendering issues with dollar signs and backslashes. 61 | * Made missing and nil variables render as empty strings. 62 | 63 | 0.5.0 (2011-09-28) 64 | ================== 65 | * Changed the Maven groupId to de.ubercode.clostache. 66 | * Added support for single value sections. 67 | * Added support for sequences as lists. 68 | 69 | 0.4.1 (2010-12-21) 70 | ================== 71 | * Added support for repeated sections. 72 | 73 | 0.4.0 (2010-11-20) 74 | ================== 75 | * Made HTML escaping identical to mustache.rb. 76 | * Added inverted sections. 77 | 78 | 0.3.0 (2010-10-24) 79 | ================== 80 | * Changed base namespace to `clostache`. 81 | 82 | 0.2.0 (2010-10-20) 83 | ================== 84 | * Added comment tags. 85 | * Made it possible for tags to contain whitespace (e.g. `{{ name }}`). 86 | * Added support for alternative (ampersand) unescape syntax. 87 | * Added boolean sections. 88 | 89 | 0.1.0 (2010-10-16) 90 | ================== 91 | * Added variable tags (escaped and unescaped). 92 | * Added lists. 93 | -------------------------------------------------------------------------------- /test/cljstache/mustache_spec_test.cljc: -------------------------------------------------------------------------------- 1 | (ns cljstache.mustache-spec-test 2 | "Test against the [Mustache spec](http://github.com/mustache/spec)" 3 | (:require [cljstache.core :refer [render]] 4 | [clojure.string :as str] 5 | ;; #?(:cljs [cljs.tools.reader :refer [read-string]]) 6 | ;; #?(:cljs [cljs.js :refer [empty-state js-eval]]) 7 | #?(:clj [clojure.test :refer :all]) 8 | #?(:cljs [cljs.test :refer-macros [deftest testing is]]) 9 | #?(:clj [clojure.data.json :as json])) 10 | #?(:cljs (:require-macros [cljstache.mustache-spec-test :refer [load-specs]]))) 11 | 12 | ;; We load the specs at compile time via macro 13 | ;; for clojurescript compatibility 14 | 15 | (def specs ["comments" "delimiters" "interpolation" "sections" "inverted" "partials" "~lambdas"]) 16 | 17 | (defn- spec-path [spec] (str "test-resources/spec/specs/" spec ".json")) 18 | 19 | #?(:clj (defn- load-spec-tests [spec] 20 | (-> spec spec-path slurp json/read-json :tests))) 21 | 22 | #?(:clj (defmacro load-specs [] 23 | (into {} (for [spec specs] [spec (load-spec-tests spec)])))) 24 | 25 | (def spec-tests (load-specs)) 26 | 27 | (defn- update-lambda-in [data f] 28 | (if (contains? data :lambda) 29 | (update-in data [:lambda] f) 30 | data)) 31 | 32 | (defn- extract-lambdas [data] 33 | (update-lambda-in data #(:clojure %))) 34 | 35 | #?(:cljs (defn load-string [s] s 36 | #_(:value 37 | (when-let [f (read-string s)] 38 | (cljs.js/eval (cljs.js/empty-state) 39 | f 40 | {:eval js-eval 41 | :source-map true 42 | :context :expr} 43 | (fn [x] (println x) x)))))) 44 | 45 | (defn- load-lambdas [data] 46 | (update-lambda-in data #(load-string %))) 47 | 48 | (defn- flatten-string [^String s] 49 | (str/replace (str/replace s "\n" "\\\\n") "\r" "\\\\r")) 50 | 51 | (defn run-spec-test [spec-test] 52 | (let [template (:template spec-test) 53 | readable-data (extract-lambdas (:data spec-test)) 54 | data (load-lambdas readable-data) 55 | partials (:partials spec-test)] 56 | (is (= (:expected spec-test) 57 | (render template data partials)) 58 | (str (:name spec-test) " - " (:desc spec-test) "\nTemplate: \"" 59 | (flatten-string template) "\"\nData: " readable-data 60 | (if partials (str "\nPartials: " partials)))))) 61 | 62 | (defn run-spec-tests [spec] 63 | (doseq [spec-test (spec-tests spec)] 64 | (run-spec-test spec-test))) 65 | 66 | (deftest test-comments 67 | (run-spec-tests "comments")) 68 | 69 | (deftest test-delimiters 70 | (run-spec-tests "delimiters")) 71 | 72 | (deftest test-interpolation 73 | (run-spec-tests "interpolation")) 74 | 75 | (deftest test-sections 76 | (run-spec-tests "sections")) 77 | 78 | (deftest test-inverted 79 | (run-spec-tests "inverted")) 80 | 81 | (deftest test-partials 82 | (run-spec-tests "partials")) 83 | 84 | ;; Unable to load the labdas in cljs due to eval issues 85 | #?(:clj 86 | (deftest test-lambdas 87 | (run-spec-tests "~lambdas"))) 88 | -------------------------------------------------------------------------------- /COPYING: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Cljstache 2 | ========= 3 | {{ [mustache](http://mustache.github.com) }} templates for Clojure[Script]. 4 | 5 | Compliant with the [Mustache spec](http://github.com/mustache/spec), 6 | including lambdas (jvm only) 7 | 8 | Forked from [clostache](https://github.com/fhd/clostache) and updated to be compatible with ClojureScript. 9 | 10 | [![Build Status](https://travis-ci.org/fotoetienne/cljstache.svg?branch=master)](https://travis-ci.org/fotoetienne/cljstache) 11 | [![Clojars Project](https://img.shields.io/clojars/v/cljstache.svg)](https://clojars.org/cljstache) 12 | 13 | Usage 14 | ----- 15 | 16 | To render a template, just pass a template string and a map of data to `render`: 17 | 18 | ```clj 19 | (require '[cljstache.core :refer [render]]) 20 | 21 | (render "Hello, {{name}}!" {:name "Felix"}) 22 | ``` 23 | 24 | The map of data can have keyword or string keys 25 | 26 | On the JVM, you can also render a resource from the classpath like this: 27 | 28 | ```clj 29 | (require '[cljstache.core :refer [render-resource]]) 30 | 31 | (render-resource "templates/hello.mustache" {:name "Michael"}) 32 | ``` 33 | 34 | Both of these functions support an optional third argument, containing partials (see below). 35 | 36 | Examples 37 | -------- 38 | 39 | ### Variable replacement ### 40 | 41 | Variables are tags enclosed by two curly brackets (*mustaches*) and 42 | will be replaced with the respective data. 43 | 44 | Template: 45 | 46 | ```mustache 47 | Hello, {{person}}! 48 | ``` 49 | 50 | Data: 51 | 52 | ```clj 53 | {:person "World"} 54 | ``` 55 | 56 | Output: 57 | 58 | ``` 59 | Hello, World! 60 | ``` 61 | 62 | ### Escaped output ### 63 | 64 | The following characters will be replaced with HTML entities: 65 | `&"<>`. Tags that use three curly brackets or start with `{{&` will 66 | not be escaped. 67 | 68 | Template: 69 | 70 | ```mustache 71 | Escaped: {{html}} 72 | Unescaped: {{{html}}} 73 | Unescaped: {{&html}} 74 | ``` 75 | 76 | Data: 77 | 78 | ```clj 79 | {:html "

Hello, World!

"} 80 | ``` 81 | 82 | Output: 83 | 84 | ```html 85 | Escaped: <h1>Hello, World!</h1> 86 | Unescaped:

Hello, World!

87 | Unescaped:

Hello, World!

88 | ``` 89 | 90 | ### Sections ### 91 | 92 | Sections start with a tag beginning with `{{#` and end with one 93 | beginning with `{{/`. Their content is only rendered if the data is 94 | either the boolean value `true`, a value or a non-empty list. 95 | 96 | Template: 97 | 98 | ```mustache 99 | {{#greet}}Hello, World!{{/greet}} 100 | ``` 101 | 102 | Data: 103 | 104 | ```clj 105 | {:greet true} 106 | ``` 107 | 108 | Output: 109 | 110 | ``` 111 | Hello, World! 112 | ``` 113 | 114 | In case of a list, the section's content is rendered for each element, 115 | and it can contain tags refering to the elements. 116 | 117 | Template: 118 | 119 | ```mustache 120 | 125 | ``` 126 | 127 | Data: 128 | 129 | ```clj 130 | {:people [{:name "Felix"} {:name "Jenny"}]} 131 | ``` 132 | 133 | Output: 134 | 135 | ```html 136 | 140 | ``` 141 | 142 | For single values, the section is rendered exactly once. 143 | 144 | Template: 145 | 146 | ```mustache 147 | {{#greeting}}{{text}}!{{/greeting}} 148 | ``` 149 | 150 | Data: 151 | 152 | ```clj 153 | {:greeting {:text "Hello, World"}} 154 | ``` 155 | 156 | Output: 157 | 158 | ``` 159 | Hello, World! 160 | ``` 161 | 162 | ### Inverted sections ### 163 | 164 | Inverted sections start with a tag beginning with `{{^` and end with one 165 | beginning with `{{/`. Their content is only rendered if the data is 166 | either the boolean value `false` or an empty list. 167 | 168 | Template: 169 | 170 | ```mustache 171 | {{^ignore}}Hello, World!{{/ignore}} 172 | ``` 173 | 174 | Data: 175 | 176 | ```clj 177 | {:ignore false} 178 | ``` 179 | 180 | Output: 181 | 182 | ``` 183 | Hello, World! 184 | ``` 185 | 186 | ### Comments ### 187 | 188 | Comments are tags that begin with `{{!`. They will not be rendered. 189 | 190 | Template: 191 | 192 | ```mustache 193 |

Felix' section

194 | {{! Look ma, I've written a section }} 195 | ``` 196 | 197 | Output: 198 | 199 | ```html 200 |

Felix' section

201 | ``` 202 | 203 | ### Dotted names ### 204 | 205 | Dotted names are a shorter and more convenient way of accessing nested 206 | variables or sections. 207 | 208 | Template: 209 | 210 | ```mustache 211 | {{greeting.text}} 212 | ``` 213 | 214 | Data: 215 | 216 | ```clj 217 | {:greeting {:text "Hello, World"}} 218 | ``` 219 | 220 | Output: 221 | 222 | ``` 223 | Hello, World 224 | ``` 225 | 226 | ### Implicit iterators ### 227 | 228 | Implicit iterators allow you to iterate over a one dimensional list of 229 | elements. 230 | 231 | Template: 232 | 233 | ```mustache 234 | 239 | ``` 240 | 241 | Data: 242 | 243 | ```clj 244 | {:names ["Felix" "Jenny"]} 245 | ``` 246 | 247 | Output: 248 | 249 | ```html 250 | 254 | ``` 255 | 256 | ### Partials ### 257 | 258 | Partials allow you to include other templates (e.g. from separate files). 259 | 260 | Template: 261 | 262 | ```mustache 263 | Hello{{>names}}! 264 | ``` 265 | 266 | Data: 267 | 268 | ```clj 269 | {:people [{:name "Felix"} {:name "Jenny"}]} 270 | ``` 271 | 272 | Partials: 273 | 274 | ```mustache 275 | {:names "{{#people}}, {{name}}{{/people}}"} 276 | ``` 277 | 278 | Output: 279 | 280 | ``` 281 | Hello, Felix, Jenny! 282 | ``` 283 | 284 | #### Using Partials as Includes 285 | 286 | You can use partials as "includes" to build up a document from other pieces. 287 | For example, when building a web page, you can have header and footer template 288 | files that are included in the main page template. This document describes one 289 | way to do that. 290 | 291 | In your project directory (let's call it "my-proj"), create a "resources" 292 | directory if you don't already have one. Then, 293 | 294 | ```sh 295 | cd path/to/my-proj/resources 296 | mkdir templates 297 | cd templates 298 | touch header.mustache footer.mustache my-page.mustache 299 | ``` 300 | 301 | Make header.mustache look something like this: 302 | 303 | ```html 304 | 305 | 306 | {{my-title}} 307 | 308 |

header here!

309 | ``` 310 | 311 | and footer.mustache look something like: 312 | 313 | ```html 314 |

footer here!

315 | 316 | 317 | ``` 318 | 319 | Edit my-page.mustache to contain: 320 | 321 | ```html 322 | {{> header}} 323 | 324 |

{{my-title}}

325 | 326 |

Learn about {{stuff}} here.

327 | 328 | {{> footer}} 329 | ``` 330 | 331 | Now, in your source code (in a Compojure project, this might be 332 | my-proj/src/my_proj/handler.clj), in the `ns` macro's :require 333 | vector, add 334 | 335 | ```clojure 336 | [clostache.parser :refer [render-resource]] 337 | [clojure.java.io :as io] 338 | ``` 339 | 340 | then create a `render-page` helper function: 341 | 342 | ```clojure 343 | (defn render-page 344 | "Pass in the template name (a string, sans its .mustache 345 | filename extension), the data for the template (a map), and a list of 346 | partials (keywords) corresponding to like-named template filenames." 347 | [template data partials] 348 | (render-resource 349 | (str "templates/" template ".mustache") 350 | data 351 | (reduce (fn [accum pt] ;; "pt" is the name (as a keyword) of the partial. 352 | (assoc accum pt (slurp (io/resource (str "templates/" 353 | (name pt) 354 | ".mustache"))))) 355 | {} 356 | partials))) 357 | ``` 358 | 359 | (thanks to samflores for that idea ☺). You'd then call this function like so: 360 | 361 | ```clojure 362 | (render-page "my-page" 363 | {:my-title "My Title" :stuff "giraffes"} 364 | [:header :footer])) 365 | ``` 366 | 367 | Note that the value for :my-title which you pass in makes its way 368 | not only into the my-page.mustache template, but also down into 369 | the included header.mustache. 370 | 371 | ### Set delimiters ### 372 | 373 | You don't have to use mustaches, you can change the delimiters to 374 | anything you like. 375 | 376 | Template: 377 | 378 | ```mustache 379 | {{=<% %>=}} 380 | Hello, <%name%>! 381 | ``` 382 | 383 | Data: 384 | 385 | ```clj 386 | {:name "Felix"} 387 | ``` 388 | 389 | Output: 390 | 391 | ``` 392 | Hello, Felix! 393 | ``` 394 | 395 | ### Lambdas ### 396 | 397 | You can also call functions from templates. 398 | 399 | Template: 400 | 401 | ```mustache 402 | {{hello}} 403 | {{#greet}}Felix{{/greet}} 404 | ``` 405 | 406 | Data: 407 | 408 | ```clj 409 | {:hello "Hello, World!"} 410 | {:greet #(str "Hello, " %)} 411 | ``` 412 | 413 | 414 | Output: 415 | 416 | ``` 417 | Hello, World! 418 | Hello, Felix! 419 | ``` 420 | 421 | Functions can also render the text given to them if they need to do something more complicated. 422 | 423 | Template: 424 | 425 | ```mustache 426 | "{{#people}}Hi {{#upper}}{{name}}{{/upper}}{{/people}}" 427 | ``` 428 | 429 | Data: 430 | ```clj 431 | {:people [{:name "Felix"}] 432 | :upper (fn [text] 433 | (fn [render-fn] 434 | (clojure.string/upper-case (render-fn text))))} 435 | ``` 436 | 437 | Output: 438 | 439 | ``` 440 | Hello FELIX 441 | ``` 442 | 443 | Development 444 | ----------- 445 | 446 | Make sure you have 447 | [Leiningen 2](https://github.com/technomancy/leiningen/wiki/Upgrading) 448 | installed. 449 | 450 | To run the spec tests, fetch them like this: 451 | 452 | ``` 453 | git submodule update --init 454 | ``` 455 | 456 | And run them against all supported Clojure versions: 457 | 458 | ``` 459 | lein test-all 460 | ``` 461 | 462 | Requirements 463 | ------------ 464 | 465 | As cljstache uses Clojure's reader conditionals, cljstache is dependent on both Clojure 1.7 and Leiningen 2.5.2 or later. 466 | Java 8 or greater is required to run the clojurescript tests (using Nashorn.) 467 | 468 | License 469 | ------- 470 | 471 | Copyright (C) 2014 Felix H. Dahlke 472 | 473 | This library is free software; you can redistribute it and/or modify 474 | it under the terms of the GNU Lesser General Public License as 475 | published by the Free Software Foundation; either version 2.1 of the 476 | License, or (at your option) any later version. 477 | 478 | This library is distributed in the hope that it will be useful, but 479 | WITHOUT ANY WARRANTY; without even the implied warranty of 480 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 481 | Lesser General Public License for more details. 482 | 483 | You should have received a copy of the GNU Lesser General Public 484 | License along with this library; see the file COPYING. If not, write 485 | to the Free Software Foundation, Inc., 51 Franklin Street, Fifth 486 | Floor, Boston, MA 02110-1301 USA 487 | 488 | Contributors 489 | ------------ 490 | 491 | * [Felix H. Dahlke](https://github.com/fhd) (Original Author) 492 | * [Stephen Spalding](https://github.com/fotoetienne) (ClojureScript port) 493 | * [Rory Geoghegan](https://github.com/rgeoghegan) 494 | * [Santtu Lintervo](https://github.com/santervo) 495 | * [Pierre-Alexandre St-Jean](https://github.com/pastjean) 496 | * [Michael Klishin](https://github.com/michaelklishin) 497 | * [Stan Rozenraukh](https://github.com/stanistan) 498 | * [Tero Parviainen](https://github.com/teropa) 499 | * [Masashi Iizuka](https://github.com/liquidz) 500 | * [Julian Birch](https://github.com/JulianBirch) 501 | * [Ryan Cole](https://github.com/ryancole) 502 | * [Simon Lawrence](https://github.com/simonl2002) 503 | * [Darrell Hamilton](https://github.com/zeroem) 504 | * [Mike Konkov](https://github.com/wambat) 505 | -------------------------------------------------------------------------------- /test/cljstache/core_test.cljc: -------------------------------------------------------------------------------- 1 | (ns cljstache.core-test 2 | (:require 3 | #?(:clj [clojure.test :refer :all] 4 | :cljs [cljs.test :refer-macros [deftest testing is]]) 5 | #?(:clj [cljstache.core :as cs :refer [render render-resource]]) 6 | #?(:cljs [cljstache.core :as cs :refer [render re-matcher re-find re-groups]]))) 7 | 8 | ;; cljs compatibility tests 9 | 10 | (deftest test-re-quote-replacement 11 | (is (= "$abc" (#'cs/str-replace "foo" "foo" (cs/re-quote-replacement "$abc")))) 12 | (is (= "fabc\\abc" 13 | (#'cs/str-replace "foo" "oo" (cs/re-quote-replacement "abc\\abc")))) 14 | (is (= "f$abc\\abc$" 15 | (#'cs/str-replace "foo" "oo" (cs/re-quote-replacement "$abc\\abc$"))))) 16 | 17 | (deftest re-find-test 18 | (is (= "1" (re-find (re-pattern "\\d") "ab1def"))) 19 | (is (= "1" (re-find (re-matcher #"\d" "ab1def")))) 20 | (is (= ["1" "1"] (re-find (re-matcher #"(\d)" "ab1de3f")))) 21 | (is (= ["<%foo%>" "<%foo%>"] 22 | (re-find 23 | (#'cs/delim-matcher (#'cs/escape-regex "<%") (#'cs/escape-regex "%>") 24 | "asdf <%foo%> lkhasdf")))) 25 | (is (= ["{{=<% %>=}}" "<%" "%>"] 26 | (#'cs/find-custom-delimiters 27 | "\\{\\{" 28 | "\\}\\}" 29 | "{{=<% %>=}}Hello, <%name%>")))) 30 | 31 | (deftest re-groups-test 32 | (let [matcher (re-matcher #"(\d)" "ab1cd3") 33 | match (re-find matcher)] 34 | (is (= ["1" "1"] (vec (re-groups matcher)))))) 35 | 36 | (deftest matcher-find-test 37 | (is (= {:match-start 2 38 | :match-end 7} 39 | (cs/matcher-find (re-matcher #"\d+" "ab12345def")))) 40 | (is (= {:match-start 6 41 | :match-end 7} 42 | (cs/matcher-find (re-matcher #"\d" "ab1def4") 3))) 43 | (is (= {:match-start 5 44 | :match-end 12} 45 | (cs/matcher-find 46 | (#'cs/delim-matcher (#'cs/escape-regex "<%") (#'cs/escape-regex "%>") 47 | "asdf <%foo%> lkhasdf"))))) 48 | 49 | (deftest str-replace-test 50 | (is (= "password" (#'cs/str-replace "pa55word" "\\d" "s"))) 51 | (is (= "password" (#'cs/str-replace "pasword" "s" "ss")))) 52 | 53 | (deftest replace-all-test 54 | (testing "escape-html" 55 | (is (= "<html>&"" 56 | (#'cs/escape-html "&\"")))) 57 | (testing "indent-partial" 58 | (is (= "a\n-->b\n-->c\n-->d" 59 | (#'cs/indent-partial "a\nb\nc\nd" "-->")))) 60 | (testing "escape-regex" 61 | (is (= "\\\\\\{\\}\\[\\]\\(\\)\\.\\?\\^\\+\\-\\|=" 62 | (#'cs/escape-regex "\\{}[]().?^+-|=")))) 63 | (testing "unescape-regex" 64 | (is (= "{}[]().?^+-|=\\" 65 | (#'cs/unescape-regex "\\{\\}\\[\\]\\(\\)\\.\\?\\^\\+\\-\\|=\\"))))) 66 | 67 | (deftest stringbuilder-test 68 | (let [b (fn [] (#'cs/->stringbuilder "abcdef"))] 69 | (testing "sb->str" 70 | (is (= "abcdef" (#'cs/sb->str (b))))) 71 | (testing "sb-replace" 72 | (is (= "abcDDDef" 73 | (-> (b) (#'cs/sb-replace 3 4 "DDD") (#'cs/sb->str))))) 74 | (testing "sb-delete" 75 | (is (= "abcef" 76 | (-> (b) (#'cs/sb-delete 3 4) (#'cs/sb->str))))) 77 | (testing "sb-append" 78 | (is (= "abcdefghijk" 79 | (-> (b) (#'cs/sb-append "ghijk") (#'cs/sb->str))))) 80 | (testing "sb-insert" 81 | (is (= "abc123def" 82 | (-> (b) (#'cs/sb-insert 3 "123") (#'cs/sb->str))))))) 83 | 84 | 85 | 86 | (deftest process-set-delimiters-test 87 | (testing "Correctly replaces custom delimiters" 88 | (is (= ["Hello, {{name}}" {:name "Felix"}] 89 | (#'cs/process-set-delimiters "{{=<% %>=}}Hello, <%name%>" {:name "Felix"})))) 90 | 91 | (testing "Do not replaces other delimiters" 92 | (is (= ["{{greeting}}, {{{names}}}" {:greeting "Hello" :name "Felix&Jenny"}] 93 | (#'cs/process-set-delimiters "{{=<% %>=}}<%greeting%>, {{{names}}}" 94 | {:greeting "Hello" :name "Felix&Jenny"}))))) 95 | 96 | ;; Render Tests 97 | 98 | (deftest test-render-simple 99 | (is (= "Hello, Felix" (render "Hello, {{name}}" {:name "Felix"}))) 100 | (testing "test render string key" 101 | (is (= "Hello, Felix" (render "Hello, {{name}}" {"name" "Felix"}))))) 102 | 103 | (deftest test-render-with-dollar-sign 104 | (is (= "Hello, $Felix!" (render "Hello, {{! This is a comment.}}{{name}}!" 105 | {:name "$Felix"})))) 106 | 107 | (deftest test-render-multi-line 108 | (is (= "Hello\nFelix" (render "Hello\n{{name}}" {:name "Felix"})))) 109 | 110 | (deftest test-nil-variable 111 | (is (= "Hello, " (render "Hello, {{name}}" {:name nil})))) 112 | 113 | (deftest test-missing-variables 114 | (is (= "Hello, . " (render "Hello, {{name}}. {{{greeting}}}" {})))) 115 | 116 | (deftest test-render-html-unescaped 117 | (is (= "&\\\"<>" 118 | (render "{{{string}}}" {:string "&\\\"<>"})))) 119 | 120 | (deftest test-render-html-unescaped-ampersand 121 | (is (= "&\"<>" 122 | (render "{{&string}}" {:string "&\"<>"})))) 123 | 124 | (deftest test-render-html-escaped 125 | (is (= "&"<>" 126 | (render "{{string}}" {:string "&\"<>"})))) 127 | 128 | (deftest test-render-list 129 | (is (= "Hello, Felix, Jenny!" (render "Hello{{#names}}, {{name}}{{/names}}!" 130 | {:names [{:name "Felix"} 131 | {:name "Jenny"}]})))) 132 | 133 | (deftest test-render-list-twice 134 | (is (= "Hello, Felix, Jenny! Hello, Felix, Jenny!" 135 | (render (str "Hello{{#names}}, {{name}}{{/names}}! " 136 | "Hello{{#names}}, {{name}}{{/names}}!") 137 | {:names [{:name "Felix"} {:name "Jenny"}]})))) 138 | 139 | (deftest test-render-single-value 140 | (is (= "Hello, Felix!" (render "Hello{{#person}}, {{name}}{{/person}}!" 141 | {:person {:name "Felix"}})))) 142 | 143 | (deftest test-render-seq 144 | (is (= "Hello, Felix, Jenny!" (render "Hello{{#names}}, {{name}}{{/names}}!" 145 | {:names (seq [{:name "Felix"} 146 | {:name "Jenny"}])})))) 147 | 148 | (deftest test-render-hash 149 | ; according to mustache(5) non-false, non-list value 150 | ; should be used as a context for a single rendering of a block 151 | (is (= "Hello, Felix!" (render "Hello{{#person}}, {{name}}{{/person}}!" 152 | {:person {:name "Felix"}})))) 153 | 154 | (deftest test-render-empty-list 155 | (is (= "" (render "{{#things}}Something{{/things}}" {:things []})))) 156 | 157 | 158 | (deftest test-render-nested-list 159 | (is (= "z" (render "{{#x}}{{#y}}{{z}}{{/y}}{{/x}}" {:x {:y {:z "z"}}})))) 160 | 161 | (deftest test-render-comment 162 | (is (= "Hello, Felix!" (render "Hello, {{! This is a comment.}}{{name}}!" 163 | {:name "Felix"})))) 164 | 165 | (deftest test-render-tags-with-whitespace 166 | (is (= "Hello, Felix" (render "Hello, {{# names }}{{ name }}{{/ names }}" 167 | {:names [{:name "Felix"}]})))) 168 | 169 | (deftest test-render-boolean-true 170 | (is (= "Hello, Felix" (render "Hello, {{#condition}}Felix{{/condition}}" 171 | {:condition true})))) 172 | 173 | (deftest test-render-boolean-false 174 | (is (= "Hello, " (render "Hello, {{#condition}}Felix{{/condition}}" 175 | {:condition false})))) 176 | 177 | (deftest test-render-atomic-string 178 | (is (= "Hello, Atomic string" (render "Hello{{#string}}, {{string}}{{/string}}" 179 | {:string "Atomic string"})))) 180 | 181 | (deftest test-render-inverted-empty-list 182 | (is (= "Empty" (render "{{^things}}Empty{{/things}}" {:things []})))) 183 | 184 | (deftest test-render-inverted-list 185 | (is (= "" (render "{{^things}}Empty{{/things}}" {:things ["Something"]})))) 186 | 187 | (deftest test-render-inverted-boolean-true 188 | (is (= "Hello, " (render "Hello, {{^condition}}Felix{{/condition}}" 189 | {:condition true})))) 190 | 191 | (deftest test-render-inverted-boolean-false 192 | (is (= "Hello, Felix" (render "Hello, {{^condition}}Felix{{/condition}}" 193 | {:condition false})))) 194 | 195 | (deftest test-render-with-delimiters 196 | (is (= "Hello, Felix" (render "{{=<% %>=}}Hello, <%name%>" {:name "Felix"})))) 197 | 198 | 199 | (deftest test-render-with-regex-delimiters 200 | (is (= "Hello, Felix" (render "{{=[ ]=}}Hello, [name]" {:name "Felix"})))) 201 | 202 | 203 | (deftest test-render-with-delimiters-changed-twice 204 | (is (= "Hello, Felix" (render "{{=[ ]=}}[greeting], [=<% %>=]<%name%>" 205 | {:greeting "Hello" :name "Felix"})))) 206 | 207 | (deftest test-render-twice-with-different-delimiters 208 | (is (= "Hello, Felix&Jenny!" 209 | (let [data {:greeting "Hello" :names "Felix&Jenny"}] 210 | (-> "{{={% %}=}}{%greeting%}, {{&names}}!" 211 | (render data) 212 | (render data))))) 213 | 214 | (is (= "Hello, Felix&Jenny!" 215 | (let [data {:greeting "Hello" :names "Felix&Jenny"}] 216 | (-> "{{={% %}=}}{%greeting%}, {{{names}}}!" 217 | (render data) 218 | (render data))))) 219 | 220 | (is (= "Hello, Felix&Jenny!" 221 | (let [data {:greeting "Hello" :names "Felix&Jenny"}] 222 | (-> "{{greeting}}, {{{names}}}!" 223 | (render data) 224 | (render data)))))) 225 | 226 | (deftest test-render-tag-with-dotted-name-like-section 227 | (is (= "Hello, Felix" (render "Hello, {{felix.name}}" 228 | {:felix {:name "Felix"}}))) 229 | (testing "now with string keys" 230 | (is (= "Hello, Felix" (render "Hello, {{felix.name}}" 231 | {:felix {"name" "Felix"}}))))) 232 | 233 | (deftest test-string-section-data 234 | (is (= "Hello Felix" 235 | (render "Hello {{#foo.bar}}{{felix.name}}{{/foo.bar}}" {"foo" {"bar" "yo"} :felix {:name "Felix"}})))) 236 | 237 | (deftest test-render-multiple-sections 238 | (is (= "Hello\n\nFelix\n\nFelix\n\n!" 239 | (render "Hello\n\n{{#felix}}{{name}}{{/felix}}\n\n{{#felix}}{{name}}{{/felix}}\n\n!" {:felix {:name "Felix"}}))) 240 | (is (= "Hello\n\nFelix\n\nFelix\n\n!" 241 | (render "Hello\n\n{{#felix}}\n{{name}}\n{{/felix}}\n\n{{#felix}}{{name}}{{/felix}}\n\n!" {:felix {:name "Felix"}}))) 242 | (is (= "Hello\n\nFelix\n\nFelix\n\n!" 243 | (render "Hello\n\n{{#felix}}{{name}}{{/felix}}\n\n{{#felix}}\n{{name}}\n{{/felix}}\n\n!" {:felix {:name "Felix"}}))) 244 | (is (= "Hello\n\nFelix\n\nFelix\n\n!" 245 | (render "Hello\n\n{{#felix}}\n{{name}}\n{{/felix}}\n\n{{#felix}}\n{{name}}\n{{/felix}}\n\n!" {:felix {:name "Felix"}})))) 246 | 247 | (deftest test-render-lambda 248 | (is (= "Hello, Felix" (render "Hello, {{name}}" 249 | {:name (fn [] "Felix")})))) 250 | 251 | (deftest test-render-lambda-with-params 252 | (is (= "Hello, Felix" (render "{{#greet}}Felix{{/greet}}" 253 | {:greet #(str "Hello, " %)}))) 254 | (is (= "Hi TOM Hi BOB " 255 | (render "{{#people}}Hi {{#upper}}{{name}}{{/upper}} {{/people}}" 256 | {:people [{:name "Tom"}, {:name "Bob"}] 257 | :upper (fn [text] (fn [render-fn] (clojure.string/upper-case (render-fn text))))})))) 258 | 259 | ;; Not implemented for cljs 260 | #?(:clj 261 | (deftest test-render-resource-template 262 | (is (= "Hello, Felix" (render-resource "templates/hello.mustache" {:name "Felix"}))))) 263 | 264 | (deftest test-render-with-partial 265 | (is (= "Hi, Felix" (render "Hi, {{>name}}" {:n "Felix"} {:name "{{n}}"})))) 266 | 267 | (deftest test-render-partial-recursive 268 | (is (= "One Two Three Four Five" (render "One {{>two}}" 269 | {} 270 | {:two "Two {{>three}}" 271 | :three "Three {{>four}}" 272 | :four "Four {{>five}}" 273 | :five "Five"})))) 274 | 275 | (deftest test-render-with-variable-containing-template 276 | (is (= "{{hello}},world" (render "{{tmpl}},{{hello}}" {:tmpl "{{hello}}" :hello "world"})))) 277 | 278 | (deftest test-render-sorted-set 279 | (let [sort-by-x (fn [x y] (compare (:x x) (:x y))) 280 | l (sorted-set-by sort-by-x {:x 1} {:x 5} {:x 3})] 281 | (is (= "135" (render "{{#l}}{{x}}{{/l}}" {:l l}))) 282 | (is (= "" (render "{{^l}}X{{/l}}" {:l l})))) 283 | (is (= "X" (render "{{^l}}X{{/l}}" {:l (sorted-set)})))) 284 | 285 | (deftest test-path-whitespace-handled-consistently 286 | (is (= (render "{{a}}" {:a "value"}) "value")) 287 | (is (= (render "{{ a }}" {:a "value"}) "value")) 288 | (is (= (render "{{a.b}}" {:a {:b "value"}}) "value")) 289 | (is (= (render "{{ a.b }}" {:a {:b "value"}}) "value"))) 290 | 291 | (deftest test-ignore-invalid-tag 292 | (is (= (render "{{#a}}" {:a ["value"]}) "")) 293 | (is (= (render "{{/a}}" {:a ["value"]}) "")) 294 | (is (= (render "{{#a" {:a ["value"]}) "{{#a")) 295 | (is (= (render "{{#a}}{{/" {:a ["value"]}) "{{/"))) 296 | -------------------------------------------------------------------------------- /src/cljstache/core.cljc: -------------------------------------------------------------------------------- 1 | (ns cljstache.core 2 | "A parser for mustache templates." 3 | #?(:clj (:refer-clojure :exclude (seqable?)) 4 | :cljs (:refer-clojure :exclude [re-find])) 5 | (:require #?(:clj [clojure.java.io :as io]) 6 | [clojure.string :as str :refer [split]])) 7 | 8 | ;; cljs support 9 | (def re-quote-replacement 10 | #?(:clj str/re-quote-replacement 11 | :cljs identity)) 12 | 13 | ;; clj < 1.9 support 14 | #?(:clj 15 | (def ^Boolean seqable? 16 | "Returns true if (seq x) will succeed, false otherwise. 17 | Included in clojure core from v1.9" 18 | (when (-> "seqable?" symbol resolve) 19 | seqable? 20 | (fn [x] 21 | (or (seq? x) 22 | (instance? clojure.lang.Seqable x) 23 | (nil? x) 24 | (instance? Iterable x) 25 | (-> x .getClass .isArray) 26 | (string? x) 27 | (instance? java.util.Map x)))))) 28 | 29 | (defn- ^String map-str 30 | "Apply f to each element of coll, concatenate all results into a 31 | String." 32 | [f coll] 33 | (apply str (map f coll))) 34 | 35 | ; To match clj regex api 36 | #?(:cljs 37 | (defn re-matcher [pattern s] 38 | [(re-pattern pattern) s])) 39 | 40 | #?(:cljs 41 | (defn re-find 42 | ([re s] (cljs.core/re-find re s)) 43 | ([[re s]] (re-find re s)))) 44 | 45 | #?(:cljs 46 | (defn re-groups 47 | ([[re s]] (.exec re s)))) 48 | 49 | (defn matcher-find 50 | ([^java.util.regex.Matcher m] (matcher-find m 0)) 51 | #?(:clj 52 | ([^java.util.regex.Matcher m offset] 53 | (when (.find m offset) 54 | {:match-start (.start m) 55 | :match-end (.end m)}))) 56 | #?(:cljs 57 | ([[m s] offset] 58 | (when-let [match (.exec m (subs s offset))] 59 | {:match-start (+ (.-index match) offset) 60 | :match-end (+ (.-index match) (-> match first count) offset)})))) 61 | 62 | (defrecord Section [name body start end inverted]) 63 | 64 | (defn- str-replace 65 | "Replace all instances of pattern in str" 66 | [^String s ^String from ^String to] 67 | #?(:clj (.replaceAll s from to)) 68 | #?(:cljs (str/replace s (re-pattern from) to))) 69 | 70 | (defn- replace-all 71 | "Applies all replacements from the replacement list to the string. 72 | Replacements are a sequence of two element sequences where the first element 73 | is the pattern to match and the second is the replacement. 74 | An optional third boolean argument can be set to true if the replacement 75 | should not be quoted." 76 | [string replacements] 77 | (reduce (fn [string [from to dont-quote]] 78 | (str-replace (str string) from 79 | (if dont-quote 80 | to 81 | (re-quote-replacement to)))) 82 | string replacements)) 83 | 84 | (defn- escape-html 85 | "Replaces angle brackets with the respective HTML entities." 86 | [string] 87 | (replace-all string [["&" "&"] 88 | ["\"" """] 89 | ["<" "<"] 90 | [">" ">"]])) 91 | 92 | (defn- indent-partial 93 | "Indent all lines of the partial by indent." 94 | [partial indent] 95 | (replace-all partial [["(\r\n|[\r\n])(.+)" (str "$1" indent "$2") true]])) 96 | 97 | (def regex-chars ["\\" "{" "}" "[" "]" "(" ")" "." "?" "^" "+" "-" "|"]) 98 | 99 | (defn- escape-regex 100 | "Escapes characters that have special meaning in regular expressions." 101 | [regex] 102 | (replace-all regex (map #(repeat 2 (str "\\" %)) regex-chars))) 103 | 104 | (defn- unescape-regex 105 | "Unescapes characters that have special meaning in regular expressions." 106 | [regex] 107 | (replace-all regex (map (fn [char] [(str "\\\\\\" char) char true]) 108 | regex-chars))) 109 | 110 | (defn- ^StringBuilder ->stringbuilder 111 | ([] (->stringbuilder "")) 112 | #?(:clj ([^String s] (StringBuilder. s))) 113 | #?(:cljs ([s] (atom s)))) 114 | 115 | (defn- sb! 116 | "Perform mutation on stringbuilder object" 117 | [s f] 118 | (swap! s f) s) 119 | 120 | (defn- ^String sb->str [^StringBuilder s] 121 | #?(:clj (.toString s)) 122 | #?(:cljs @s)) 123 | 124 | (defn- ^StringBuilder sb-replace 125 | [^StringBuilder s ^Integer start ^Integer end ^String s'] 126 | #?(:clj (.replace s start end s')) 127 | #?(:cljs (sb! s #(str (subs % 0 start) s' (subs % end))))) 128 | 129 | (defn- ^StringBuilder sb-delete 130 | [^StringBuilder s ^Integer start ^Integer end] 131 | #?(:clj (.delete s start end)) 132 | #?(:cljs (sb! s #(str (subs % 0 start) (subs % end))))) 133 | 134 | (defn- ^StringBuilder sb-append 135 | [^StringBuilder s s'] 136 | #?(:clj (.append s s')) 137 | #?(:cljs (sb! s #(str % s')))) 138 | 139 | (defn- ^StringBuilder sb-insert 140 | [^StringBuilder s ^Integer index ^StringBuilder s'] 141 | #?(:clj (.insert s index s')) 142 | #?(:cljs (sb-replace s index index s'))) 143 | 144 | (defn- delim-matcher [open close s] 145 | (re-matcher 146 | (re-pattern (str "(" open ".*?" close 147 | (when (not= "\\{\\{" open) 148 | (str "|\\{\\{.*?\\}\\}")) 149 | ")")) 150 | s)) 151 | 152 | (defn- find-custom-delimiters [open close s] 153 | (re-find (re-pattern (str open "=\\s*(.*?) (.*?)\\s*=" close)) s)) 154 | 155 | (defn- process-set-delimiters 156 | "Replaces custom set delimiters with mustaches." 157 | [^String template data] 158 | (let [builder (->stringbuilder template) 159 | data (atom data) 160 | open-delim (atom (escape-regex "{{")) 161 | close-delim (atom (escape-regex "}}")) 162 | set-delims (fn [open close] 163 | (doseq [[var delim] 164 | [[open-delim open] [close-delim close]]] 165 | (swap! var (constantly (escape-regex delim)))))] 166 | (loop [offset 0] 167 | (let [custom-delim? (not= "\\{\\{" @open-delim) 168 | s (sb->str builder) 169 | matcher (delim-matcher @open-delim @close-delim s)] 170 | (when-let [match-result (matcher-find matcher offset)] 171 | (let [{:keys [match-start match-end]} match-result 172 | match (subs s match-start match-end)] 173 | (cond 174 | (and custom-delim? (= "{{{" (subs match 0 3))) 175 | (when-let [tag (re-find #"\{\{\{(.*?)\}\}\}" match)] 176 | (sb-replace builder match-start match-end 177 | (str "\\{\\{\\{" (second tag) "\\}\\}\\}")) 178 | (recur (int match-end))) 179 | 180 | (and custom-delim? (= "{{" (subs match 0 2))) 181 | (when-let [tag (re-find #"\{\{(.*?)\}\}" match)] 182 | (sb-replace builder match-start match-end 183 | (str "\\{\\{" (second tag) "\\}\\}")) 184 | (recur (int match-end))) 185 | 186 | :else 187 | (if-let [delim-change 188 | (find-custom-delimiters 189 | @open-delim @close-delim match)] 190 | (do 191 | (apply set-delims (rest delim-change)) 192 | (sb-delete builder match-start match-end) 193 | (recur (int match-start))) 194 | (when-let [tag (re-find 195 | (re-pattern (str @open-delim "(.*?)" 196 | @close-delim)) 197 | match)] 198 | (let [section-start (re-find (re-pattern 199 | (str "^" 200 | @open-delim 201 | "\\s*#\\s*(.*?)\\s*" 202 | @close-delim)) 203 | (first tag)) 204 | key (if section-start (keyword (second section-start))) 205 | value (if key (key @data))] 206 | (if (and value (fn? value) 207 | (not (and (= @open-delim "\\{\\{") 208 | (= @close-delim "\\}\\}")))) 209 | (swap! data 210 | #(update-in % [key] 211 | (fn [old] 212 | (fn [data] 213 | (str "{{=" 214 | (unescape-regex @open-delim) 215 | " " 216 | (unescape-regex @close-delim) 217 | "=}}" 218 | (old data))))))) 219 | (sb-replace builder match-start match-end 220 | (str "{{" (second tag) "}}")) 221 | (recur (int match-end)))))))))) 222 | [(sb->str builder) @data])) 223 | 224 | (defn- create-partial-replacements 225 | "Creates pairs of partial replacements." 226 | [template partials] 227 | (apply concat 228 | (for [k (keys partials)] 229 | (let [regex (re-pattern (str "(\r\n|[\r\n]|^)([ \\t]*)\\{\\{>\\s*" 230 | (name k) "\\s*\\}\\}")) 231 | indent (nth (first (re-seq (re-pattern regex) template)) 2)] 232 | [[(str "\\{\\{>\\s*" (name k) "\\s*\\}\\}") 233 | (first (process-set-delimiters (indent-partial (str (get partials k)) 234 | indent) {}))]])))) 235 | 236 | (defn- include-partials 237 | "Include partials within the template." 238 | [template partials] 239 | (replace-all template (create-partial-replacements template partials))) 240 | 241 | (defn- remove-comments 242 | "Removes comments from the template." 243 | [template] 244 | (let [comment-regex "\\{\\{\\![^\\}]*\\}\\}"] 245 | (replace-all template [[(str "(^|[\n\r])[ \t]*" comment-regex 246 | "(\r\n|[\r\n]|$)") "$1" true] 247 | [comment-regex ""]]))) 248 | #?(:clj 249 | (defn- next-index 250 | "Return the next index of the supplied regex." 251 | ([section regex] (next-index section regex 0)) 252 | ([^String section regex index] 253 | (if (= index -1) 254 | -1 255 | (let [s (subs section index) 256 | matcher (re-matcher regex s)] 257 | (if (nil? (re-find matcher)) 258 | -1 259 | (+ index (.start (.toMatchResult matcher))))))))) 260 | 261 | #?(:cljs 262 | (defn- next-index 263 | "Return the next index of the supplied regex." 264 | ([section regex] (next-index section regex 0)) 265 | ([^String section regex index] 266 | (if (= index -1) 267 | -1 268 | (let [s (subs section index) 269 | matcher (js/RegExp. (.-source (str regex)) "g")] 270 | (if-let [m (.exec regex s)] 271 | (+ index (.-index m)) 272 | -1)))))) 273 | 274 | (defn- find-section-start-tag 275 | "Find the next section start tag, starting to search at index." 276 | [^String template index] 277 | (next-index template #"\{\{[#\^][^\}]*\}\}" index)) 278 | 279 | (defn- find-section-end-tag 280 | "Find the matching end tag for a section at the specified level, 281 | starting to search at index." 282 | [^String template 283 | #?(:clj ^long index :cljs index) 284 | #?(:clj ^long level :cljs level)] 285 | (let [next-start (find-section-start-tag template index) 286 | next-end (next-index template #"\{\{/[^\}]*\}\}" index)] 287 | (if (= next-end -1) 288 | -1 289 | (if (and (not= next-start -1) (< next-start next-end)) 290 | (find-section-end-tag template (+ next-start 3) (inc level)) 291 | (if (= level 1) 292 | next-end 293 | (find-section-end-tag template (+ next-end 3) (dec level))))))) 294 | 295 | (defn- extract-section 296 | "Extracts the outer section from the template." 297 | [^String template] 298 | (let [#?(:clj ^Long start :cljs start) 299 | (find-section-start-tag template 0)] 300 | (when (not= start -1) 301 | (let [inverted (= (str (.charAt template (+ start 2))) "^") 302 | ^Long end-tag (find-section-end-tag template (+ start 3) 1)] 303 | (when (not= end-tag -1) 304 | (let [end (+ (.indexOf template "}}" end-tag) 2) 305 | section (subs template start end) 306 | body-start (+ (.indexOf section "}}") 2) 307 | body-end (.lastIndexOf section "{{") 308 | body (if (or (= body-start -1) (= body-end -1) 309 | (< body-end body-start)) 310 | "" 311 | (subs section body-start body-end)) 312 | section-name (.trim (subs section 3 313 | (.indexOf section "}}")))] 314 | (Section. section-name body start end inverted))))))) 315 | 316 | (defn- replace-all-callback 317 | "Replaces each occurrence of the regex with the return value of the callback." 318 | [^String string regex callback] 319 | (str/replace string regex callback)) 320 | 321 | (declare render-template) 322 | 323 | (defn replace-variables 324 | "Replaces variables in the template with their values from the data." 325 | [template data partials] 326 | (let [regex #"\{\{(\{|\&|\>|)\s*(.*?)\s*\}{2,3}"] 327 | (replace-all-callback template regex 328 | #(let [var-name (nth % 2) 329 | var-k (keyword var-name) 330 | var-type (second %) 331 | var-value (or (get data var-k) (get data var-name)) 332 | var-value (if (fn? var-value) 333 | (render-template 334 | (var-value) 335 | (dissoc data var-name) 336 | partials 337 | false) 338 | var-value) 339 | var-value (str var-value)] 340 | (cond (= var-type "") (escape-html var-value) 341 | (= var-type ">") (render-template (var-k partials) data partials false) 342 | :else var-value))))) 343 | 344 | (defn- join-standalone-delimiter-tags 345 | "Remove newlines after standalone (i.e. on their own line) delimiter tags." 346 | [template] 347 | (replace-all 348 | template 349 | (let [eol-start "(\r\n|[\r\n]|^)" 350 | eol-end "(\r\n|[\r\n]|$)"] 351 | [[(str eol-start "[ \t]*(\\{\\{=[^\\}]*\\}\\})" eol-end) "$1$2" 352 | true]]))) 353 | 354 | (defn- path-data 355 | "Extract the data for the supplied path." 356 | [elements data] 357 | (reduce (fn [r n] 358 | (or (get r (keyword n)) (get r n))) 359 | data 360 | elements)) 361 | 362 | (defn- convert-path 363 | "Convert a tag with a dotted name to nested sections, using the 364 | supplied delimiters to access the value." 365 | [tag open-delim close-delim data] 366 | (let [tag-type (last open-delim) 367 | section-tag (some #{tag-type} [\# \^ \/]) 368 | section-end-tag (= tag-type \/) 369 | builder (->stringbuilder) 370 | tail-builder (when-not section-tag (->stringbuilder)) 371 | elements (split tag #"\.") 372 | element-to-invert (when (= tag-type \^) 373 | (loop [path [(first elements)] 374 | remaining-elements (rest elements)] 375 | (when-not (empty? remaining-elements) 376 | (if (nil? (path-data path data)) 377 | (last path) 378 | (recur (conj path (first remaining-elements)) 379 | (next remaining-elements))))))] 380 | (if (and (not section-tag) (nil? (path-data elements data))) 381 | "" 382 | (let [elements (if section-end-tag (reverse elements) elements)] 383 | (doseq [element (butlast elements)] 384 | (sb-append builder (str "{{" (if section-end-tag "/" 385 | (if (= element element-to-invert) 386 | "^" "#")) 387 | element "}}")) 388 | (if (not (nil? tail-builder)) 389 | (sb-insert tail-builder 0 (str "{{/" element "}}")))) 390 | (sb-append builder (str open-delim (last elements) close-delim)) 391 | (str (sb->str builder) (if (not (nil? tail-builder)) 392 | (sb->str tail-builder))))))) 393 | 394 | (defn- convert-paths 395 | "Converts tags with dotted tag names to nested sections." 396 | [^String template data] 397 | (loop [^String s ^String template] 398 | (let [matcher (re-matcher #"(\{\{[\{&#\^/]?)([^\}]+\.[^\}]+)(\}{2,3})" s)] 399 | (if-let [match-result (matcher-find matcher)] 400 | (let [{:keys [match-start match-end]} match-result 401 | groups (re-groups matcher) 402 | converted (convert-path (str/trim (nth groups 2)) (nth groups 1) 403 | (nth groups 3) data)] 404 | (recur (str (subs s 0 match-start) converted 405 | (subs s match-end)))) 406 | s)))) 407 | 408 | (defn- join-standalone-tags 409 | "Remove newlines after standalone (i.e. on their own line) section/partials 410 | tags." 411 | [template] 412 | (replace-all 413 | template 414 | (let [eol-start "(\r\n|[\r\n]|^)" 415 | eol-end "(\r\n|[\r\n]|$)"] 416 | [[(str eol-start 417 | "\\{\\{[#\\^][^\\}]*\\}\\}(\r\n|[\r\n])\\{\\{/[^\\}]*\\}\\}" 418 | eol-end) 419 | "$1" true] 420 | [(str eol-start "[ \t]*(\\{\\{[#\\^/][^\\}]*\\}\\})" eol-end) "$1$2" 421 | true] 422 | [(str eol-start "([ \t]*\\{\\{>\\s*[^\\}]*\\s*\\}\\})" eol-end) "$1$2" 423 | true]]))) 424 | 425 | (defn- delimiter-preprocess 426 | [template data] 427 | (let [template (join-standalone-delimiter-tags template) 428 | [template data] (process-set-delimiters template data)] 429 | [template data])) 430 | 431 | (defn- preprocess 432 | "Preprocesses template and data (e.g. removing comments)." 433 | [template data partials] 434 | (let [[template1 data1] (delimiter-preprocess template data) 435 | template2 (join-standalone-delimiter-tags template1) 436 | [template3 data2] (process-set-delimiters template2 data1) 437 | template4 (join-standalone-tags template3) 438 | template5 (remove-comments template4) 439 | template6 (include-partials template5 partials) 440 | template7 (convert-paths template6 data2)] 441 | [template7 data2])) 442 | 443 | (defn- render-section 444 | [section data partials] 445 | (let [section-data (or (get data (:name section)) ((keyword (:name section)) data))] 446 | (if (:inverted section) 447 | (if (or (and (seqable? section-data) (empty? section-data)) 448 | (not section-data)) 449 | (:body section)) 450 | (if section-data 451 | (if (fn? section-data) 452 | (let [result (section-data (:body section))] 453 | (if (fn? result) 454 | (result #(render-template % data partials false)) 455 | result)) 456 | (let [section-data (cond (string? section-data) [{}] 457 | (sequential? section-data) section-data 458 | (map? section-data) [section-data] 459 | (seqable? section-data) (seq section-data) 460 | :else [{}]) 461 | section-data1 (if (map? (first section-data)) 462 | section-data 463 | (map (fn [e] {(keyword ".") e}) 464 | section-data)) 465 | section-data2 (map #(conj data %) section-data1)] 466 | (map-str (fn [m] 467 | (render-template (:body section) m partials false)) 468 | section-data2))))))) 469 | 470 | (defn- render-template 471 | "Renders the template with the data and partials." 472 | [^String template data partials skip-delimiter-preprocess?] 473 | (let [[^String template data] (if skip-delimiter-preprocess? 474 | [template data] 475 | (delimiter-preprocess template data )) 476 | ^String section (extract-section template)] 477 | (if (nil? section) 478 | (replace-variables template data partials) 479 | (let [before (subs template 0 (:start section)) 480 | after (subs template (:end section))] 481 | (recur (str before (render-section section data partials) after) data 482 | partials 483 | false))))) 484 | 485 | (defn tags 486 | "Returns set of all tags in template" 487 | [template] 488 | (let [[^String template data] (preprocess template {} {}) 489 | matches (re-seq #"\{\{(\{|\&|\>|)\s*(.*?)\s*\}{2,3}" template) 490 | tags (map (comp keyword last) matches)] 491 | (set tags))) 492 | 493 | (defn render 494 | "Renders the template with the data and, if supplied, partials." 495 | ([template] 496 | (render template {} {})) 497 | ([template data] 498 | (render template data {})) 499 | ([template data partials] 500 | (let [[template data] (preprocess template data partials)] 501 | (replace-all (render-template template data partials true) 502 | [["\\\\\\{\\\\\\{" "{{"] 503 | ["\\\\\\}\\\\\\}" "}}"]])))) 504 | 505 | #?(:clj 506 | (defn render-resource 507 | "Renders a resource located on the classpath. 508 | Only available on the JVM" 509 | ([^String path] 510 | (render (slurp (io/resource path)) {})) 511 | ([^String path data] 512 | (render (slurp (io/resource path)) data)) 513 | ([^String path data partials] 514 | (render (slurp (io/resource path)) data partials)))) 515 | --------------------------------------------------------------------------------