├── 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.
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 |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 | --------------------------------------------------------------------------------