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