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