├── .gitignore
├── .travis.yml
├── LICENSE
├── README.md
├── project.clj
├── src
└── stencil
│ ├── ast.clj
│ ├── core.clj
│ ├── loader.clj
│ ├── parser.clj
│ ├── re_utils.clj
│ └── utils.clj
└── test
└── stencil
└── test
├── core.clj
├── extensions.clj
├── no_cache.clj
├── parser.clj
├── re_utils.clj
├── spec.clj
└── utils.clj
/.gitignore:
--------------------------------------------------------------------------------
1 | .cake
2 | pom.xml
3 | *.jar
4 | *.war
5 | lib
6 | classes
7 | build
8 | /stencil
9 | /target
10 | .lein-*
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: clojure
2 | lein: lein2
3 | script: lein2 all test
4 | jdk:
5 | - openjdk7
6 | - openjdk6
7 | - oraclejdk7
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | Eclipse Public License - v 1.0
2 |
3 | THE ACCOMPANYING PROGRAM IS PROVIDED UNDER THE TERMS OF THIS ECLIPSE
4 | PUBLIC LICENSE ("AGREEMENT"). ANY USE, REPRODUCTION OR DISTRIBUTION OF
5 | THE PROGRAM CONSTITUTES RECIPIENT'S ACCEPTANCE OF THIS AGREEMENT.
6 |
7 | 1. DEFINITIONS
8 |
9 | "Contribution" means:
10 |
11 | a) in the case of the initial Contributor, the initial code and
12 | documentation distributed under this Agreement, and
13 |
14 | b) in the case of each subsequent Contributor:
15 |
16 | i) changes to the Program, and
17 |
18 | ii) additions to the Program;
19 |
20 | where such changes and/or additions to the Program originate from and
21 | are distributed by that particular Contributor. A Contribution
22 | 'originates' from a Contributor if it was added to the Program by such
23 | Contributor itself or anyone acting on such Contributor's
24 | behalf. Contributions do not include additions to the Program which:
25 | (i) are separate modules of software distributed in conjunction with
26 | the Program under their own license agreement, and (ii) are not
27 | derivative works of the Program.
28 |
29 | "Contributor" means any person or entity that distributes the Program.
30 |
31 | "Licensed Patents" mean patent claims licensable by a Contributor
32 | which are necessarily infringed by the use or sale of its Contribution
33 | alone or when combined with the Program.
34 |
35 | "Program" means the Contributions distributed in accordance with this
36 | Agreement.
37 |
38 | "Recipient" means anyone who receives the Program under this
39 | Agreement, including all Contributors.
40 |
41 | 2. GRANT OF RIGHTS
42 |
43 | a) Subject to the terms of this Agreement, each Contributor hereby
44 | grants Recipient a non-exclusive, worldwide, royalty-free copyright
45 | license to reproduce, prepare derivative works of, publicly display,
46 | publicly perform, distribute and sublicense the Contribution of such
47 | Contributor, if any, and such derivative works, in source code and
48 | object code form.
49 |
50 | b) Subject to the terms of this Agreement, each Contributor hereby
51 | grants Recipient a non-exclusive, worldwide, royalty-free patent
52 | license under Licensed Patents to make, use, sell, offer to sell,
53 | import and otherwise transfer the Contribution of such Contributor, if
54 | any, in source code and object code form. This patent license shall
55 | apply to the combination of the Contribution and the Program if, at
56 | the time the Contribution is added by the Contributor, such addition
57 | of the Contribution causes such combination to be covered by the
58 | Licensed Patents. The patent license shall not apply to any other
59 | combinations which include the Contribution. No hardware per se is
60 | licensed hereunder.
61 |
62 | c) Recipient understands that although each Contributor grants the
63 | licenses to its Contributions set forth herein, no assurances are
64 | provided by any Contributor that the Program does not infringe the
65 | patent or other intellectual property rights of any other entity. Each
66 | Contributor disclaims any liability to Recipient for claims brought by
67 | any other entity based on infringement of intellectual property rights
68 | or otherwise. As a condition to exercising the rights and licenses
69 | granted hereunder, each Recipient hereby assumes sole responsibility
70 | to secure any other intellectual property rights needed, if any. For
71 | example, if a third party patent license is required to allow
72 | Recipient to distribute the Program, it is Recipient's responsibility
73 | to acquire that license before distributing the Program.
74 |
75 | d) Each Contributor represents that to its knowledge it has sufficient
76 | copyright rights in its Contribution, if any, to grant the copyright
77 | license set forth in this Agreement.
78 |
79 | 3. REQUIREMENTS
80 |
81 | A Contributor may choose to distribute the Program in object code form
82 | under its own license agreement, provided that:
83 |
84 | a) it complies with the terms and conditions of this Agreement; and
85 |
86 | b) its license agreement:
87 |
88 | i) effectively disclaims on behalf of all Contributors all warranties
89 | and conditions, express and implied, including warranties or
90 | conditions of title and non-infringement, and implied warranties or
91 | conditions of merchantability and fitness for a particular purpose;
92 |
93 | ii) effectively excludes on behalf of all Contributors all liability
94 | for damages, including direct, indirect, special, incidental and
95 | consequential damages, such as lost profits;
96 |
97 | iii) states that any provisions which differ from this Agreement are
98 | offered by that Contributor alone and not by any other party; and
99 |
100 | iv) states that source code for the Program is available from such
101 | Contributor, and informs licensees how to obtain it in a reasonable
102 | manner on or through a medium customarily used for software exchange.
103 |
104 | When the Program is made available in source code form:
105 |
106 | a) it must be made available under this Agreement; and
107 |
108 | b) a copy of this Agreement must be included with each copy of the Program.
109 |
110 | Contributors may not remove or alter any copyright notices contained
111 | within the Program.
112 |
113 | Each Contributor must identify itself as the originator of its
114 | Contribution, if any, in a manner that reasonably allows subsequent
115 | Recipients to identify the originator of the Contribution.
116 |
117 | 4. COMMERCIAL DISTRIBUTION
118 |
119 | Commercial distributors of software may accept certain
120 | responsibilities with respect to end users, business partners and the
121 | like. While this license is intended to facilitate the commercial use
122 | of the Program, the Contributor who includes the Program in a
123 | commercial product offering should do so in a manner which does not
124 | create potential liability for other Contributors. Therefore, if a
125 | Contributor includes the Program in a commercial product offering,
126 | such Contributor ("Commercial Contributor") hereby agrees to defend
127 | and indemnify every other Contributor ("Indemnified Contributor")
128 | against any losses, damages and costs (collectively "Losses") arising
129 | from claims, lawsuits and other legal actions brought by a third party
130 | against the Indemnified Contributor to the extent caused by the acts
131 | or omissions of such Commercial Contributor in connection with its
132 | distribution of the Program in a commercial product offering. The
133 | obligations in this section do not apply to any claims or Losses
134 | relating to any actual or alleged intellectual property
135 | infringement. In order to qualify, an Indemnified Contributor must: a)
136 | promptly notify the Commercial Contributor in writing of such claim,
137 | and b) allow the Commercial Contributor tocontrol, and cooperate with
138 | the Commercial Contributor in, the defense and any related settlement
139 | negotiations. The Indemnified Contributor may participate in any such
140 | claim at its own expense.
141 |
142 | For example, a Contributor might include the Program in a commercial
143 | product offering, Product X. That Contributor is then a Commercial
144 | Contributor. If that Commercial Contributor then makes performance
145 | claims, or offers warranties related to Product X, those performance
146 | claims and warranties are such Commercial Contributor's responsibility
147 | alone. Under this section, the Commercial Contributor would have to
148 | defend claims against the other Contributors related to those
149 | performance claims and warranties, and if a court requires any other
150 | Contributor to pay any damages as a result, the Commercial Contributor
151 | must pay those damages.
152 |
153 | 5. NO WARRANTY
154 |
155 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, THE PROGRAM IS
156 | PROVIDED ON AN "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
157 | KIND, EITHER EXPRESS OR IMPLIED INCLUDING, WITHOUT LIMITATION, ANY
158 | WARRANTIES OR CONDITIONS OF TITLE, NON-INFRINGEMENT, MERCHANTABILITY
159 | OR FITNESS FOR A PARTICULAR PURPOSE. Each Recipient is solely
160 | responsible for determining the appropriateness of using and
161 | distributing the Program and assumes all risks associated with its
162 | exercise of rights under this Agreement , including but not limited to
163 | the risks and costs of program errors, compliance with applicable
164 | laws, damage to or loss of data, programs or equipment, and
165 | unavailability or interruption of operations.
166 |
167 | 6. DISCLAIMER OF LIABILITY
168 |
169 | EXCEPT AS EXPRESSLY SET FORTH IN THIS AGREEMENT, NEITHER RECIPIENT NOR
170 | ANY CONTRIBUTORS SHALL HAVE ANY LIABILITY FOR ANY DIRECT, INDIRECT,
171 | INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING
172 | WITHOUT LIMITATION LOST PROFITS), HOWEVER CAUSED AND ON ANY THEORY OF
173 | LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
174 | NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OR
175 | DISTRIBUTION OF THE PROGRAM OR THE EXERCISE OF ANY RIGHTS GRANTED
176 | HEREUNDER, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGES.
177 |
178 | 7. GENERAL
179 |
180 | If any provision of this Agreement is invalid or unenforceable under
181 | applicable law, it shall not affect the validity or enforceability of
182 | the remainder of the terms of this Agreement, and without further
183 | action by the parties hereto, such provision shall be reformed to the
184 | minimum extent necessary to make such provision valid and enforceable.
185 |
186 | If Recipient institutes patent litigation against any entity
187 | (including a cross-claim or counterclaim in a lawsuit) alleging that
188 | the Program itself (excluding combinations of the Program with other
189 | software or hardware) infringes such Recipient's patent(s), then such
190 | Recipient's rights granted under Section 2(b) shall terminate as of
191 | the date such litigation is filed.
192 |
193 | All Recipient's rights under this Agreement shall terminate if it
194 | fails to comply with any of the material terms or conditions of this
195 | Agreement and does not cure such failure in a reasonable period of
196 | time after becoming aware of such noncompliance. If all Recipient's
197 | rights under this Agreement terminate, Recipient agrees to cease use
198 | and distribution of the Program as soon as reasonably
199 | practicable. However, Recipient's obligations under this Agreement and
200 | any licenses granted by Recipient relating to the Program shall
201 | continue and survive.
202 |
203 | Everyone is permitted to copy and distribute copies of this Agreement,
204 | but in order to avoid inconsistency the Agreement is copyrighted and
205 | may only be modified in the following manner. The Agreement Steward
206 | reserves the right to publish new versions (including revisions) of
207 | this Agreement from time to time. No one other than the Agreement
208 | Steward has the right to modify this Agreement. The Eclipse Foundation
209 | is the initial Agreement Steward. The Eclipse Foundation may assign
210 | the responsibility to serve as the Agreement Steward to a suitable
211 | separate entity. Each new version of the Agreement will be given a
212 | distinguishing version number. The Program (including Contributions)
213 | may always be distributed subject to the version of the Agreement
214 | under which it was received. In addition, after a new version of the
215 | Agreement is published, Contributor may elect to distribute the
216 | Program (including its Contributions) under the new version. Except as
217 | expressly stated in Sections 2(a) and 2(b) above, Recipient receives
218 | no rights or licenses to the intellectual property of any Contributor
219 | under this Agreement, whether expressly, by implication, estoppel or
220 | otherwise. All rights in the Program not expressly granted under this
221 | Agreement are reserved.
222 |
223 | This Agreement is governed by the laws of the State of Washington and
224 | the intellectual property laws of the United States of America. No
225 | party to this Agreement will bring a legal action under this Agreement
226 | more than one year after the cause of action arose. Each party waives
227 | its rights to a jury trial in any resulting litigation.
228 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Stencil
2 |
3 | A fast, compliant implementation of [Mustache](http://mustache.github.com)
4 | in Clojure.
5 |
6 | ## Introduction
7 |
8 | Stencil is a complete implementation of the
9 | [Mustache spec](http://github.com/mustache/spec), including the optional
10 | lambdas.
11 |
12 | The unit tests for Stencil will automatically pull down the spec
13 | files using git and run the tests against the current implementation (If you
14 | want to do this yourself, you can clone the repo and type `lein test`).
15 | Currently, all spec tests are passing.
16 |
17 | To learn about the language itself, you should read the language
18 | [documentation](http://mustache.github.com). The rest of this document will
19 | focus on the API that Stencil provides.
20 |
21 | Like Mustache itself, the interface is very simple, consisting of two main
22 | functions that will probably do most of what you want.
23 |
24 | (use 'stencil.core)
25 | (render-string "Hi there, {{name}}."
26 | {:name "Donald"})
27 | "Hi there, Donald."
28 |
29 | The easiest way to render a small template is using the function
30 | `render-string`, which takes two arguments, a string containing the text of
31 | the Mustache template and a map of the values referenced in the template.
32 |
33 | The keys of the value map can be either keywords or strings; if a
34 | keyword and string of the same name are present, the keyword is
35 | preferred. (Why support both? Keywords are more convenient to use in
36 | Clojure, but not all valid Mustache keys can be made into
37 | keywords. Rather than force strings, Stencil lets you use whichever
38 | will work better for you).
39 |
40 | (render-string "Hi there, {{name}}."
41 | {"name" "Dick" :name "Donald"})
42 | "Hi there, Donald."
43 |
44 | For a larger template, holding onto it and passing it in as a string is
45 | neither the most convenient nor the fastest option. Most commonly, Mustache
46 | templates are placed into their own files, ending in ".mustache", and put on
47 | the app's classpath somewhere. In this case, the `render-file` function can
48 | be used to open the file by its name and render it.
49 |
50 | (render-file "hithere"
51 | {:name "Donald"})
52 | "Hi there, Donald."
53 |
54 | The `render-file` function, given "hithere" as its first argument, will look
55 | in the classpath for "hithere.mustache". If that is not found, it looks for
56 | just the literal string itself, in this case "hithere". Remember that a
57 | file-separating slash is perfectly fine to pull a file out of a subdirectory.
58 |
59 | An important advantage that `render-file` has over `render-string` is that
60 | the former will cache the results of parsing the file, and reuse the parsed
61 | AST on subsequent renders, greatly improving the speed.
62 |
63 | ## Lower Level APIs
64 |
65 | You can also manage things at a much lower level, if you prefer. In the
66 | `stencil.loader` namespace are functions that Stencil itself uses the load
67 | and cache templates. In particular, the function `load` will take a template
68 | name and return the parsed AST out of cache if possible, and if not, it will
69 | load and parse it. The AST returned from `load` can then be rendered with
70 | the function `render`.
71 |
72 | (use 'stencil.loader)
73 | (render (load "hithere") {:name "Donald"})
74 | "Hi there, Donald."
75 |
76 | At an even lower level, you can manually generate the AST used in rendering
77 | using the function `parse` from the `stencil.parser` namespace. Of course,
78 | doing it this way will bypass the cache entirely, but it's there if you want
79 | it.
80 |
81 | ### Manual Cache Management
82 |
83 | Stencil uses [core.cache](https://github.com/clojure/core.cache) for
84 | caching. By default, Stencil uses a simple LRU cache. This is a pretty
85 | good cache to use in deployed code, where the set of templates being
86 | rendered is probably not going to change during runtime. However, you
87 | can control the type of cache used by Stencil to get the most benefit
88 | out of your specific code's usage patterns. You can set the cache
89 | manually using the function `set-cache` from the `stencil.loader`
90 | namespace; pass it some object that implements the `CacheProtocol`
91 | protocol from core.cache. In particular, during development, you might
92 | want to use a TTL cache with a very low TTL parameter, so that
93 | templates are reloaded as soon as you modify them. For example:
94 |
95 | (stencil.loader/set-cache (clojure.core.cache/ttl-cache-factory {} :ttl 0))
96 |
97 | You can also work at an even lower-level, manually caching templates using the
98 | `cache` function and the functions related to accessing the cache, then
99 | calling `render` yourself. You should read the source for a better idea of
100 | how to do that.
101 |
102 | #### Core.Cache Optional Mode (Experts only!)
103 |
104 | You can also run Stencil without the core.cache dependency present. If
105 | you don't have a really good reason for doing this, you almost
106 | certainly don't want to do it! It's not a great idea, and it doesn't
107 | provide any performance improvements or other benefits. It's actually
108 | all drawbacks and degradations. Nonetheless, there are unlikely
109 | scenarios where you might need to use Stencil this way to get by.
110 |
111 | If you still think this is for you, you need to call
112 | `stencil.loader/set-cache` with a "cache-like object" before you
113 | attempt to use any Stencil functions, or you will get an error on any
114 | use attempts. A plain map will work. Be aware, though, that if your
115 | cache-like object is not actually a cache (ie, doesn't evict entries
116 | once it reaches a size threshold of some sort), then it's quite
117 | possible that this object will simply grow larger and larger in memory
118 | over time without end, depending on how your code uses templates. Some
119 | apps could get by in this situation (a command line app that runs once
120 | and exits immediately, for example), while others might not.
121 |
122 | ### Manual Template Management
123 |
124 | Sometimes it can be useful to refer to a template by name, even though that
125 | template is not available as a file on the classpath. In that case, you can
126 | register the template's source with Stencil, and later when you refer to that
127 | template by its name, Stencil will check first to see if it is one that you
128 | have manually registered, before checking the filesystem for it.
129 |
130 | (use 'stencil.loader)
131 | (register-template "hithere" "Hi there, {{name}}.")
132 | (render-file "hithere" {:name "Donald"})
133 | "Hi there, Donald."
134 |
135 | ## Performance
136 |
137 | Performance isn't the most important thing in a template language, but I've
138 | tried to make Stencil as fast as possible. In
139 | [basic tests](http://github.com/davidsantiago/mustachequerade), it
140 | appears to be pretty fast. Of course, the actual performance of any given
141 | template is dictated by many factors, especially the size of the template,
142 | the amount and type of data it is given, and what types of operations are
143 | performed by the template.
144 |
145 | In particular, the Mustache spec specifies that the output of lambda tags
146 | should not be cached, and so Stencil does not. Keep that in mind if you decide
147 | to use them in your templates.
148 |
149 | I'd like to thank YourKit for helping me keep Stencil fast.
150 |
151 | YourKit is kindly supporting open source projects with its full-featured Java Profiler.
152 | YourKit, LLC is the creator of innovative and intelligent tools for profiling
153 | Java and .NET applications. Take a look at YourKit's leading software products:
154 | YourKit Java Profiler and
155 | YourKit .NET Profiler.
156 |
157 | ## Obtaining
158 |
159 | Simply add
160 |
161 | [stencil "0.5.0"]
162 |
163 | to the `:dependencies` key of your project.clj.
164 |
165 | ## Bugs and Missing Features
166 |
167 | I don't currently know of any bugs or issues with the software, but there
168 | probably are some. If you run into anything, please let me know so I can fix
169 | it as soon as possible.
170 |
171 | ## Recently
172 |
173 | * Released version 0.5.0.
174 | - Removed the dependency on slingshot, in favor of Clojure's built-in
175 | ex-info. ex-info was added in Clojure 1.4, so Stencil versions higher
176 | than 0.5.0 will require Clojure 1.4 or later. Thanks to
177 | [Ryan Wilson](https://github.com/rwilson).
178 |
179 | * Released version 0.4.0.
180 | - Lambdas that have `:stencil/pass-render` true in their metadata
181 | will be called with the render function as an explicit arg, in
182 | addition to the current context. This allows the lambda to have
183 | control of whether and when to pass the lambda's output through
184 | the full stencil rendering process. Careful use of this feature
185 | can enable performance improvements, but use with caution because
186 | it allows deviations from the usual rendering process. Thanks to
187 | [Max Penet](https://github.com/mpenet).
188 |
189 | * Released version 0.3.5.
190 | - Fixes a bug in the code that handles running without core.cache.
191 |
192 | * Released version 0.3.4.
193 | - Fixed output for boolean interpolations.
194 |
195 | * Released version 0.3.3.
196 | - It's now possible to run Stencil without core.cache. It's still probably not a good idea (see above).
197 |
198 | * Released version 0.3.2.
199 | - Fixed a problem causing an infinite loop when attempting to parse a malformed set-delimiter tag.
200 | - Updated code to work with Clojure 1.5. (Thanks to @bmabey).
201 |
202 | * Released version 0.3.1.
203 | - Update version of core.cache to one that fixes bugs.
204 |
205 | * Released version 0.3.0.
206 | - Performance improvements (Thanks YourKit!).
207 | - Keywords are now preferred over strings in contexts.
208 | - Change to using core.cache for more flexible and easier to use
209 | caching. API is slightly different, but only if you were managing
210 | cache policy manually (see above).
211 | - Lambdas that have `:stencil/pass-context` true in their metadata will be called with
212 | the current context as their second argument.
213 |
214 | ### Previously...
215 |
216 | * Released version 0.2.0. Supports Clojure 1.3 and now builds with lein instead of cake. Now uses Slingshot for exceptions instead of clojure.contrib.condition; should not result in any code changes unless you are examining exceptions.
217 |
218 | * Released version 0.1.2, fixing bug in the handling of missing partial templates and adding functions to remove entries from the dynamic template store and cache.
219 |
220 | * Released version 0.1.1, fixing bug in the handling of inverted sections.
221 |
222 | ## License
223 |
224 | Eclipse Public License
225 |
--------------------------------------------------------------------------------
/project.clj:
--------------------------------------------------------------------------------
1 | (defproject stencil "0.5.0"
2 | :description "Mustache in Clojure"
3 | :url "https://github.com/davidsantiago/stencil"
4 | :dependencies [[org.clojure/clojure "1.6.0"]
5 | [scout "0.1.0"]
6 | [quoin "0.1.2"]
7 | [org.clojure/core.cache "0.6.3"]]
8 | :profiles {:dev {:dependencies [[org.clojure/data.json "0.1.2"]]}
9 | :cacheless-test
10 | {:dependencies ^:replace [[org.clojure/clojure "1.4.0"]
11 | [scout "0.1.0"]
12 | [quoin "0.1.2"]
13 | [org.clojure/data.json "0.1.2"]]}
14 | :clj1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]}
15 | :clj1.5 {:dependencies [[org.clojure/clojure "1.5.1"]]}}
16 | :aliases {"all" ["with-profile" "dev:dev,clj1.4:dev,clj1.5"]
17 | "test-no-cache" ["with-profile" "+cacheless-test" "test"]}
18 | :repositories {"sonatype" {:url "http://oss.sonatype.org/content/repositories/releases"
19 | :snapshots false
20 | :releases {:checksum :fail :update :always}}
21 | "sonatype-snapshots" {:url "http://oss.sonatype.org/content/repositories/snapshots"
22 | :snapshots true
23 | :releases {:checksum :fail :update :always}}}
24 | :test-paths ["test/" "target/test/spec"])
25 |
--------------------------------------------------------------------------------
/src/stencil/ast.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.ast
2 | (:refer-clojure :exclude [partial])
3 | (:require [clojure.zip :as zip]
4 | [clojure.string :as string])
5 | (:use stencil.utils))
6 |
7 | ;;
8 | ;; Data structures
9 | ;;
10 |
11 | (defprotocol ASTZipper
12 | (branch? [this] "Returns true if this node can possibly have children,
13 | whether it currently does or not.")
14 | (children [this] "When called on a branch node, returns its children.")
15 | (make-node [this children] "Given a node (potentially with existing children)
16 | and a seq of children that should totally replace
17 | the existing children, make the new node."))
18 |
19 | (defprotocol ASTNode
20 | (render [this ^StringBuilder sb context-stack]
21 | "Given a StringBuilder and the current context-stack, render this node to
22 | the result string in the StringBuilder."))
23 |
24 | ;; Section and InvertedSection need to keep track of the raw source code of
25 | ;; their contents, since lambdas need access to that. The attrs field lets them
26 | ;; keep track of that, with fields
27 | ;; - content-start : position in source string of content start
28 | ;; - content-end : position in source string of end of content
29 | ;; - content : string holding the raw content
30 | (defrecord Section [name attrs contents]
31 | ASTZipper
32 | (branch? [this] true)
33 | (children [this] contents)
34 | (make-node [this children] (Section. name attrs (vec children))))
35 | ;; ASTNode IS implemented, but not here. To avoid Clojure's circular
36 | ;; dependency inadequacies, we have to implement ASTNode at the top of
37 | ;; core.clj.
38 | (defn section [name attrs contents]
39 | (Section. name attrs contents))
40 |
41 | (defrecord InvertedSection [name attrs contents]
42 | ASTZipper
43 | (branch? [this] true)
44 | (children [this] contents)
45 | (make-node [this children] (InvertedSection. name attrs (vec children)))
46 | ASTNode
47 | (render [this sb context-stack]
48 | ;; Only render the section if the value is not present, false, or
49 | ;; an empty list.
50 | (let [ctx (first context-stack)
51 | ctx-val (context-get context-stack name)]
52 | ;; Per the spec, a function is truthy, so we should not render.
53 | (if (and (not (instance? clojure.lang.Fn ctx-val))
54 | (or (not ctx-val)
55 | (and (sequential? ctx-val)
56 | (empty? ctx-val))))
57 | (render contents sb context-stack)))))
58 | (defn inverted-section [name attrs contents]
59 | (InvertedSection. name attrs contents))
60 |
61 | ;; Partials can be obligated to indent the entire contents of the sub-template's
62 | ;; output, so we hold on to any padding here and apply it after the sub-
63 | ;; template renders.
64 | (defrecord Partial [name padding]
65 | ASTZipper
66 | (branch? [this] false)
67 | (children [this] nil)
68 | (make-node [this children] nil))
69 | ;; ASTNode IS implemented, but not here. To avoid Clojure's circular
70 | ;; dependency inadequacies, we have to implement ASTNode at the end of
71 | ;; loader.clj.
72 | (defn partial [name padding] (Partial. name padding))
73 |
74 | (defrecord EscapedVariable [name]
75 | ASTZipper
76 | (branch? [this] false)
77 | (children [this] nil)
78 | (make-node [this children] nil))
79 | ;; ASTNode IS implemented, but not here. To avoid Clojure's circular
80 | ;; dependency inadequacies, we have to implement ASTNode at the top of
81 | ;; core.clj.
82 | (defn escaped-variable [name] (EscapedVariable. name))
83 |
84 | (defrecord UnescapedVariable [name]
85 | ASTZipper
86 | (branch? [this] false)
87 | (children [this] nil)
88 | (make-node [this children] nil))
89 | ;; ASTNode IS implemented, but not here. To avoid Clojure's circular
90 | ;; dependency inadequacies, we have to implement ASTNode at the top of
91 | ;; core.clj.
92 | (defn unescaped-variable [name] (UnescapedVariable. name))
93 |
94 | (extend-protocol ASTZipper
95 | ;; Want to be able to just stick Strings in the AST.
96 | java.lang.String
97 | (branch? [this] false)
98 | (children [this] nil)
99 | (make-node [this children] nil)
100 | ;; Want to be able to use vectors to create lists in the AST.
101 | clojure.lang.PersistentVector
102 | (branch? [this] true)
103 | (children [this] this)
104 | (make-node [this children] (vec children)))
105 |
106 | (extend-protocol ASTNode
107 | java.lang.String
108 | (render [this ^StringBuilder sb context-stack] (.append sb this))
109 | clojure.lang.PersistentVector
110 | (render [this sb context-stack]
111 | (dotimes [i (count this)]
112 | (render (nth this i) sb context-stack))))
113 |
114 | ;; Implement a Zipper over ASTZippers.
115 |
116 | (defn ast-zip
117 | "Returns a zipper for ASTZippers, given a root ASTZipper."
118 | [root]
119 | (zip/zipper branch?
120 | children
121 | make-node
122 | root))
--------------------------------------------------------------------------------
/src/stencil/core.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.core
2 | (:require [clojure.string :as string]
3 | [stencil.loader :as loader])
4 | (:use [stencil.parser :exclude [partial]]
5 | [stencil.ast :rename {render node-render
6 | partial node-partial}]
7 | [quoin.text :as qtext]
8 | [clojure.java.io :only [resource]]
9 | stencil.utils))
10 |
11 | (declare render)
12 | (declare render-string)
13 |
14 | ;; This is stupid. Clojure can't do circular dependencies between namespaces
15 | ;; at all. Some types need access to render/render-string to do what they are
16 | ;; supposed to do. But render-string depends on parser, parser depends on ast,
17 | ;; and to implement, ast would have to depend on core. So instead of doing what
18 | ;; Clojure wants you to do, and jam it all into one huge file, we're going to
19 | ;; just implement ASTNode for some of the ASTNode types here.
20 |
21 | (extend-protocol ASTNode
22 | stencil.ast.Section
23 | (render [this ^StringBuilder sb context-stack]
24 | (let [ctx-val (context-get context-stack (:name this))]
25 | (cond (or (not ctx-val) ;; "False" or the empty list -> do nothing.
26 | (and (sequential? ctx-val)
27 | (empty? ctx-val)))
28 | nil
29 | ;; Non-empty list -> Display content once for each item in list.
30 | (sequential? ctx-val)
31 | (doseq [val ctx-val]
32 | ;; For each render, push the value to top of context stack.
33 | (node-render (:contents this) sb (conj context-stack val)))
34 | ;; Callable value -> Invoke it with the literal block of src text.
35 | (instance? clojure.lang.Fn ctx-val)
36 | (let [current-context (first context-stack)]
37 | ;; We have to manually parse because the spec says lambdas in
38 | ;; sections get parsed with the current parser delimiters.
39 | (.append sb (call-lambda ctx-val
40 | current-context
41 | (fn [tmpl ctx]
42 | (render (parse tmpl
43 | (select-keys (:attrs this)
44 | [:tag-open :tag-close]))
45 | ctx))
46 | (:content (:attrs this)))))
47 | ;; Non-false non-list value -> Display content once.
48 | :else
49 | (node-render (:contents this) sb (conj context-stack ctx-val)))))
50 | stencil.ast.EscapedVariable
51 | (render [this ^StringBuilder sb context-stack]
52 | (let [value (context-get context-stack (:name this))]
53 | ;; Need to explicitly check for nilness so we render boolean false.
54 | (if (not (nil? value))
55 | (if (instance? clojure.lang.Fn value)
56 | (.append sb (qtext/html-escape
57 | (call-lambda value
58 | (first context-stack)
59 | render-string)))
60 | ;; Otherwise, just append its html-escaped value by default.
61 | (.append sb (qtext/html-escape (str value)))))))
62 | stencil.ast.UnescapedVariable
63 | (render [this ^StringBuilder sb context-stack]
64 | (let [value (context-get context-stack (:name this))]
65 | ;; Need to explicitly check for nilness so we render boolean false.
66 | (if (not (nil? value))
67 | (if (instance? clojure.lang.Fn value)
68 | (.append sb (call-lambda value
69 | (first context-stack)
70 | render-string))
71 | ;; Otherwise, just append its value.
72 | (.append sb value))))))
73 |
74 | (defn render
75 | "Given a parsed template (output of load or parse) and map of args,
76 | renders the template."
77 | [template data-map]
78 | (let [sb (StringBuilder.)
79 | context-stack (conj '() data-map)]
80 | (node-render template sb context-stack)
81 | (.toString sb)))
82 |
83 | (defn render-file
84 | "Given a template name (string) and map of args, loads and renders the named
85 | template."
86 | [template-name data-map]
87 | (render (loader/load template-name) data-map))
88 |
89 | (defn render-string
90 | "Renders a given string containing the source of a template and a map
91 | of args."
92 | [template-src data-map]
93 | (render (parse template-src) data-map))
94 |
--------------------------------------------------------------------------------
/src/stencil/loader.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.loader
2 | (:refer-clojure :exclude [load])
3 | (:use [clojure.java.io :only [resource]]
4 | [stencil.parser :exclude [partial]]
5 | [stencil.ast :exclude [partial]]
6 | [quoin.text :as qtext]
7 | stencil.utils)
8 | (:import [java.io FileNotFoundException]))
9 |
10 | ;;
11 | ;; Support for operation without core.cache. We can't just
12 | ;; error out when core.cache isn't present, so we default to
13 | ;; an object that prints an informative error whenever it is
14 | ;; used.
15 | ;;
16 |
17 | (def ^{:private true} no-core-cache-msg
18 | "Could not load core.cache. To use Stencil without core.cache, you must first use set-cache to provide a map(-like object) to use as a cache, and consult the readme to make sure you fully understand the ramifications of running Stencil this way.")
19 |
20 | (defn- no-core-cache-ex []
21 | (Exception. no-core-cache-msg))
22 |
23 | (deftype CoreCacheUnavailableStub_SeeReadme []
24 | clojure.lang.ILookup
25 | (valAt [this key] (throw (no-core-cache-ex)))
26 | (valAt [this key notFound] (throw (no-core-cache-ex)))
27 | clojure.lang.IPersistentCollection
28 | (count [this] (throw (no-core-cache-ex)))
29 | (cons [this o] (throw (no-core-cache-ex)))
30 | (empty [this] (throw (no-core-cache-ex)))
31 | (equiv [this o] (throw (no-core-cache-ex)))
32 | clojure.lang.Seqable
33 | (seq [this] (throw (no-core-cache-ex)))
34 | clojure.lang.Associative
35 | (containsKey [this key] (throw (no-core-cache-ex)))
36 | (entryAt [this key] (throw (no-core-cache-ex)))
37 | (assoc [this key val] (throw (no-core-cache-ex))))
38 |
39 | ;; The dynamic template store just maps a template name to its source code.
40 | (def ^{:private true} dynamic-template-store (atom {}))
41 |
42 | ;; The parsed template cache maps a template name to its parsed versions.
43 | (def ^{:private true} parsed-template-cache
44 | (atom (try
45 | (require 'clojure.core.cache)
46 | ((resolve 'clojure.core.cache/lru-cache-factory) {})
47 | (catch ExceptionInInitializerError _
48 | (CoreCacheUnavailableStub_SeeReadme.))
49 | (catch FileNotFoundException _
50 | (CoreCacheUnavailableStub_SeeReadme.)))))
51 |
52 |
53 | ;; Holds a cache entry
54 | (defrecord TemplateCacheEntry [src ;; The source code of the template
55 | parsed]) ;; Parsed ASTNode structure.
56 |
57 | (defn template-cache-entry
58 | "Given template source and parsed ASTNodes, creates a cache entry.
59 | If only source is given, parse tree is calculated automatically."
60 | ([src]
61 | (template-cache-entry src (parse src)))
62 | ([src parsed]
63 | (TemplateCacheEntry. src parsed)))
64 |
65 | (defn set-cache
66 | "Takes a core.cache cache as the single argument and resets the cache to that
67 | cache. In particular, the cache will now follow the cache policy of the given
68 | cache. Also note that using this function has the effect of flushing
69 | the template cache."
70 | [cache]
71 | (reset! parsed-template-cache cache))
72 |
73 | (declare invalidate-cache-entry invalidate-cache)
74 |
75 | (defn register-template
76 | "Allows one to register a template in the dynamic template store. Give the
77 | template a name and provide its content as a string."
78 | [template-name content-string]
79 | (swap! dynamic-template-store assoc template-name content-string)
80 | (invalidate-cache-entry template-name))
81 |
82 | (defn unregister-template
83 | "Removes the template with the given name from the dynamic template store."
84 | [template-name]
85 | (swap! dynamic-template-store dissoc template-name)
86 | (invalidate-cache-entry template-name))
87 |
88 | (defn unregister-all-templates
89 | "Clears the dynamic template store. Also necessarily clears the template
90 | cache."
91 | []
92 | (reset! dynamic-template-store {})
93 | (invalidate-cache))
94 |
95 | (defn find-file
96 | "Given a name of a mustache template, attempts to find the corresponding
97 | file. Returns a URL if found, nil if not. First tries to find
98 | filename.mustache on the classpath. Failing that, looks for filename on the
99 | classpath. Note that you can use slashes as path separators to find a file
100 | in a subdirectory."
101 | [template-name]
102 | (if-let [file-url (resource (str template-name ".mustache"))]
103 | file-url
104 | (if-let [file-url (resource template-name)]
105 | file-url)))
106 |
107 | ;;
108 | ;; Cache mechanics
109 | ;;
110 | ;; The template cache has two string keys, the template name, and a
111 | ;; secondary key that is called the variant. A variant of a template
112 | ;; is created when a partial has to change the whitespace of the
113 | ;; template (or when a user wants it), and the key is a string unless
114 | ;; it is a special value for internal use; the default variant is
115 | ;; set/fetched with :default as the variant key. Invalidating an entry
116 | ;; invalidates all variants. The variants do NOT work with "fuzzy" map
117 | ;; logic for getting/setting, they must be strings.
118 | ;;
119 |
120 | (defn cache
121 | "Given a template name (string), variant key (string), template source
122 | (string), and optionally a parsed AST, and stores that entry in the
123 | template cache. Returns the parsed template."
124 | ([template-name template-variant template-src]
125 | (cache template-name template-variant template-src (parse template-src)))
126 | ([template-name template-variant template-src parsed-template]
127 | (swap! parsed-template-cache
128 | assoc-in [template-name template-variant]
129 | (template-cache-entry template-src
130 | parsed-template))
131 | parsed-template))
132 |
133 | (defn invalidate-cache-entry
134 | "Given a template name, invalidates the cache entry for that name, if there
135 | is one."
136 | [template-name]
137 | (swap! parsed-template-cache dissoc template-name))
138 |
139 | (defn invalidate-cache
140 | "Clears all entries out of the cache."
141 | []
142 | ;; Need to use empty to make sure we get a new cache of the same type.
143 | (reset! parsed-template-cache (empty @parsed-template-cache)))
144 |
145 | (defn cache-get
146 | "Given a template name, attempts to fetch the template with that
147 | name from the template cache. If it is not in the cache, nil will
148 | be returned. Single argument version gets the default variant."
149 | ([template-name]
150 | (cache-get template-name :default))
151 | ([template-name template-variant]
152 | (get-in @parsed-template-cache [template-name template-variant])))
153 |
154 |
155 | ;;
156 | ;; Loader API
157 | ;;
158 |
159 | (defn load
160 | "Attempts to load a mustache template by name. When given something like
161 | \"myfile\", it attempts to load the mustache template called myfile. First it
162 | will look in the dynamic template store, then look in the classpath for
163 | a file called myfile.mustache or just myfile.
164 |
165 | With additional arguments template-variant and variant-fn, supports the load
166 | and caching of template variants. The template-variant arg is a variant key,
167 | while the variant-fn arg is a single argument function that will be called
168 | with the template source as argument before it is cached or returned."
169 | ([template-name]
170 | (load template-name nil identity))
171 | ([template-name template-variant variant-fn]
172 | (if-let [cached (cache-get template-name template-variant)]
173 | (:parsed cached)
174 | ;; It wasn't cached, so we have to load it. Try dynamic store first.
175 | (if-let [dynamic-src (get @dynamic-template-store template-name)]
176 | ;; If found, parse and cache it, then return it.
177 | (cache template-name template-variant (variant-fn dynamic-src))
178 | ;; Otherwise, try to load it from disk.
179 | (if-let [file-url (find-file template-name)]
180 | (let [template-src (slurp file-url)]
181 | (cache template-name
182 | template-variant
183 | (variant-fn template-src))))))))
184 |
185 | ;; This is stupid. Clojure can't do circular dependencies between namespaces
186 | ;; at all. Partials need access to load to do what they are supposed to do.
187 | ;; But loader depends on parser, parser depends on ast, and to implement, ast
188 | ;; would have to depend on loader. So instead of doing what Clojure wants you
189 | ;; to do, and jam it all into one huge file, we're going to just implement
190 | ;; ASTNode for Partial here.
191 | (extend-protocol ASTNode
192 | stencil.ast.Partial
193 | (render [this sb context-stack]
194 | (let [padding (:padding this)
195 | template (if padding
196 | (load (:name this)
197 | padding
198 | #(qtext/indent-string % padding))
199 | (load (:name this)))]
200 | (when template
201 | (render template sb context-stack)))))
202 |
--------------------------------------------------------------------------------
/src/stencil/parser.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.parser
2 | (:refer-clojure :exclude [partial])
3 | (:require [scout.core :as scan]
4 | [clojure.zip :as zip]
5 | [clojure.string :as string])
6 | (:import java.util.regex.Pattern
7 | scout.core.Scanner)
8 | (:use [stencil ast re-utils utils]
9 | clojure.pprint))
10 |
11 | ;;
12 | ;; Settings and defaults.
13 | ;;
14 |
15 | ;; These tags, when used standalone (only content on a line, excluding
16 | ;; whitespace before the tag), will cause all whitespace to be removed from
17 | ;; the line.
18 | (def standalone-tag-sigils #{\# \^ \/ \< \> \= \!})
19 |
20 | ;; These tags will allow anything in their content.
21 | (def freeform-tag-sigils #{\! \=})
22 |
23 | (defn closing-sigil
24 | "Given a sigil (char), returns what its closing sigil could possibly be."
25 | [sigil]
26 | (if (= \{ sigil)
27 | \}
28 | sigil))
29 |
30 | (def valid-tag-content #"(\w|[?!/.-])*")
31 |
32 | (def parser-defaults {:tag-open "{{" :tag-close "}}"})
33 |
34 | ;; The main parser data structure. The only tricky bit is the output, which is
35 | ;; a zipper. The zipper is kept in a state where new things are added with
36 | ;; append-child. This means that the current loc in the zipper is a branch
37 | ;; vector, and the actual "next location" is enforced in the code through using
38 | ;; append-child, and down or up when necessary due to the creation of a section.
39 | ;; This makes it easier to think of sections as being a stack.
40 | (defrecord Parser [scanner ;; The current scanner state.
41 | output ;; Current state of the output (a zipper).
42 | state]) ;; Various options as the parser progresses.
43 |
44 | (defn parser
45 | ([scanner]
46 | (parser scanner (ast-zip [])))
47 | ([scanner output]
48 | (parser scanner output parser-defaults))
49 | ([scanner output state]
50 | (Parser. scanner output state)))
51 |
52 | (defn get-line-col-from-index
53 | "Given a string and an index into the string, returns which line of text
54 | the position is on. Specifically, returns an index containing a pair of
55 | numbers, the row and column."
56 | [s idx]
57 | (if (> idx (count s))
58 | (throw (java.lang.IndexOutOfBoundsException. (str "At index " idx))))
59 | (loop [lines 0
60 | last-line-start 0 ;; Index in string of the last line beginning seen.
61 | i 0]
62 | (cond (= i idx) ;; Reached the index, return the number of lines we saw.
63 | [(inc lines) (inc (- i last-line-start))] ;; Un-zero-index.
64 | (= "\n" (subs s i (+ 1 i)))
65 | (recur (inc lines) (inc i) (inc i))
66 | :else
67 | (recur lines last-line-start (inc i)))))
68 |
69 | (defn format-location
70 | "Given either a scanner or a string and index into the string, return a
71 | message describing the location by row and column."
72 | ([^Scanner sc]
73 | (format-location (:src sc) (scan/position sc)))
74 | ([s idx]
75 | (let [[line col] (get-line-col-from-index s idx)]
76 | (str "line " line ", column " col))))
77 |
78 | (defn write-string-to-output
79 | "Given a zipper and a string, adds the string to the zipper at the current
80 | cursor location (as zip/append-child would) and returns the new zipper. This
81 | function will collate adjacent strings and remove empty strings, so use it
82 | when adding strings to a parser's output."
83 | [zipper ^String s]
84 | (let [preceding-value (-> zipper zip/down zip/rightmost)]
85 | (cond (empty? s) ;; If the string is empty, just throw it away!
86 | zipper
87 | ;; Otherwise, if the value right before the one we are trying to add
88 | ;; is also a string, we should replace the existing value with the
89 | ;; concatenation of the two.
90 | (and preceding-value
91 | (string? (zip/node preceding-value)))
92 | (-> zipper zip/down zip/rightmost
93 | (zip/replace (str (zip/node preceding-value) s)) zip/up)
94 | ;; Otherwise, actually append it.
95 | :else
96 | (-> zipper (zip/append-child s)))))
97 |
98 | (defn tag-position?
99 | "Takes a scanner and returns true if it is currently in \"tag position.\"
100 | That is, if the only thing between it and the start of a tag is possibly some
101 | non-line-breaking whitespace padding."
102 | [^Scanner s parser-state]
103 | (let [tag-open-re (re-concat #"([ \t]*)?"
104 | (re-quote (:tag-open parser-state)))]
105 | ;; Return true if first expr makes progress.
106 | (not= (scan/position (scan/scan s tag-open-re))
107 | (scan/position s))))
108 |
109 | (defn parse-tag-name
110 | "This function takes a tag name (string) and parses it into a run-time data
111 | structure useful during rendering of the templates. Following the rules of
112 | mustache, it checks for a single \".\", which indicates the implicit
113 | iterator. If not, it splits it on periods, returning a list of
114 | the pieces. See interpolation.yml in the spec."
115 | [^String s]
116 | (if (= "." s)
117 | :implicit-top
118 | (doall (map keyword (string/split s #"\.")))))
119 |
120 | (defn parse-text
121 | "Given a parser that is not in tag position, reads text until it is and
122 | appends it to the output of the parser."
123 | [^Parser p]
124 | (let [scanner (:scanner p)
125 | state (:state p)
126 | ffwd-scanner (scan/skip-to-match-start
127 | scanner
128 | ;; (?m) is to turn on MULTILINE mode for the pattern. This
129 | ;; will make it so ^ matches embedded newlines and not
130 | ;; just the start of the input string.
131 | (re-concat #"(?m)(^[ \t]*)?"
132 | (re-quote (:tag-open state))))
133 | text (subs (:src scanner)
134 | (scan/position scanner)
135 | (scan/position ffwd-scanner))]
136 | (if (nil? (:match ffwd-scanner))
137 | ;; There was no match, so the remainder of input is plain text.
138 | ;; Jump scanner to end of input and add rest of text to output.
139 | (parser (scan/scanner (:src scanner) (count (:src scanner)))
140 | (write-string-to-output (:output p) (scan/remainder scanner))
141 | state)
142 | ;; Otherwise, add the text chunk we found.
143 | (parser ffwd-scanner
144 | (write-string-to-output (:output p) text)
145 | state))))
146 |
147 | ;; Grrr, I know this function is really long, but it's really simple. It's just
148 | ;; parsing along a tag, and keeping hold of the scanner state at various steps.
149 | ;; Then the logic at the bottom is fairly simple (modulo some logic for dealing
150 | ;; with standalone tags everywhere), and uses the saved scanner states or the
151 | ;; derived values. Whitespace rules cause a lot of complexity.
152 | (defn parse-tag
153 | "Given a parser that is in tag position, reads the next tag and appends it
154 | to the output of the parser with appropriate processing."
155 | [^Parser p]
156 | (let [{:keys [scanner output state]} p
157 | beginning-of-line? (scan/beginning-of-line? scanner)
158 | tag-position-scanner scanner ;; Save the original scanner, might be used
159 | ;; in closing tags to get source code.
160 | ;; Skip and save any leading whitespace.
161 | padding-scanner (scan/scan scanner
162 | #"([ \t]*)?")
163 | padding (second (scan/groups padding-scanner))
164 | tag-start-scanner (scan/scan padding-scanner
165 | (re-quote (:tag-open state)))
166 | ;; Identify the sigil (and then eat any whitespace).
167 | sigil-scanner (scan/scan tag-start-scanner
168 | #"#|\^|\/|=|!|<|>|&|\{")
169 | sigil (first (scan/matched sigil-scanner)) ;; first gets the char.
170 | sigil-scanner (scan/scan sigil-scanner #"\s*")
171 | ;; Scan the tag content, taking into account the content allowed by
172 | ;; this type of tag.
173 | tag-content-scanner (if (freeform-tag-sigils sigil)
174 | (scan/skip-to-match-start
175 | sigil-scanner
176 | (re-concat #"\s*"
177 | (re-quote (closing-sigil sigil)) "?"
178 | (re-quote (:tag-close state))))
179 | ;; Otherwise, restrict tag content.
180 | (scan/scan sigil-scanner
181 | valid-tag-content))
182 | tag-content (subs (:src scanner)
183 | (scan/position sigil-scanner)
184 | (scan/position tag-content-scanner))
185 | ;; Finish the tag: any trailing whitespace, closing sigils, and tag end.
186 | ;; Done separately so they can succeed/fail independently.
187 | tag-content-scanner (scan/scan (scan/scan tag-content-scanner #"\s*")
188 | (re-quote (closing-sigil sigil)))
189 | close-scanner (scan/scan tag-content-scanner
190 | (re-quote (:tag-close state)))
191 | ;; Check if the line end comes right after... if this is a "standalone"
192 | ;; tag, we should remove the padding and newline.
193 | trailing-newline-scanner (scan/scan close-scanner #"\r?\n|$")
194 | strip-whitespace? (and beginning-of-line?
195 | (standalone-tag-sigils sigil)
196 | (not (nil? (:match trailing-newline-scanner))))
197 | ;; Go ahead and add the padding to the current state now, if we should.
198 | p (if strip-whitespace?
199 | (parser trailing-newline-scanner ;; Which has moved past newline...
200 | output state)
201 | ;; Otherwise, need to add padding to output and leave parser with
202 | ;; a scanner that is looking at what came right after closing tag.
203 | (parser close-scanner
204 | (write-string-to-output output padding)
205 | state))
206 | {:keys [scanner output state]} p]
207 | ;; First, let's analyze the results and throw any errors necessary.
208 | (cond (empty? tag-content)
209 | (throw (ex-info (str "Illegal content in tag: " tag-content
210 | " at " (format-location tag-content-scanner))
211 | {:type :illegal-tag-content
212 | :tag-content tag-content
213 | :scanner tag-content-scanner}))
214 | (nil? (:match close-scanner))
215 | (throw (ex-info (str "Unclosed tag: " tag-content
216 | " at " (format-location close-scanner))
217 | {:type :unclosed-tag
218 | :tag-content tag-content
219 | :scanner close-scanner})))
220 | (case sigil
221 | (\{ \&) (parser scanner
222 | (zip/append-child output
223 | (unescaped-variable
224 | (parse-tag-name tag-content)))
225 | state)
226 | \# (parser scanner
227 | (-> output
228 | (zip/append-child
229 | (section (parse-tag-name tag-content)
230 | {:content-start
231 | ;; Need to respect whether to strip white-
232 | ;; space in the source.
233 | (scan/position (if strip-whitespace?
234 | trailing-newline-scanner
235 | close-scanner))
236 | ;; Lambdas in sections need to parse with
237 | ;; current delimiters.
238 | :tag-open (:tag-open state)
239 | :tag-close (:tag-close state)}
240 | []))
241 | zip/down zip/rightmost)
242 | state)
243 | \^ (parser scanner
244 | (-> output
245 | (zip/append-child
246 | (inverted-section (parse-tag-name tag-content)
247 | {:content-start
248 | (scan/position
249 | (if strip-whitespace?
250 | trailing-newline-scanner
251 | close-scanner))}
252 | []))
253 | zip/down zip/rightmost)
254 | state)
255 | \/ (let [top-section (zip/node output)] ;; Do consistency checks...
256 | (if (not= (:name top-section) (parse-tag-name tag-content))
257 | (throw (ex-info (str "Attempt to close section out of order: "
258 | tag-content
259 | " at "
260 | (format-location tag-content-scanner))
261 | {:type :mismatched-closing-tag
262 | :tag-content tag-content
263 | :scanner tag-content-scanner}))
264 | ;; Going to close it by moving up the zipper tree, but first
265 | ;; we need to store the source code between the tags so that
266 | ;; it can be used in a lambda.
267 | (let [content-start (:content-start (-> output
268 | zip/node
269 | :attrs))
270 | ;; Where the content ends depends on whether we are
271 | ;; stripping whitespace from the current tag.
272 | content-end (scan/position (if strip-whitespace?
273 | tag-position-scanner
274 | padding-scanner))
275 | content (subs (:src scanner) content-start content-end)]
276 | (parser scanner
277 | ;; We need to replace the current zip node with
278 | ;; one with the attrs added to its attrs field.
279 | (-> output
280 | (zip/replace
281 | (assoc (zip/node output)
282 | :attrs
283 | (merge (:attrs (zip/node output))
284 | {:content-end content-end
285 | :content content})))
286 | zip/up)
287 | state))))
288 | ;; Just ignore comments.
289 | \! p
290 | (\> \<) (parser scanner
291 | (-> output
292 | ;; A standalone partial instead holds onto its
293 | ;; padding and uses it to indent its sub-template.
294 | (zip/append-child (partial tag-content
295 | (if strip-whitespace?
296 | padding))))
297 | state)
298 | ;; Set delimiters only affect parser state.
299 | \= (let [[tag-open tag-close]
300 | (drop 1 (re-matches #"([\S|[^=]]+)\s+([\S|[^=]]+)"
301 | tag-content))]
302 | (if (or (nil? tag-open) (nil? tag-close))
303 | (throw (ex-info (str "Invalid set delimiters command: "
304 | tag-content
305 | " at "
306 | (format-location tag-content-scanner))
307 | {:type :invalid-set-delimiters-tag
308 | :tag-content tag-content
309 | :scanner tag-content-scanner}))
310 | (parser scanner
311 | output
312 | (assoc state :tag-open tag-open
313 | :tag-close tag-close))))
314 | ;; No sigil: it was an escaped variable reference.
315 | (parser scanner
316 | (zip/append-child output
317 | (escaped-variable (parse-tag-name
318 | tag-content)))
319 | state))))
320 |
321 | (defn parse
322 | ([template-string]
323 | (parse template-string parser-defaults))
324 | ([template-string parser-state]
325 | (loop [p (parser (scan/scanner template-string)
326 | (ast-zip [])
327 | parser-state)]
328 | (let [s (:scanner p)]
329 | (cond
330 | ;; If we are at the end of input, return the output.
331 | (scan/end? s)
332 | (let [output (:output p)]
333 | ;; If we can go up from the zipper's current loc, then there is an
334 | ;; unclosed tag, so raise an error.
335 | (if (zip/up output)
336 | (throw (ex-info (str "Unclosed section: "
337 | (second (zip/node output))
338 | " at " (format-location s))
339 | {:type :unclosed-tag
340 | :scanner s}))
341 | (zip/root output)))
342 | ;; If we are in tag-position, read a tag.
343 | (tag-position? s (:state p))
344 | (recur (parse-tag p))
345 | ;; Otherwise, we must have some text to read. Read until next line.
346 | :else
347 | (recur (parse-text p)))))))
348 |
--------------------------------------------------------------------------------
/src/stencil/re_utils.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.re-utils
2 | "Some utility functions to make working with regular expressions easier."
3 | (:import java.util.regex.Pattern))
4 |
5 | (defn re-concat
6 | "Concatenates its arguments into one regular expression
7 | (java.util.regex.Pattern). Args can be strings or java.util.regex.Pattern
8 | (what the #\"...\" reader macro creates). Or anything that responds to
9 | .toString, really."
10 | [& args]
11 | (re-pattern (apply str args)))
12 |
13 | (defn re-quote
14 | "Turns its argument into a regular expression that recognizes its literal
15 | content as a string, quoting for any RE control characters as needed."
16 | [s]
17 | (re-pattern (Pattern/quote (str s))))
--------------------------------------------------------------------------------
/src/stencil/utils.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.utils
2 | (:require [clojure.string :as str]
3 | [quoin.map-access :as map])
4 | (:import [java.io FileNotFoundException]))
5 |
6 | ;;
7 | ;; Context stack access logic
8 | ;;
9 | ;; find-containing-context and context-get are a significant portion of
10 | ;; execution time during rendering, so they are written in a less beautiful
11 | ;; way to make them go faster.
12 | ;;
13 |
14 | (defn find-containing-context
15 | "Given a context stack and a key, walks down the context stack until
16 | it finds a context that contains the key. The key logic is fuzzy as
17 | in get-named/contains-named? in quoin. Returns the context, not the
18 | key's value, so nil when no context is found that contains the
19 | key."
20 | [context-stack key]
21 | (loop [curr-context-stack context-stack]
22 | (if-let [context-top (peek curr-context-stack)]
23 | (if (and (associative? context-top)
24 | (map/contains-named? context-top key))
25 | context-top
26 | ;; Didn't have the key, so walk down the stack.
27 | (recur (next curr-context-stack)))
28 | ;; Either ran out of context stack or key, in either case, we were
29 | ;; unsuccessful in finding the key.
30 | nil)))
31 |
32 | (defn context-get
33 | "Given a context stack and key, implements the rules for getting the
34 | key out of the context stack (see interpolation.yml in the spec). The
35 | key is assumed to be either the special keyword :implicit-top, or a list of
36 | strings or keywords."
37 | ([context-stack key]
38 | (context-get context-stack key nil))
39 | ([context-stack key not-found]
40 | ;; First need to check for an implicit top reference.
41 | (if (.equals :implicit-top key) ;; .equals is faster than =
42 | (first context-stack)
43 | ;; Walk down the context stack until we find one that has the
44 | ;; first part of the key.
45 | (if-let [matching-context (find-containing-context context-stack
46 | (first key))]
47 | ;; If we found a matching context and there are still segments of the
48 | ;; key left, we repeat the process using only the matching context as
49 | ;; the context stack.
50 | (if (next key)
51 | (recur (list (map/get-named matching-context
52 | (first key))) ;; Singleton ctx stack.
53 | (next key)
54 | not-found)
55 | ;; Otherwise, we found the item!
56 | (map/get-named matching-context (first key)))
57 | ;; Didn't find a matching context.
58 | not-found))))
59 |
60 | (defn call-lambda
61 | "Calls a lambda function, respecting the options given in its metadata, if
62 | any. The content arg is the content of the tag being processed as a lambda in
63 | the template, and the context arg is the current context at this point in the
64 | processing. The latter will be ignored unless metadata directs otherwise.
65 |
66 | Respected metadata:
67 | - :stencil/pass-context: passes the current context to the lambda as the
68 | second arg.
69 |
70 | - :stencil/pass-render: the lambda will receive the context
71 | and the render function to be used in this context, respecting
72 | custom section delimiters"
73 | ([lambda-fn context render]
74 | (cond
75 | (:stencil/pass-render (meta lambda-fn))
76 | (str (lambda-fn context render))
77 |
78 | (:stencil/pass-context (meta lambda-fn))
79 | (render (str (lambda-fn context)) context)
80 |
81 | :else
82 | (render (str (lambda-fn)) context)))
83 |
84 | ([lambda-fn context render content]
85 | (cond
86 | (:stencil/pass-render (meta lambda-fn))
87 | (str (lambda-fn content context render))
88 |
89 | (:stencil/pass-context (meta lambda-fn))
90 | (render (str (lambda-fn content context)) context)
91 |
92 | :else
93 | (render (str (lambda-fn content)) context))))
94 |
95 | (defn core-cache-present?
96 | "Returns true if the core.cache library is available, and false otherwise."
97 | []
98 | (try
99 | (require 'clojure.core.cache)
100 | true
101 | (catch ExceptionInInitializerError _
102 | false)
103 | (catch FileNotFoundException _
104 | false)))
105 |
--------------------------------------------------------------------------------
/test/stencil/test/core.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.test.core
2 | (:use clojure.test
3 | stencil.core))
4 |
5 | ;; Test case to make sure we don't get a regression on inverted sections with
6 | ;; list values for a name.
7 |
8 | (deftest inverted-section-list-key-test
9 | (is (= ""
10 | (render-string "{{^a}}a{{b}}a{{/a}}" {:a [:b "11"]})))
11 | (is (= ""
12 | (render-string "{{^a}}a{{b}}a{{/a}}" {"a" ["b" "11"]}))))
13 |
14 | ;; Test case to make sure we print a boolean false as "false"
15 |
16 | (deftest boolean-false-print-test
17 | (is (= "false" (render-string "{{a}}" {:a false})))
18 | (is (= "false" (render-string "{{{a}}}" {:a false}))))
19 |
--------------------------------------------------------------------------------
/test/stencil/test/extensions.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.test.extensions
2 | (:use clojure.test
3 | stencil.core))
4 |
5 | ;; Test case to make sure we can run a lambda with the :stencil/pass-context
6 | ;; option in all the places a lambda can be used (escaped interpolation,
7 | ;; unescaped interpolation, and sections).
8 |
9 | (deftest extension-pass-context-test
10 | ;; This calls an escaped interpolation lambda that returns some
11 | ;; mustache code based on the current context.
12 | (is (= "things"
13 | (render-string "{{lambda}}"
14 | {:stuff "things"
15 | :tag "stuff"
16 | :lambda ^{:stencil/pass-context true}
17 | (fn [ctx] (str "{{" (:tag ctx) "}}"))})))
18 | ;; This calls an unescaped interpolation lambda that returns some mustache
19 | ;; code based on the current context.
20 | (is (= "things"
21 | (render-string "{{{lambda}}}"
22 | {:stuff "things"
23 | :tag "stuff"
24 | :lambda ^{:stencil/pass-context true}
25 | (fn [ctx] (str "{{" (:tag ctx) "}}"))})))
26 | ;; This calls a section lambda that returns some mustache code based on the
27 | ;; current context.
28 | (is (= "peanut butter jelly time"
29 | (render-string "{{#lambda}}{{thing1}}{{/lambda}} time"
30 | {:thing1 "peanut butter"
31 | :thing2 "jelly"
32 | :new-tag "thing2"
33 | :lambda ^{:stencil/pass-context true}
34 | (fn [src ctx] (str src " {{" (:new-tag ctx) "}}"))}))))
35 |
--------------------------------------------------------------------------------
/test/stencil/test/no_cache.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.test.no-cache
2 | (:use clojure.test)
3 | (:require [stencil.loader :as sldr]
4 | [stencil.utils :as utils])
5 | (:import
6 | [stencil.loader CoreCacheUnavailableStub_SeeReadme]))
7 |
8 | ;; This namespace only runs a test when core.cache is unavailable.
9 | ;; It merely tests that the stencil.loader functions will barf with
10 | ;; a message to the user when the user has not set a usable cache
11 | ;; alternative using set-cache.
12 |
13 |
14 |
15 | (defn core-cache-unavailable-stub-fixture
16 | [f]
17 | (sldr/set-cache (CoreCacheUnavailableStub_SeeReadme.))
18 | (f)
19 | (sldr/set-cache {}))
20 |
21 | (use-fixtures :once core-cache-unavailable-stub-fixture)
22 |
23 | (when (not (utils/core-cache-present?))
24 | (deftest barfs-properly-test
25 | (is (thrown-with-msg? Exception #"Could not load core.cache."
26 | (sldr/load "nonexistentfile.mustache")))))
27 |
28 |
--------------------------------------------------------------------------------
/test/stencil/test/parser.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.test.parser
2 | (:refer-clojure :exclude [partial])
3 | (:require [clojure.zip :as zip])
4 | (:use clojure.test
5 | [stencil ast parser utils]
6 | [scout.core :rename {peek peep}]))
7 |
8 | (deftest test-get-line-col-from-index
9 | (is (= [1 1] (get-line-col-from-index "a\nb\nc" 0)))
10 | (is (= [1 2] (get-line-col-from-index "a\nb\nc" 1)))
11 | (is (= [2 1] (get-line-col-from-index "a\nb\nc" 2)))
12 | ;; Same, but with the other line endings.
13 | (is (= [1 1] (get-line-col-from-index "a\r\nb\r\nc" 0)))
14 | (is (= [1 2] (get-line-col-from-index "a\r\nb\r\nc" 1)))
15 | (is (= [1 3] (get-line-col-from-index "a\r\nb\r\nc" 2)))
16 | (is (= [2 1] (get-line-col-from-index "a\r\nb\r\nc" 3))))
17 |
18 | (deftest test-format-location
19 | (is (= "line 1, column 1"
20 | (format-location (scanner "a\r\nb\r\nc"))))
21 | (is (= "line 1, column 1"
22 | (format-location "a\r\nb\r\nc" 0))))
23 |
24 | (deftest test-tag-position?
25 | (is (= true
26 | (tag-position? (scanner " {{test}}") parser-defaults)))
27 | (is (= true
28 | (tag-position? (scanner "{{test}}") parser-defaults)))
29 | (is (= true
30 | (tag-position? (scanner "\t{{test}}") parser-defaults)))
31 | (is (= false
32 | (tag-position? (scanner "\r\n{{test}}") parser-defaults)))
33 | (is (= false
34 | (tag-position? (scanner "Hi. {{test}}") parser-defaults))))
35 |
36 | (deftest test-parse-tag-name
37 | (is (= [:test]
38 | (parse-tag-name "test")))
39 | (is (= [:test :test2]
40 | (parse-tag-name "test.test2"))))
41 |
42 | (deftest test-parse-text
43 | (is (= ["test string"]
44 | (zip/root (:output (parse-text (parser (scanner "test string")))))))
45 | (is (= ["test string"]
46 | (zip/root (:output (parse-text
47 | (parser (scanner "test string{{tag}}")))))))
48 | (is (= ["test string\n"]
49 | (zip/root (:output (parse-text
50 | (parser (scanner "test string\n{{tag}}")))))))
51 | (is (= ["test string\n"]
52 | (zip/root (:output (parse-text
53 | (parser (scanner "test string\n {{tag}}")))))))
54 | (is (= ["\ntest string"]
55 | (zip/root (:output (parse-text
56 | (parser (scanner "\ntest string{{tag}}")))))))
57 | (is (= ["\ntest string\n"]
58 | (zip/root (:output (parse-text
59 | (parser (scanner "\ntest string\n{{tag}}"))))))))
60 |
61 | (deftest test-parse-tag
62 | (is (= [" " (escaped-variable (parse-tag-name "blah"))]
63 | (zip/root (:output (parse-tag
64 | (parser (scanner " {{blah}}")))))))
65 | (is (= [" " (unescaped-variable (parse-tag-name "blah"))]
66 | (zip/root (:output (parse-tag
67 | (parser (scanner " {{{blah}}}")))))))
68 | (is (= [" " (unescaped-variable (parse-tag-name "blah"))]
69 | (zip/root (:output (parse-tag
70 | (parser (scanner " {{{ blah}}}")))))))
71 | (is (= [" " (unescaped-variable (parse-tag-name "blah"))]
72 | (zip/root (:output (parse-tag
73 | (parser (scanner " {{{ blah }}}")))))))
74 | (is (= [" " (unescaped-variable (parse-tag-name "blah"))]
75 | (zip/root (:output (parse-tag
76 | (parser (scanner " {{&blah}}")))))))
77 | (is (= [" " (unescaped-variable (parse-tag-name "blah"))]
78 | (zip/root (:output (parse-tag
79 | (parser (scanner " {{& blah}}")))))))
80 | (is (= [" " (unescaped-variable (parse-tag-name "blah"))]
81 | (zip/root (:output (parse-tag
82 | (parser (scanner " {{& blah }}")))))))
83 | ;; Test whitespace removal on a standalone tag.
84 | (is (= []
85 | (zip/root (:output (parse-tag
86 | (parser (scanner " {{!blah}}\n")))))))
87 | (is (= []
88 | (zip/root (:output (parse-tag
89 | (parser (scanner " {{!blah}}\r\n"))))))))
90 |
91 | (deftest test-set-delimiter-parse
92 | (is (= [] (parse "{{= blah blah=}}")))
93 | (is (= ["hi"] (parse "{{= blah blah =}}hi")))
94 | (is (thrown? Exception (parse "{{= name}}y")))
95 | (is (thrown? Exception (parse "{{= name }} y")))
96 | (is (thrown? Exception (parse "{{= name =}}y"))))
97 |
--------------------------------------------------------------------------------
/test/stencil/test/re_utils.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.test.re-utils
2 | (:use clojure.test
3 | stencil.re-utils))
4 |
5 | (deftest test-re-concat
6 | ;; Obviously regular expressions don't have a sensible way of comparing for
7 | ;; equivalent expressions (ie, (= #"a" #"a") -> false). So just compare the
8 | ;; string version in these tests.
9 | (is (= "test" (str (re-concat #"t" #"e" #"s" #"t"))))
10 | (is (= "test" (str (re-concat "t" "e" "s" "t"))))
11 | (is (= "test" (str (re-concat #"te" "st")))))
12 |
13 | (deftest test-re-quote
14 | (is (= java.util.regex.Pattern (type (re-quote "test"))))
15 | (is (= "\\Qtest^|?\\E" (str (re-quote "test^|?")))))
16 |
--------------------------------------------------------------------------------
/test/stencil/test/spec.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.test.spec
2 | (:use clojure.test
3 | stencil.core
4 | [stencil.loader :exclude [load]])
5 | (:require [clojure.data.json :as json]
6 | [clojure.java.shell :as sh]
7 | [clojure.java.io :as io]
8 | [stencil.utils :as utils])
9 | (:import [java.io FileNotFoundException]))
10 |
11 | (def repo-url "https://github.com/mustache/spec.git")
12 | (def spec-dir "target/test/spec")
13 |
14 | ;; Acquiring the specs
15 |
16 | (defn spec-present?
17 | "Check if the spec is available in the test/spec dir. Checks for the
18 | existence of the specs subdir."
19 | []
20 | (.exists (io/file spec-dir "specs")))
21 |
22 | (defn clone-spec
23 | "Use git to clone the specs into the spec-dir."
24 | []
25 | (try (sh/sh "git" "clone" repo-url spec-dir)
26 | (catch java.io.IOException e)))
27 |
28 | (defn pull-spec-if-missing
29 | "Get the spec if it isn't already present."
30 | []
31 | (when (not (spec-present?))
32 | (clone-spec)))
33 |
34 |
35 | ;; Read specs and create tests from them.
36 |
37 | (defn spec-json
38 | []
39 | ;; JSON are duplicates of YAML, and we don't have a YAML parser
40 | ;; that can handle !code tags, so for now we use JSON.
41 | (filter #(.endsWith (.getName %) ".json")
42 | (file-seq (io/file spec-dir "specs"))))
43 |
44 | (defn read-spec-file
45 | [^java.io.File spec-file]
46 | (json/read-json (slurp spec-file)))
47 |
48 | (defn compile-data-map
49 | "Given the data map for a test, compiles the clojure lambdas for any keys
50 | that have as their value maps with a key :__tag__ with value \"code\".
51 | Should pass through maps that don't have such keys."
52 | [data-map]
53 | (into {} (for [[key val] data-map]
54 | (if (and (map? val)
55 | (contains? val :__tag__)
56 | (= "code" (:__tag__ val)))
57 | [key (load-string (:clojure val))]
58 | [key val]))))
59 |
60 | (defn tests-from-spec
61 | "Given a spec (a list of tests), create the corresponding tests."
62 | [spec]
63 | (let [tests (:tests spec)]
64 | (doseq [test tests]
65 | (let [{:keys [name data expected template desc partials]} test]
66 | ;; If there are partials, register them before test clauses.
67 | (eval `(deftest ~(symbol name)
68 | ;; Clear the dynamic template store to ensure a clean env.
69 | (unregister-all-templates)
70 | (doseq [[partial-name# partial-src#] ~partials]
71 | (register-template (name partial-name#) partial-src#))
72 | (let [data# (compile-data-map ~data)]
73 | (is (= ~expected
74 | (render-string ~template data#)) ~desc))))))))
75 |
76 | (pull-spec-if-missing)
77 |
78 | ;; We support a mode where core.cache is not present, so the tests should
79 | ;; also handle this case gracefully. When it is not present, we want to
80 | ;; ensure that the tests work with a map instead of a cache.
81 | (when (not (utils/core-cache-present?))
82 | (set-cache {}))
83 |
84 | (doseq [spec (spec-json)]
85 | (tests-from-spec (read-spec-file spec)))
86 |
87 |
--------------------------------------------------------------------------------
/test/stencil/test/utils.clj:
--------------------------------------------------------------------------------
1 | (ns stencil.test.utils
2 | (:use clojure.test
3 | stencil.utils
4 | stencil.core))
5 |
6 | (deftest test-find-containing-context
7 | (is (= {:a 1}
8 | (find-containing-context '({:a 1}) :a)))
9 | (is (= {:a 1}
10 | (find-containing-context '({:a 1}) "a")))
11 | (is (= {:a 1}
12 | (find-containing-context '({:b 2} {:a 1}) "a"))))
13 |
14 | (deftest test-context-get
15 | (is (= "success"
16 | (context-get '({:a "success"})
17 | ["a"])))
18 | (is (= "success"
19 | (context-get '({:a {:b "success"}})
20 | ["a" :b])))
21 | (is (= "success"
22 | (context-get '({:b 1} {:a "success"})
23 | ["a"])))
24 | (is (= "failure"
25 | (context-get '({:a "problem?"} {:a {:b "success"}})
26 | ["a" "b"] "failure"))))
27 |
28 | (deftest test-pass-context
29 | (is (= "foo" (call-lambda (fn [] "foo") nil render-string)))
30 | (is (= "foo*bar" (call-lambda ^{:stencil/pass-context true}
31 | (fn [ctx] (str "foo*" (:addition ctx)))
32 | {:addition "bar"}
33 | render-string)))
34 | (is (= "foo*" (call-lambda (fn [x] (str x "*"))
35 | nil
36 | render-string
37 | "foo")))
38 | (is (= "foo*bar"
39 | (call-lambda ^{:stencil/pass-context true}
40 | (fn [x ctx] (str x "*" (:second-arg ctx)))
41 | {:second-arg "bar"}
42 | render-string
43 | "foo"))))
44 |
45 | (deftest test-pass-render
46 | (is (= "{{foo}}*bar" (call-lambda ^{:stencil/pass-render true}
47 | (fn [ctx render] (str "{{foo}}*" (:addition ctx)))
48 | {:addition "bar"}
49 | render-string)))
50 | (is (= "{{baz}}" (call-lambda ^{:stencil/pass-render true}
51 | (fn [content ctx render]
52 | content)
53 | nil
54 | render-string
55 | "{{baz}}")))
56 | (is (= "baz*" (call-lambda ^{:stencil/pass-render true}
57 | (fn [content ctx render]
58 | (render content ctx))
59 | {:baz "baz*"}
60 | render-string
61 | "{{baz}}")))
62 | (is (= "bar" (call-lambda ^{:stencil/pass-render true}
63 | (fn [ctx render] (render "{{addition}}" ctx))
64 | {:addition "bar"}
65 | render-string))))
66 |
--------------------------------------------------------------------------------