pax_global_header 0000666 0000000 0000000 00000000064 12563560631 0014521 g ustar 00root root 0000000 0000000 52 comment=a91f860c05cbb3b80c89e9c35d170fdddc90ff91
stencil-0.5.0/ 0000775 0000000 0000000 00000000000 12563560631 0013164 5 ustar 00root root 0000000 0000000 stencil-0.5.0/.gitignore 0000664 0000000 0000000 00000000104 12563560631 0015147 0 ustar 00root root 0000000 0000000 .cake
pom.xml
*.jar
*.war
lib
classes
build
/stencil
/target
.lein-* stencil-0.5.0/.travis.yml 0000664 0000000 0000000 00000000142 12563560631 0015272 0 ustar 00root root 0000000 0000000 language: clojure
lein: lein2
script: lein2 all test
jdk:
- openjdk7
- openjdk6
- oraclejdk7 stencil-0.5.0/LICENSE 0000664 0000000 0000000 00000025764 12563560631 0014207 0 ustar 00root root 0000000 0000000 Eclipse 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.md 0000664 0000000 0000000 00000023304 12563560631 0014445 0 ustar 00root root 0000000 0000000 # 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.clj 0000664 0000000 0000000 00000002564 12563560631 0015333 0 ustar 00root root 0000000 0000000 (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/ 0000775 0000000 0000000 00000000000 12563560631 0013753 5 ustar 00root root 0000000 0000000 stencil-0.5.0/src/stencil/ 0000775 0000000 0000000 00000000000 12563560631 0015414 5 ustar 00root root 0000000 0000000 stencil-0.5.0/src/stencil/ast.clj 0000664 0000000 0000000 00000010562 12563560631 0016701 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000010205 12563560631 0017034 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000020014 12563560631 0017351 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000041314 12563560631 0017405 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000001176 12563560631 0017741 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000007544 12563560631 0017260 0 ustar 00root root 0000000 0000000 (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/ 0000775 0000000 0000000 00000000000 12563560631 0014143 5 ustar 00root root 0000000 0000000 stencil-0.5.0/test/stencil/ 0000775 0000000 0000000 00000000000 12563560631 0015604 5 ustar 00root root 0000000 0000000 stencil-0.5.0/test/stencil/test/ 0000775 0000000 0000000 00000000000 12563560631 0016563 5 ustar 00root root 0000000 0000000 stencil-0.5.0/test/stencil/test/core.clj 0000664 0000000 0000000 00000001101 12563560631 0020176 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000003007 12563560631 0021454 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000001507 12563560631 0021017 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000010063 12563560631 0020551 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000001112 12563560631 0021076 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000005446 12563560631 0020220 0 ustar 00root root 0000000 0000000 (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.clj 0000664 0000000 0000000 00000005046 12563560631 0020422 0 ustar 00root root 0000000 0000000 (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))))