Skip to content
Browse files

Fix README.md merge conflict.

  • Loading branch information...
2 parents 4770205 + 796da65 commit 595efafa2b502c4a568981bc5eb0a6f317eb5d10 @davidsantiago davidsantiago committed
View
1 .gitignore
@@ -7,3 +7,4 @@ classes
build
/stencil
test/spec
+.lein-*
View
7 .travis.yml
@@ -0,0 +1,7 @@
+language: clojure
+lein: lein2
+script: lein2 all test
+jdk:
+ - openjdk7
+ - openjdk6
+ - oraclejdk7
View
63 README.md
@@ -30,14 +30,15 @@ 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 string 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).
+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" "Donald" :name "Dick"})
+ {"name" "Dick" :name "Donald"})
"Hi there, Donald."
For a larger template, holding onto it and passing it in as a string is
@@ -79,26 +80,17 @@ it.
### Manual Cache Management
-By default, the template cache will keep a template in the cache for 5
-seconds before it will decide to reload it. You can set the cache policy
-manually using the function `set-cache-policy`. The argument to
-`set-cache-policy` is a function that returns true if the cached item is still
-valid and false if it should be reloaded. The argument is a cache entry data
-structure; you should check the source of `stencil.loader` for specifics.
-However, there are some cache policy functions that should cover most cases:
-
-* `cache-forever` - This function can be used as a cache policy function that
-will keep templates in the cache forever (or until they are explicitly
-reloaded). This can be useful if you know that templates won't change during
-the life of the program.
-* `cache-never` - This function can be used similarly to never cache a
-template. Could be useful for development, so that changes to templates are
-available immediately.
-* `cache-timeout` - This function cannot be used as a cache policy function
-directly, it's a combinator. It takes an integer argument that will be the
-number of milliseconds to keep an item in the cache, and returns a cache
-policy function that implements that policy. The default cache policy is
-`(cache-timeout 5000)`.
+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.
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
@@ -132,7 +124,7 @@ 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.
+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
@@ -142,11 +134,11 @@ Java and .NET applications. Take a look at YourKit's leading software products:
## Obtaining
-If you are using Leiningen, you can add
+Simply add
- [stencil "0.2.0"]
+ [stencil "0.3.0"]
-to your project.clj and run `lein deps`.
+to the `:dependencies` key of your project.clj.
## Bugs and Missing Features
@@ -156,10 +148,19 @@ it as soon as possible.
## Recently
-* 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.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.
View
24 project.clj
@@ -1,7 +1,19 @@
-(defproject stencil "0.2.0"
+(defproject stencil "0.3.0"
:description "Mustache in Clojure"
- :dependencies [[clojure "1.3.0"]
- [slingshot "0.8.0"]]
- :dev-dependencies [[ritz "0.2.0"]
- [org.clojure/data.json "0.1.1"]]
- :extra-files-to-clean ["test/spec"])
+ :dependencies [[org.clojure/clojure "1.3.0"]
+ [scout "0.1.0"]
+ [quoin "0.1.0"]
+ [slingshot "0.8.0"]
+ [org.clojure/core.cache "0.6.1"]]
+ :profiles {:dev {:dependencies [[org.clojure/data.json "0.1.2"]]}
+ :clj1.2 {:dependencies [[org.clojure/clojure "1.2.1"]]}
+ :clj1.3 {:dependencies [[org.clojure/clojure "1.3.0"]]}
+ :clj1.4 {:dependencies [[org.clojure/clojure "1.4.0"]]}
+ :clj1.5 {:dependencies [[org.clojure/clojure "1.5.0-master-SNAPSHOT"]]}}
+ :aliases {"all" ["with-profile" "dev:dev,clj1.4:dev,clj1.5"]}
+ :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}}})
View
6 src/stencil/ast.clj
@@ -17,7 +17,7 @@
the existing children, make the new node."))
(defprotocol ASTNode
- (render [this ^StrinbBuilder sb context-stack]
+ (render [this ^StringBuilder sb context-stack]
"Given a StringBuilder and the current context-stack, render this node to
the result string in the StringBuilder."))
@@ -108,8 +108,8 @@
(render [this ^StringBuilder sb context-stack] (.append sb this))
clojure.lang.PersistentVector
(render [this sb context-stack]
- (doseq [node this]
- (render node sb context-stack))))
+ (dotimes [i (count this)]
+ (render (nth this i) sb context-stack))))
;; Implement a Zipper over ASTZippers.
View
21 src/stencil/core.clj
@@ -3,7 +3,8 @@
[stencil.loader :as loader])
(:use [stencil.parser :exclude [partial]]
[stencil.ast :rename {render node-render
- partial node-partial}]
+ partial node-partial}]
+ [quoin.text :as qtext]
[clojure.java.io :only [resource]]
stencil.utils))
@@ -32,13 +33,15 @@
(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 [lambda-return (ctx-val (:content (:attrs this)))]
+ (let [current-context (first context-stack)
+ lambda-return (call-lambda ctx-val (:content (:attrs this))
+ current-context)]
;; We have to manually parse because the spec says lambdas in
;; sections get parsed with the current parser delimiters.
(.append sb (render (parse lambda-return
(select-keys (:attrs this)
[:tag-open :tag-close]))
- (first context-stack))))
+ current-context)))
;; Non-false non-list value -> Display content once.
:else
(node-render (:contents this) sb (conj context-stack ctx-val)))))
@@ -46,15 +49,19 @@
(render [this ^StringBuilder sb context-stack]
(if-let [value (context-get context-stack (:name this))]
(if (instance? clojure.lang.Fn value)
- (.append sb (html-escape (render-string (str (value))
- (first context-stack))))
+ (.append sb (qtext/html-escape
+ (render-string (str (call-lambda value
+ (first context-stack)))
+ (first context-stack))))
;; Otherwise, just append its html-escaped value by default.
- (.append sb (html-escape value)))))
+ (.append sb (qtext/html-escape (str value))))))
stencil.ast.UnescapedVariable
(render [this ^StringBuilder sb context-stack]
(if-let [value (context-get context-stack (:name this))]
(if (instance? clojure.lang.Fn value)
- (.append sb (render-string (str (value)) (first context-stack)))
+ (.append sb (render-string (str (call-lambda value
+ (first context-stack)))
+ (first context-stack)))
;; Otherwise, just append its value.
(.append sb value)))))
View
127 src/stencil/loader.clj
@@ -3,62 +3,35 @@
(:use [clojure.java.io :only [resource]]
[stencil.parser :exclude [partial]]
[stencil.ast :exclude [partial]]
+ [quoin.text :as qtext]
stencil.utils)
- (:import java.util.Date))
+ (:require [clojure.core.cache :as cache]))
;; 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 template names to its parsed versions.
-(def ^{:private true} parsed-template-cache (atom {}))
-
-;;
-;; Cache policies
-;;
-
-(defn cache-forever
- "This cache policy will let entries live on forever (until explicitly
- invalidated). Could be useful in production if mustache templates can't be
- changed in that environment."
- [cache-entry]
- true)
-
-(defn cache-never
- "This cache policy will consider cache entries to never be valid, essentially
- disabling caching. Could be useful for development."
- [cache-entry]
- false)
-
-(defn cache-timeout
- "This is a cache policy generator. Takes a timeout in milliseconds as an
- argument and returns a cache policy that considers cache entries valid for
- only that long."
- [timeout-ms]
- (fn [cache-entry]
- (let [now (Date.)]
- (< (.getTime now)
- (+ (.getTime ^Date (:entry-date cache-entry))
- timeout-ms)))))
-
-;; Cache policy dictates when a given cache entry is valid. It should be a
-;; function that takes an entry and returns true if it is still valid.
-;; By default, caches templates for 5 seconds.
-(def ^{:private true} cache-policy (atom (cache-timeout 5000)))
+;; The parsed template cache maps a template name to its parsed versions.
+(def ^{:private true} parsed-template-cache (atom (cache/lru-cache-factory {})))
;; Holds a cache entry
(defrecord TemplateCacheEntry [src ;; The source code of the template
- parsed ;; Parsed ASTNode structure.
- entry-date]) ;; Date when we cached this.
+ parsed]) ;; Parsed ASTNode structure.
(defn template-cache-entry
- "Given template source, parsed ASTNodes, and timestamp, creates a cache entry.
- If only source is given, parsed and timestamp are calculated automatically."
+ "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]
- (template-cache-entry src parsed (Date.)))
- ([src parsed timestamp]
- (TemplateCacheEntry. src parsed timestamp)))
+ (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)
@@ -66,13 +39,13 @@
"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-fuzzy 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-fuzzy template-name)
+ (swap! dynamic-template-store dissoc template-name)
(invalidate-cache-entry template-name))
(defn unregister-all-templates
@@ -97,30 +70,25 @@
;;
;; Cache mechanics
;;
-;; The template cache has two keys, the template name, and a secondary key that
-;; is called the variant. The default variant is set/fetched with nil as the
-;; variant key. Invalidating an entry invalidates all variants. The variants
-;; do NOT work with "fuzzy" map logic for getting/setting.
+;; 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-assoc
- "Function used to make atomic updates to the cache. Inserts val at the
- hierarchical position in the map given by the pair of keys template-name and
- template-variant. The first key (template-name) is fuzzy, the variant is
- not."
- [map [template-name template-variant] val]
- (let [template-variants (get-fuzzy map template-name)]
- (assoc-fuzzy map template-name (assoc template-variants
- template-variant val))))
-
(defn cache
- "Given a template name, variant key, template source, and parsed AST,
- stores that entry in the template cache. Returns the parsed template"
+ "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
- cache-assoc [template-name template-variant]
+ assoc-in [template-name template-variant]
(template-cache-entry template-src
parsed-template))
parsed-template))
@@ -129,32 +97,23 @@
"Given a template name, invalidates the cache entry for that name, if there
is one."
[template-name]
- (swap! parsed-template-cache dissoc-fuzzy template-name))
+ (swap! parsed-template-cache dissoc template-name))
(defn invalidate-cache
"Clears all entries out of the cache."
[]
- (reset! parsed-template-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. Will apply the cache policy, so if the cache policy says
- the entry is too old, it will return nil. Otherwise, returns the
- cache-entry. Single argument version gets the default (nil) variant."
+ "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 nil))
+ (cache-get template-name :default))
([template-name template-variant]
- (let [cache-entry (get (get-fuzzy @parsed-template-cache template-name)
- template-variant)]
- (when (and (not (nil? cache-entry))
- (@cache-policy cache-entry))
- cache-entry))))
-
-(defn set-cache-policy
- "Sets the function given as an argument to be the cache policy function (takes
- a cache-entry as argument, returns true if it is still valid)."
- [new-cache-policy-fn]
- (reset! cache-policy new-cache-policy-fn))
+ (get-in @parsed-template-cache [template-name template-variant])))
+
;;
;; Loader API
@@ -166,7 +125,7 @@
will look in the dynamic template store, then look in the classpath for
a file called myfile.mustache or just myfile.
- With addition arguments template-variant and variant-fn, supports the load
+ 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."
@@ -176,7 +135,7 @@
(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-fuzzy @dynamic-template-store template-name)]
+ (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.
@@ -197,7 +156,9 @@
(render [this sb context-stack]
(let [padding (:padding this)
template (if padding
- (load (:name this) padding #(indent-string % padding))
+ (load (:name this)
+ padding
+ #(qtext/indent-string % padding))
(load (:name this)))]
(when template
(render template sb context-stack)))))
View
11 src/stencil/parser.clj
@@ -1,9 +1,10 @@
(ns stencil.parser
(:refer-clojure :exclude [partial])
- (:require [stencil.scanner :as scan]
+ (:require [scout.core :as scan]
[clojure.zip :as zip]
[clojure.string :as string])
- (:import java.util.regex.Pattern)
+ (:import java.util.regex.Pattern
+ scout.core.Scanner)
(:use [stencil ast re-utils utils]
clojure.pprint
[slingshot.slingshot :only [throw+]]))
@@ -69,7 +70,7 @@
(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."
- ([^stencil.scanner.Scanner sc]
+ ([^Scanner sc]
(format-location (:src sc) (scan/position sc)))
([s idx]
(let [[line col] (get-line-col-from-index s idx)]
@@ -99,7 +100,7 @@
"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."
- [^stencil.scanner.Scanner s parser-state]
+ [^Scanner s parser-state]
(let [tag-open-re (re-concat #"([ \t]*)?"
(re-quote (:tag-open parser-state)))]
;; Return true if first expr makes progress.
@@ -115,7 +116,7 @@
[^String s]
(if (= "." s)
:implicit-top
- (string/split s #"\.")))
+ (doall (map keyword (string/split s #"\.")))))
(defn parse-text
"Given a parser that is not in tag position, reads text until it is and
View
189 src/stencil/scanner.clj
@@ -1,189 +0,0 @@
-(ns stencil.scanner
- "An old-fashioned string scanner."
- (:refer-clojure :exclude [peek])
- (:import java.util.regex.Matcher))
-
-(defrecord MatchInfo [start ;; Start index of a match.
- end ;; End index of a match.
- groups]) ;; Vector of groups.
-
-(defrecord Scanner [src ;; String to be matched.
- curr-loc ;; Current location in string.
- ^MatchInfo match]) ;; Information about the last match.
-
-(defn scanner
- ([source-string]
- (scanner source-string 0 nil))
- ([source-string pos]
- (scanner source-string pos nil))
- ([source-string pos match]
- (Scanner. source-string pos match)))
-
-(defn ^{:private true} re-groups-vec
- "Clojure's re-groups will return a plain string if there is only one match
- in the group. Really inconvenient for what we're doing, so here's a similar
- function that always returns a vector of strings."
- [^Matcher m]
- (let [groupCount (.groupCount m)]
- (loop [result (transient [])
- i 0]
- (if (<= i groupCount)
- (recur (conj! result (.group m i))
- (inc i))
- (persistent! result)))))
-
-(defn match-info
- ([^Matcher matcher]
- (match-info (.start matcher)
- (.end matcher)
- (re-groups-vec matcher)))
- ([start end]
- (match-info start end nil))
- ([start end groups]
- (MatchInfo. start end groups)))
-
-;;
-;; Positional information. These functions act on a Scanner and return
-;; the requested values.
-;;
-
-(defn position
- "Returns the current position in the string (an integer index)."
- [^Scanner scanner]
- (:curr-loc scanner))
-
-(defn beginning-of-line?
- "Return true if the current position is the beginning of a line."
- [^Scanner scanner]
- (let [curr-loc (:curr-loc scanner)]
- (or (= 0 curr-loc)
- (= \newline (get (:src scanner) (dec curr-loc))))))
-
-(defn end?
- "Return true if the current position is the end of the input string."
- [^Scanner scanner]
- (>= (:curr-loc scanner) (count (:src scanner))))
-
-(defn remainder
- "Return what remains of the string after the scan pointer."
- [^Scanner scanner]
- (let [src (:src scanner)]
- (subs src (min (:curr-loc scanner) (count src)))))
-
-(defn groups
- "Return the groups from the last match. Remember that the first group
- will be the complete match."
- [^Scanner scanner]
- (get-in scanner [:match :groups]))
-
-(defn matched
- "Return the last matched string."
- [^Scanner scanner]
- (first (groups scanner)))
-
-(defn pre-match
- "Return the 'pre-match' of the last scan. This is the part of the input
- before the beginning of the match."
- [^Scanner scanner]
- (let [match (:match scanner)]
- (if match
- (subs (:src scanner) 0 (:start match)))))
-
-(defn post-match
- "Return the 'post-match' of the last scan. This is the part of the input
- after the end of the last match."
- [^Scanner scanner]
- (let [match (:match scanner)]
- (if match
- (subs (:src scanner) (:end match)))))
-
-;;
-;; Scanning/Advancing. These functions advance the scan pointer, returning a
-;; Scanner object with the new configuration.
-;;
-
-(defn scan
- "Match pattern starting at current location. On match, advances the
- current location and puts the matched string in result. Otherwise,
- just returns the same scanner, minus any previous match data."
- [^Scanner s pattern]
- (let [src (:src s)
- ;; Need to set the region to restrict the window the matcher
- ;; looks at to start at the current position.
- matcher (.region (re-matcher pattern src)
- (position s)
- (count src))
- match-result (if (.lookingAt matcher)
- matcher)]
- (if match-result
- (let [mi (match-info matcher)
- matched-string (first (:groups mi))]
- (scanner src
- (+ (position s) (count matched-string))
- mi))
- (assoc s :match nil))))
-
-(defn scan-until
- "Match pattern at any point after the current location. On match, advances
- the current location to the end of the match, and puts just the matching
- part in the match info. Otherwise, just returns the same scanner, minus
- any previous match data."
- [^Scanner s pattern]
- (let [src (:src s)
- matcher (.region (re-matcher pattern src)
- (position s)
- (count src))
- match-result (if (.find matcher)
- matcher)]
- (if match-result
- (let [mi (match-info matcher)]
- (scanner src
- (:end mi)
- mi))
- ;; Remove the match data from the input scanner, since we failed to match.
- (assoc s :match nil))))
-
-(defn skip-to-match-start
- "Match pattern at any point after the current location. On match, advances
- the current location to the beginning of the match, so that a subsequent
- scan with the same pattern will succeed. Matched pattern is stored in the
- result. Otherwise, just returns the same scanner, minus any previous match
- data."
- [^Scanner s pattern]
- (let [src (:src s)
- scan-result (scan-until s pattern)
- matched-string (matched scan-result)]
- ;; Note: scan-until may have failed, but the calculation below should work.
- (scanner src
- (- (position scan-result) (count matched-string))
- (:match scan-result))))
-
-;;
-;; Looking ahead. These functions tell you about what is further ahead in
-;; the string. Return the answers instead of a new Scanner.
-;;
-
-(defn check
- "Returns what scan would return as its result."
- [^Scanner s pattern]
- (matched (scan s pattern)))
-
-(defn check-until
- "Returns what scan-until would return as its match."
- [^Scanner s pattern]
- (matched (scan-until s pattern)))
-
-(defn check-until-inclusive
- "Returns the string between the scanner's starting position and the end
- of what scan-until would match."
- [^Scanner s pattern]
- (let [start-pos (position s)]
- (subs (:src s) start-pos (position (scan-until s pattern)))))
-
-(defn peek
- "Returns the string containing the next n characters after current location."
- [^Scanner s n]
- (let [remainder (remainder s)]
- (subs remainder 0 (min n
- (count remainder)))))
-
View
132 src/stencil/utils.clj
@@ -1,105 +1,25 @@
(ns stencil.utils
- (:require [clojure.string :as str]))
-
-(defn html-escape
- "HTML-escapes the given string."
- [^String s]
- (-> s
- (str/replace "&" "&amp;") ;; Do & first, or it escapes other escapes!
- (str/replace "<" "&lt;")
- (str/replace ">" "&gt;")
- (str/replace "\"" "&quot;")))
-
-(defn indent-string
- "Given a String s, indents each line by inserting the string indentation
- at the beginning."
- [^String s ^String indentation]
- (let [str+padding (StringBuilder.)]
- (loop [start-idx 0
- next-idx (.indexOf s "\n")] ;; \n handles both \r\n & \n linebreaks.
- (if (= -1 next-idx)
- ;; We've reached the end. If the start and end are the same, don't
- ;; indent before an empty string. Either way, return the string.
- (do (when (not= start-idx (count s))
- (.append str+padding indentation)
- (.append str+padding s start-idx (count s)))
- (.toString str+padding))
- (let [next-idx (inc next-idx)]
- (.append str+padding indentation)
- (.append str+padding s start-idx next-idx)
- (recur next-idx (.indexOf s "\n" next-idx)))))))
-
-;;
-;; Fuzzy map access routines
-;;
-
-(defn contains-fuzzy?
- "Given a map and a key, returns \"true\" if the map contains the key, allowing
- for differences of type between string and keyword. That is, :blah and
- \"blah\" are the same key. The key of the same type is preferred. Returns
- the variant of the found key for true, nil for false."
- ([map key] (contains-fuzzy? map key nil))
- ([map key not-found]
- (if (contains? map key)
- key
- (let [str-key (name key)]
- (if (contains? map str-key)
- str-key
- (let [kw-key (keyword key)]
- (if (contains? map kw-key)
- kw-key
- not-found)))))))
-
-(defn get-fuzzy
- "Given a map and a key, gets the value out of the map, trying various
- permitted combinations of the key. Key can be either a keyword or string,
- and is tried first as it is, before being converted to the other."
- ([map key]
- (get-fuzzy map key nil))
- ([map key not-found]
- (get map key (get map (name key)
- (get map (keyword key) not-found)))))
-
-(defn assoc-fuzzy
- "Just like clojure.core/assoc, except considers keys that are keywords and
- strings equivalent. That is, if you assoc :keyword into a map with a key
- \"keyword\", the latter is replaced."
- ([map key val]
- (let [found-key (contains-fuzzy? map key key)]
- (assoc map found-key val)))
- ([map key val & kvs]
- (let [new-map (assoc-fuzzy map key val)]
- (if kvs
- (recur new-map (first kvs) (second kvs) (nnext kvs))
- new-map))))
-
-(defn dissoc-fuzzy
- "Given a map and key(s), returns a map without the mappings for the keys,
- allowing for the keys to be certain combinations (ie, string/keyword are
- equivalent)."
- ([map] map)
- ([map key]
- (if-let [found-key (contains-fuzzy? map key)]
- (dissoc map found-key)))
- ([map key & ks]
- (let [new-map (dissoc-fuzzy map key)]
- (if ks
- (recur new-map (first ks) (next ks))
- new-map))))
+ (:require [clojure.string :as str]
+ [quoin.map-access :as map]))
;;
;; 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-fuzzy/contains-fuzzy?. Returns the context, not the key's value,
- so nil when no context is found that contains the key."
+ "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 (first curr-context-stack)]
- (if (contains-fuzzy? context-top key)
+ (if-let [context-top (peek curr-context-stack)]
+ (if (map/contains-named? context-top key)
context-top
;; Didn't have the key, so walk down the stack.
(recur (next curr-context-stack)))
@@ -116,7 +36,7 @@
(context-get context-stack key nil))
([context-stack key not-found]
;; First need to check for an implicit top reference.
- (if (= :implicit-top key)
+ (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.
@@ -126,11 +46,29 @@
;; key left, we repeat the process using only the matching context as
;; the context stack.
(if (next key)
- (recur (list (get-fuzzy matching-context
- (first key))) ;; Singleton ctx stack.
+ (recur (list (map/get-named matching-context
+ (first key))) ;; Singleton ctx stack.
(next key)
not-found)
;; Otherwise, we found the item!
- (get-fuzzy matching-context (first key)))
+ (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."
+ ([lambda-fn context]
+ (if (:stencil/pass-context (meta lambda-fn))
+ (lambda-fn context)
+ (lambda-fn)))
+ ([lambda-fn content context]
+ (if (:stencil/pass-context (meta lambda-fn))
+ (lambda-fn content context)
+ (lambda-fn content))))
View
34 test/stencil/test/extensions.clj
@@ -0,0 +1,34 @@
+(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) "}}"))}))))
View
6 test/stencil/test/parser.clj
@@ -3,7 +3,7 @@
(:require [clojure.zip :as zip])
(:use clojure.test
[stencil ast parser utils]
- [stencil.scanner :rename {peek peep}]))
+ [scout.core :rename {peek peep}]))
(deftest test-get-line-col-from-index
(is (= [1 1] (get-line-col-from-index "a\nb\nc" 0)))
@@ -34,9 +34,9 @@
(tag-position? (scanner "Hi. {{test}}") parser-defaults))))
(deftest test-parse-tag-name
- (is (= ["test"]
+ (is (= [:test]
(parse-tag-name "test")))
- (is (= ["test" "test2"]
+ (is (= [:test :test2]
(parse-tag-name "test.test2"))))
(deftest test-parse-text
View
161 test/stencil/test/scanner.clj
@@ -1,161 +0,0 @@
-(ns stencil.test.scanner
- (:use clojure.test
- [stencil.scanner :rename {peek peep}]))
-
-;;
-;; Information access tests.
-;;
-
-(deftest test-position
- (is (= 0 (position (scanner ""))))
- (is (= 0 (position (scanner "test"))))
- (is (= 1 (position (stencil.scanner.Scanner. "test" 1 nil)))))
-
-(deftest test-beginning-of-line?
- (is (= true (beginning-of-line? (scanner ""))))
- (is (= true (beginning-of-line? (scanner "test"))))
- (is (= false (beginning-of-line?
- (stencil.scanner.Scanner. "test\r\ntest" 5 nil))))
- (is (= true (beginning-of-line?
- (stencil.scanner.Scanner. "test\r\ntest" 6 nil)))))
-
-(deftest test-end?
- (is (= true (end? (scanner ""))))
- (is (= false (end? (scanner "test")))))
-
-(deftest test-remainder
- (is (= "test" (remainder (scanner "test"))))
- (is (= "" (remainder (scanner ""))))
- (is (= "" (remainder (stencil.scanner.Scanner. "test" 4 nil))))
- (is (= "" (remainder (stencil.scanner.Scanner. "test" 5 nil)))))
-
-(deftest test-groups
- (is (= ["m"]
- (groups (scanner "test" 0 (match-info 0 1 ["m"]))))))
-
-(deftest test-matched
- (is (= "m"
- (matched (scanner "test" 0 (match-info 0 1 ["m"]))))))
-
-(deftest test-pre-match
- (is (= "beginn"
- (pre-match (scanner "beginning" 9 (match-info 6 8 ["in"])))))
- (is (= "test"
- (pre-match (scan (scan (scanner "test string") #"test") #"\s+")))))
-
-(deftest test-post-match
- (is (= "ning"
- (post-match (scanner "beginning" 5 (match-info 3 5 ["in"])))))
- (is (= "string"
- (post-match (scan (scan (scanner "test string") #"test") #"\s+")))))
-
-;;
-;; Scanning/Advancing tests.
-;;
-
-(deftest test-scan
- (is (= "t"
- (matched (scan (scanner "test") #"t"))))
- (is (= 1 (position (scan (scanner "test") #"t"))))
- (is (= "test"
- (matched (scan (scanner "test") #"test"))))
- (is (end? (scan (scanner "test") #"test")))
- (is (= ["t"]
- (groups (scan (scanner "test string") #"t"))))
- ;; Compounded scans should work.
- (is (= 5
- (position (scan (scan (scanner "test string") #"test") #"\s+"))))
- (is (= 4
- (:start (:match (scan (scan (scanner "test string")
- #"test")
- #"\s+")))))
- (is (= 5
- (:end (:match (scan (scan (scanner "test string")
- #"test")
- #"\s+")))))
- ;; Failing to match shoud leave us in the same position
- (is (= 0 (position (scan (scanner "testgoal") #"notinthestring"))))
- ;; Failing to match should remove pre-existing match data.
- (is (= nil (:match (scan (scan (scanner "test string") #"test")
- #"notinthestring")))))
-
-(deftest test-scan-until
- (is (= "goal"
- (matched (scan-until (scanner "testgoal")
- #"goal"))))
- (is (= 8 (position (scan-until (scanner "testgoal") #"goal"))))
- (is (= "goal"
- (matched (scan-until (scanner "goal") #"goal"))))
- (is (end? (scan-until (scanner "goal") #"goal")))
- (is (end? (scan-until (scanner "testgoal") #"goal")))
- (is (= ["s"]
- (groups (scan-until (scanner "test string") #"s"))))
- ;; Compounded scan-untils should work.
- (is (= 8
- (position (scan-until (scan-until (scanner "test string")
- #"s")
- #"r"))))
- (is (= 7
- (:start (:match (scan-until (scan-until (scanner "test string")
- #"s")
- #"r")))))
- (is (= 8
- (:end (:match (scan-until (scan-until (scanner "test-string")
- #"s")
- #"r")))))
- ;; Failing to match should leave us in the same position.
- (is (= 0 (position (scan-until (scanner "testgoal") #"notinthestring"))))
- ;; Failing to match should remove pre-existing match data.
- (is (= nil (:match (scan-until (scan (scanner "test string") #"test")
- #"notinthestring")))))
-
-(deftest test-skip-to-match-start
- (is (= "goal"
- (matched (skip-to-match-start (scanner "testgoal")
- #"goal"))))
- (is (= 4 (position (skip-to-match-start (scanner "testgoal") #"goal"))))
- (is (= "goal"
- (matched (skip-to-match-start (scanner "goal") #"goal"))))
- ;; Calling scan on result of skip-to-match-start should work.
- (is (= "goal"
- (matched (scan (skip-to-match-start (scanner "testgoal")
- #"goal")
- #"goal"))))
- (is (end? (scan (skip-to-match-start (scanner "testgoal")
- #"goal")
- #"goal")))
- ;; Failing to match should leave us in the same position.
- (is (= 0 (position (skip-to-match-start (scanner "testgoal") #"yes"))))
- ;; Failing to match should remove pre-existing match data.
- (is (= nil (:match (skip-to-match-start (scan (scanner "test string") #"test")
- #"notinthestring")))))
-
-;;
-;; Look-ahead tests.
-;;
-
-(deftest test-check
- (is (= "t"
- (check (scanner "test") #"t")))
- (is (= "test"
- (check (scanner "test") #"test"))))
-
-(deftest test-check-until
- (is (= "goal"
- (check-until (scanner "testgoal") #"goal")))
- (is (= "goal"
- (check-until (scanner "goal") #"goal"))))
-
-(deftest test-check-until-inclusive
- (is (= "testgoal"
- (check-until-inclusive (scanner "testgoal") #"goal")))
- (is (= "goal"
- (check-until-inclusive (scanner "goal") #"goal"))))
-
-(deftest test-peek ;; Renamed to peep here.
- (is (= "t"
- (peep (scanner "test") 1)))
- (is (= "test"
- (peep (scanner "test") 4)))
- (is (= "test"
- (peep (scanner "test") 500))))
View
2 test/stencil/test/spec.clj
@@ -66,7 +66,7 @@
;; Clear the dynamic template store to ensure a clean env.
(unregister-all-templates)
(doseq [[partial-name# partial-src#] ~partials]
- (register-template partial-name# partial-src#))
+ (register-template (name partial-name#) partial-src#))
(let [data# (compile-data-map ~data)]
(is (= ~expected
(render-string ~template data#)) ~desc))))))))
View
74 test/stencil/test/utils.clj
@@ -2,69 +2,6 @@
(:use clojure.test
stencil.utils))
-(deftest test-html-escape
- (is (= "&lt;script&gt;"
- (html-escape "<script>")))
- (is (= "&amp;lt;script&amp;gt;"
- (html-escape (html-escape "<script>"))))
- (is (= "&lt;script src=&quot;blah.js&quot;&gt;"
- (html-escape "<script src=\"blah.js\">"))))
-
-(deftest test-indent-string
- (is (= " blah\n blah")
- (indent-string "blah\nblah" " "))
- (is (= " blah\r\n blah"
- (indent-string "blah\r\nblah" " ")))
- (is (= " blah"
- (indent-string "blah" " ")))
- ;; Shouldn't indent a non-existing last line when string ends on \n.
- (is (= " blah\n"
- (indent-string "blah\n" " "))))
-
-(deftest test-contains-fuzzy?
- (is (= :a
- (contains-fuzzy? {:a 1} :a)))
- (is (= :a
- (contains-fuzzy? {:a 1} "a")))
- (is (= "a"
- (contains-fuzzy? {"a" 1} :a)))
- (is (= "a"
- (contains-fuzzy? {"a" 1} "a")))
- (is (= 1
- (contains-fuzzy? {:a 2} "b" 1))))
-
-(deftest test-get-fuzzy
- (is (= "success"
- (get-fuzzy {"test" "success"} "test")))
- (is (= "success"
- (get-fuzzy {"test" "success"} :test)))
- (is (= "success"
- (get-fuzzy {:test "success"} :test)))
- (is (= "success"
- (get-fuzzy {:test "success"} "test")))
- (is (= "failure"
- (get-fuzzy {:test "success"} "TEST" "failure"))))
-
-(deftest test-assoc-fuzzy
- (is (= {:a 1}
- (assoc-fuzzy {:a 0} :a 1)))
- (is (= {:a 1}
- (assoc-fuzzy {:a 0} "a" 1)))
- (is (= {"a" 1}
- (assoc-fuzzy {"a" 0} :a 1)))
- (is (= {"a" 1}
- (assoc-fuzzy {"a" 0} "a" 1)))
- (is (= {:a 1 :b 2}
- (assoc-fuzzy {:b 0} :a 1 "b" 2))))
-
-(deftest test-dissoc-fuzzy
- (is (= {}
- (dissoc-fuzzy {"test" 1} "test")))
- (is (= {}
- (dissoc-fuzzy {"test" 1} :test)))
- (is (= {}
- (dissoc-fuzzy {"test1" 1 :test2 2} :test1 "test2"))))
-
(deftest test-find-containing-context
(is (= {:a 1}
(find-containing-context '({:a 1}) :a)))
@@ -86,3 +23,14 @@
(is (= "failure"
(context-get '({:a "problem?"} {:a {:b "success"}})
["a" "b"] "failure"))))
+
+(deftest test-pass-context
+ (is (= "foo" (call-lambda (fn [] "foo") nil)))
+ (is (= "foo*bar" (call-lambda ^{:stencil/pass-context true}
+ (fn [ctx] (str "foo*" (:addition ctx)))
+ {:addition "bar"})))
+ (is (= "foo*" (call-lambda (fn [x] (str x "*")) "foo" nil)))
+ (is (= "foo*bar"
+ (call-lambda ^{:stencil/pass-context true}
+ (fn [x ctx] (str x "*" (:second-arg ctx)))
+ "foo" {:second-arg "bar"}))))

0 comments on commit 595efaf

Please sign in to comment.
Something went wrong with that request. Please try again.