diff --git a/README.md b/README.md
--- a/README.md
+++ b/README.md
@@ -4,11 +4,12 @@
 be a module in pandoc. It has been split off to make it easier
 to use independently.
 
-Example:
+## Example of use
 
 ``` haskell
 {-# LANGUAGE OverloadedStrings #-}
-import Data.Text
+import Data.Text (Text)
+import qualified Data.Text.IO as T
 import Data.Aeson
 import Text.DocTemplates
 
@@ -23,9 +24,12 @@
 template :: Text
 template = "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"
 
-main = case compileTemplate template of
+main :: IO ()
+main = do
+  res <- compileTemplate "mytemplate.txt" template
+  case res of
          Left e    -> error e
-         Right t   -> putStrLn $ renderTemplate t $ object
+         Right t   -> T.putStrLn $ renderTemplate t $ object
                         ["employee" .=
                           [ Employee "John" "Doe" Nothing
                           , Employee "Omar" "Smith" (Just 30000)
@@ -33,34 +37,204 @@
                         ]
 ```
 
+## Delimiters
+
+To mark variables and control structures in the template,
+either `$`...`$` or `${`...`}` may be used as delimiters.
+The styles may also be mixed in the same template, but the
+opening and closing delimiter must match in each case.  The
+opening delimiter may be followed by one or more spaces
+or tabs, which will be ignored. The closing delimiter may
+be followed by one or more spaces or tabs, which will be
+ignored.
+
+To include a literal `$` in the document, use `$$`.
+
+## Comments
+
+Anything between the sequence `$--` and the end of the
+line will be treated as a comment and omitted from the output.
+
+## Interpolated variables
+
 A slot for an interpolated variable is a variable name surrounded
-by dollar signs.  To include a literal `$` in your template, use
-`$$`.  Variable names must begin with a letter and can contain letters,
-numbers, `_`, `-`, and `.`.
+by matched delimiters.  Variable names must begin with a letter
+and can contain letters, numbers, `_`, `-`, and `.`.  The
+keywords `it`, `if`, `else`, `endif`, `for`, `sep`, and `endfor` may
+not be used as variable names. Examples:
 
+```
+$foo$
+$foo.bar.baz$
+$foo_bar.baz-bim$
+$ foo $
+${foo}
+${foo.bar.baz}
+${foo_bar.baz-bim}
+${ foo }
+```
+
 The values of variables are determined by a JSON object that is
 passed as a parameter to `renderTemplate`.  So, for example,
 `title` will return the value of the `title` field, and
 `employee.salary` will return the value of the `salary` field
 of the object that is the value of the `employee` field.
 
+- If the value of the variable is a JSON string, the string will
+  be rendered verbatim.  (Note that no escaping is done on the
+  string; the assumption is that the calling program will escape
+  the strings appropriately for the output format.)
+- If the value is a JSON array, the values will be concatenated.
+- If the value is a JSON object, the string `true` will be
+  rendered.
+- If the value is a JSON number, it will be rendered as an
+  integer if possible, otherwise as a floating-point number.
+- If the value is a JSON boolean, it will be rendered as `true`
+  if true, and as the empty string if false.
+- Every other value will be rendered as the empty string.
+
 The value of a variable will be indented to the same level as the
-variable.
+opening delimiter of the variable.
 
-A conditional begins with `$if(variable_name)$` and ends with `$endif$`.
-It may optionally contain an `$else$` section.  The if section is
-used if `variable_name` has a non-null value, otherwise the else section
-is used.
+## Conditionals
 
+A conditional begins with `if(variable)` (enclosed in
+matched delimiters) and ends with `endif` (enclosed in matched
+delimiters).  It may optionally contain an `else` (enclosed in
+matched delimiters).  The `if` section is used if
+`variable` has a non-empty value, otherwise the `else`
+section is used (if present).  (Note that even the
+string `false` counts as a true value.) Examples:
+
+```
+$if(foo)$bar$endif$
+
+$if(foo)$
+  $foo$
+$endif$
+
+$if(foo)$
+part one
+$else$
+part two
+$endif$
+
+${if(foo)}bar${endif}
+
+${if(foo)}
+  ${foo}
+${endif}
+
+${if(foo)}
+${ foo.bar }
+${else}
+no foo!
+${endif}
+```
+
 Conditional keywords should not be indented, or unexpected spacing
 problems may occur.
 
-The `$for$` keyword can be used to iterate over an array.  If
-the value of the associated variable is not an array, a single
-iteration will be performed on its value.
+## For loops
 
-You may optionally specify separators using `$sep$`, as in the
-example above.
+A for loop begins with `for(variable)` (enclosed in
+matched delimiters) and ends with `endfor` (enclosed in matched
+delimiters.  If `variable` is an array, the material inside
+the loop will be evaluated repeatedly, with `variable` being set
+to each value of the array in turn.  If the value of the
+associated variable is not an array, a single iteration will be
+performed on its value.
 
-Anything between the sequence `$--` and the end of the line
-will be treated as a comment.
+Examples:
+
+```
+$for(foo)$$foo$$sep$, $endfor$
+
+$for(foo)$
+  - $foo.last$, $foo.first$
+$endfor$
+
+${ for(foo.bar) }
+  - ${ foo.bar.last }, ${ foo.bar.first }
+${ endfor }
+```
+
+You may optionally specify a separator between consecutive
+values using `sep` (enclosed in matched delimiters).  The
+material between `sep` and the `endfor` is the separator.
+
+```
+${ for(foo) }${ foo }${ sep }, ${ endfor }
+```
+
+Instead of using `variable` inside the loop, the special
+anaphoric keyword `it` may be used.
+
+```
+${ for(foo.bar) }
+  - ${ it.last }, ${ it.first }
+${ endfor }
+```
+
+## Partials
+
+Partials (subtemplates stored in different files) may be
+included using the syntax
+
+```
+${ boilerplate() }
+```
+
+The partials are obtained using `getPartial` from
+the `TemplateMonad` class.  This may be implemented
+differently in different monads.  The path passed
+to `getPartial` is computed on the basis of the
+original template path (a parameter to `compileTemplate`)
+and the partial's name.  The partial's name is substituted
+for the *base name* of the original template path
+(leaving the original template's extension), unless
+the partial has an explicit extension, in which case
+this is kept.  So, with the `TemplateMonad` instance
+for IO, partials will be sought in the directory
+containing the main template, and will be assumed
+to have the extension of the main template.
+
+Partials may optionally be applied to variables using
+a colon:
+
+```
+${ date:fancy() }
+
+${ articles:bibentry() }
+```
+
+If `articles` is an array, this will iterate over its
+values, applying the partial `bibentry()` to each one.
+So the second example above is equivalent to
+
+```
+${ for(articles) }
+${ it:bibentry() }
+${ endfor }
+```
+
+Final newlines are omitted from included partials.
+
+Partials may include other partials.  If you exceed
+a nesting level of 50, though, in resolving partials,
+the literal `(loop)` will be returned, to avoid infinite loops.
+
+A separator between values of an array may be specified
+in square brackets, immediately after the variable name
+or partial:
+
+```
+${months[, ]}$
+
+${articles:bibentry()[; ]$
+```
+
+The separator in this case is literal and (unlike with `sep`
+in an explicit `for` loop) cannot contain interpolated
+variables or other template directives.
+
diff --git a/changelog.md b/changelog.md
new file mode 100644
--- /dev/null
+++ b/changelog.md
@@ -0,0 +1,49 @@
+# doctemplates
+
+## 0.3
+
+* Note that all of the changes to template syntax described
+  below are backwards compatible, and all old pandoc templates
+  should continue to work as before.
+
+* Allow `${...}` style delimiters around variables and
+  directives, in addition to `$...$`.  Allow space around
+  the delimiters.
+
+* Support `$it$` as a variable for the current value in
+  an iteration.  (The old method, where the containing
+  variable name is used, still works.)
+
+* Support partials (subtemplates defined in different files).
+
+* Interpolated array variables now have all elements rendered,
+  concatenated, with an optional separator that can be
+  specified using a new bracketed syntax.
+
+* Remove `TemplateTarget` class.  It was pointless; the
+  calling program can just do these trivial transformations.
+  Avoids dependencies on bytestring, blaze-html, blaze-markup.
+
+* Change type of `renderTemplate` and `applyTemplate` to produce
+  a `Text`, instead of being polymorphic.
+
+* Changed type of `compileTemplate`: it now takes a
+  template path and the template contents, and returns
+  either a template or an error.  It runs in an instance
+  of `TemplateMonad`, which is an abstraction around different
+  ways of getting partials.  (For example, in IO we can get
+  partials by reading them from a file system, but in a
+  web application one might want to obtain them from the
+  database or have a set of them baked in.)
+
+* Remove `varListToJSON`.
+
+* Changed the architecture: `Template` is no longer just
+  a newtype around a function, but a list of `TemplatePart`s.
+
+* Added a newtype for `Variable`.
+
+* Improved documentation in README.md.
+
+* Added a new test framework and much more extensive tests.
+
diff --git a/doctemplates.cabal b/doctemplates.cabal
--- a/doctemplates.cabal
+++ b/doctemplates.cabal
@@ -1,5 +1,5 @@
 name:                doctemplates
-version:             0.2.2.1
+version:             0.3
 synopsis:            Pandoc-style document templates
 description:         A simple text templating system used by pandoc.
 homepage:            https://github.com/jgm/doctemplates#readme
@@ -12,6 +12,10 @@
 build-type:          Simple
 -- extra-source-files:
 data-files:          README.md
+                     changelog.md
+extra-source-files:  test/*.test
+                     test/*.txt
+                     test/*.tex
 cabal-version:       >=1.10
 
 library
@@ -19,13 +23,12 @@
   exposed-modules:     Text.DocTemplates
   build-depends:       base >= 4.7 && < 5,
                        aeson,
-                       bytestring,
-                       blaze-markup,
-                       blaze-html,
                        text,
                        containers,
                        vector,
+                       filepath,
                        parsec,
+                       mtl,
                        unordered-containers,
                        scientific
   if !impl(ghc >= 8.0)
@@ -36,11 +39,18 @@
 test-suite doctemplates-test
   type:                exitcode-stdio-1.0
   hs-source-dirs:      test
-  main-is:             Spec.hs
+  main-is:             test.hs
   build-depends:       base,
                        doctemplates,
+                       mtl,
                        aeson,
-                       hspec,
+                       Glob,
+                       tasty,
+                       tasty-golden,
+                       tasty-hunit,
+                       filepath,
+                       temporary,
+                       bytestring,
                        text
   ghc-options:         -threaded -rtsopts -with-rtsopts=-N
   default-language:    Haskell2010
diff --git a/src/Text/DocTemplates.hs b/src/Text/DocTemplates.hs
--- a/src/Text/DocTemplates.hs
+++ b/src/Text/DocTemplates.hs
@@ -1,5 +1,11 @@
-{-# LANGUAGE TypeSynonymInstances, FlexibleInstances,
-    OverloadedStrings, GeneralizedNewtypeDeriving, ScopedTypeVariables #-}
+{-# LANGUAGE CPP #-}
+{-# LANGUAGE TypeSynonymInstances #-}
+{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE FlexibleInstances #-}
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+{-# LANGUAGE DeriveDataTypeable #-}
+{-# LANGUAGE DeriveGeneric #-}
 {- |
    Module      : Text.Pandoc.Templates
    Copyright   : Copyright (C) 2009-2016 John MacFarlane
@@ -9,12 +15,15 @@
    Stability   : alpha
    Portability : portable
 
-A simple templating system with variable substitution and conditionals.
-This module was formerly part of pandoc and is used for pandoc's
-templates.  The following program illustrates its use:
+This is the templating system used by pandoc. It was formerly be a
+module in pandoc. It has been split off to make it easier to use
+independently.
 
+== Example of use
+
 > {-# LANGUAGE OverloadedStrings #-}
-> import Data.Text
+> import Data.Text (Text)
+> import qualified Data.Text.IO as T
 > import Data.Aeson
 > import Text.DocTemplates
 >
@@ -29,223 +38,468 @@
 > template :: Text
 > template = "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"
 >
-> main = case compileTemplate template of
+> main :: IO ()
+> main = do
+>   res <- compileTemplate "mytemplate.txt" template
+>   case res of
 >          Left e    -> error e
->          Right t   -> putStrLn $ renderTemplate t $ object
+>          Right t   -> T.putStrLn $ renderTemplate t $ object
 >                         ["employee" .=
 >                           [ Employee "John" "Doe" Nothing
 >                           , Employee "Omar" "Smith" (Just 30000)
 >                           , Employee "Sara" "Chen" (Just 60000) ]
 >                         ]
 
-A slot for an interpolated variable is a variable name surrounded
-by dollar signs.  To include a literal @$@ in your template, use
-@$$@.  Variable names must begin with a letter and can contain letters,
-numbers, @_@, @-@, and @.@.
+== Delimiters
 
-The values of variables are determined by a JSON object that is
-passed as a parameter to @renderTemplate@.  So, for example,
-@title@ will return the value of the @title@ field, and
-@employee.salary@ will return the value of the @salary@ field
-of the object that is the value of the @employee@ field.
+To mark variables and control structures in the template, either @$@…@$@
+or @${@…@}@ may be used as delimiters. The styles may also be mixed in
+the same template, but the opening and closing delimiter must match in
+each case. The opening delimiter may be followed by one or more spaces
+or tabs, which will be ignored. The closing delimiter may be followed by
+one or more spaces or tabs, which will be ignored.
 
+To include a literal @$@ in the document, use @$$@.
+
+== Comments
+
+Anything between the sequence @$--@ and the end of the line will be
+treated as a comment and omitted from the output.
+
+== Interpolated variables
+
+A slot for an interpolated variable is a variable name surrounded by
+matched delimiters. Variable names must begin with a letter and can
+contain letters, numbers, @_@, @-@, and @.@. The keywords @it@, @if@,
+@else@, @endif@, @for@, @sep@, and @endfor@ may not be used as variable
+names. Examples:
+
+> $foo$
+> $foo.bar.baz$
+> $foo_bar.baz-bim$
+> $ foo $
+> ${foo}
+> ${foo.bar.baz}
+> ${foo_bar.baz-bim}
+> ${ foo }
+
+The values of variables are determined by a JSON object that is passed
+as a parameter to @renderTemplate@. So, for example, @title@ will return
+the value of the @title@ field, and @employee.salary@ will return the
+value of the @salary@ field of the object that is the value of the
+@employee@ field.
+
+-   If the value of the variable is a JSON string, the string will be
+    rendered verbatim. (Note that no escaping is done on the string; the
+    assumption is that the calling program will escape the strings
+    appropriately for the output format.)
+-   If the value is a JSON array, the values will be concatenated.
+-   If the value is a JSON object, the string @true@ will be rendered.
+-   If the value is a JSON number, it will be rendered as an integer if
+    possible, otherwise as a floating-point number.
+-   If the value is a JSON boolean, it will be rendered as @true@ if
+    true, and as the empty string if false.
+-   Every other value will be rendered as the empty string.
+
 The value of a variable will be indented to the same level as the
-variable.
+opening delimiter of the variable.
 
-A conditional begins with @$if(variable_name)$@ and ends with @$endif$@.
-It may optionally contain an @$else$@ section.  The if section is
-used if @variable_name@ has a non-null value, otherwise the else section
-is used.
+== Conditionals
 
+A conditional begins with @if(variable)@ (enclosed in matched
+delimiters) and ends with @endif@ (enclosed in matched delimiters). It
+may optionally contain an @else@ (enclosed in matched delimiters). The
+@if@ section is used if @variable@ has a non-empty value, otherwise the
+@else@ section is used (if present). (Note that even the string @false@
+counts as a true value.) Examples:
+
+> $if(foo)$bar$endif$
+>
+> $if(foo)$
+>   $foo$
+> $endif$
+>
+> $if(foo)$
+> part one
+> $else$
+> part two
+> $endif$
+>
+> ${if(foo)}bar${endif}
+>
+> ${if(foo)}
+>   ${foo}
+> ${endif}
+>
+> ${if(foo)}
+> ${ foo.bar }
+> ${else}
+> no foo!
+> ${endif}
+
 Conditional keywords should not be indented, or unexpected spacing
 problems may occur.
 
-The @$for$@ keyword can be used to iterate over an array.  If
-the value of the associated variable is not an array, a single
-iteration will be performed on its value.
+== For loops
 
-You may optionally specify separators using @$sep$@, as in the
-example above.
+A for loop begins with @for(variable)@ (enclosed in matched delimiters)
+and ends with @endfor@ (enclosed in matched delimiters. If @variable@ is
+an array, the material inside the loop will be evaluated repeatedly,
+with @variable@ being set to each value of the array in turn. If the
+value of the associated variable is not an array, a single iteration
+will be performed on its value.
 
+Examples:
+
+> $for(foo)$$foo$$sep$, $endfor$
+>
+> $for(foo)$
+>   - $foo.last$, $foo.first$
+> $endfor$
+>
+> ${ for(foo.bar) }
+>   - ${ foo.bar.last }, ${ foo.bar.first }
+> ${ endfor }
+
+You may optionally specify a separator between consecutive values using
+@sep@ (enclosed in matched delimiters). The material between @sep@ and
+the @endfor@ is the separator.
+
+> ${ for(foo) }${ foo }${ sep }, ${ endfor }
+
+Instead of using @variable@ inside the loop, the special anaphoric
+keyword @it@ may be used.
+
+> ${ for(foo.bar) }
+>   - ${ it.last }, ${ it.first }
+> ${ endfor }
+
+== Partials
+
+Partials (subtemplates stored in different files) may be included using
+the syntax
+
+> ${ boilerplate() }
+
+The partials are obtained using @getPartial@ from the @TemplateMonad@
+class. This may be implemented differently in different monads. The path
+passed to @getPartial@ is computed on the basis of the original template
+path (a parameter to @compileTemplate@) and the partial’s name. The
+partial’s name is substituted for the /base name/ of the original
+template path (leaving the original template’s extension), unless the
+partial has an explicit extension, in which case this is kept. So, with
+the @TemplateMonad@ instance for IO, partials will be sought in the
+directory containing the main template, and will be assumed to have the
+extension of the main template.
+
+Partials may optionally be applied to variables using a colon:
+
+> ${ date:fancy() }
+>
+> ${ articles:bibentry() }
+
+If @articles@ is an array, this will iterate over its values, applying
+the partial @bibentry()@ to each one. So the second example above is
+equivalent to
+
+> ${ for(articles) }
+> ${ it:bibentry() }
+> ${ endfor }
+
+Final newlines are omitted from included partials.
+
+Partials may include other partials. If you exceed a nesting level of
+50, though, in resolving partials, the literal @(loop)@ will be
+returned, to avoid infinite loops.
+
+A separator between values of an array may be specified in square
+brackets, immediately after the variable name or partial:
+
+> ${months[, ]}$
+>
+> ${articles:bibentry()[; ]$
+
+The separator in this case is literal and (unlike with @sep@ in an
+explicit @for@ loop) cannot contain interpolated variables or other
+template directives.
+
 -}
 
 module Text.DocTemplates ( renderTemplate
-                         , applyTemplate
-                         , TemplateTarget(..)
-                         , varListToJSON
                          , compileTemplate
-                         , Template
+                         , applyTemplate
+                         , TemplateMonad(..)
+                         , Template(..)
+                         , TemplatePart(..)
+                         , Variable(..)
                          ) where
 
 import Data.Char (isAlphaNum)
 import Control.Monad (guard, when)
-import Data.Aeson (ToJSON(..), Value(..))
+import Data.Aeson (Value(..), ToJSON(..))
 import qualified Text.Parsec as P
-import Text.Parsec.Text (Parser)
-import qualified Data.Set as Set
-import Data.Monoid
+import qualified Text.Parsec.Pos as P
+import Control.Monad.Except
+import Control.Exception
+import System.IO.Error (isDoesNotExistError)
+import Control.Monad.State
+import Control.Monad.Identity
 import Control.Applicative
 import qualified Data.Text as T
+import qualified Data.Text.IO as TIO
+import Data.Data (Data)
+import Data.Typeable (Typeable)
+import GHC.Generics (Generic)
 import Data.Text (Text)
-import Data.Text.Encoding (encodeUtf8)
 import Data.List (intersperse)
-import qualified Data.Map as M
 import qualified Data.HashMap.Strict as H
 import Data.Foldable (toList)
-import Text.Blaze.Html (Html)
-import Text.Blaze.Internal (preEscapedText)
-import Data.ByteString.Lazy (ByteString, fromChunks)
-import Data.Vector ((!?))
+import qualified Data.Vector as V
 import Data.Scientific (floatingOrInteger)
-import Data.Semigroup (Semigroup)
+import Data.Semigroup (Semigroup, (<>))
+import System.FilePath
 
--- | A 'Template' is essentially a function that takes
--- a JSON 'Value' and produces 'Text'.
-newtype Template = Template { unTemplate :: Value -> Text }
-                 deriving (Semigroup, Monoid)
+newtype Template = Template { unTemplate :: [TemplatePart] }
+     deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
 
-type Variable = [Text]
+#if MIN_VERSION_base(4,11,0)
+instance Semigroup Template where
+  Template xs <> Template ys = Template (xs <> ys)
 
-class TemplateTarget a where
-  toTarget :: Text -> a
+instance Monoid Template where
+  mempty = Template []
+#else
+instance Monoid Template where
+  mappend (Template xs) (Template ys) = Template (mappend xs ys)
+  mempty = Template []
+#endif
 
-instance TemplateTarget Text where
-  toTarget = id
+data TemplatePart =
+       Interpolate Variable
+     | Conditional Variable Template Template
+     | Iterate Variable Template Template
+     | Partial Template
+     | Literal Text
+     deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
 
-instance TemplateTarget String where
-  toTarget = T.unpack
+newtype Variable = Variable { unVariable :: [Text] }
+  deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
 
-instance TemplateTarget ByteString where
-  toTarget = fromChunks . (:[]) . encodeUtf8
+#if MIN_VERSION_base(4,11,0)
+instance Semigroup Variable where
+  Variable xs <> Variable ys = Variable (xs <> ys)
 
-instance TemplateTarget Html where
-  toTarget = preEscapedText
+instance Monoid Variable where
+  mempty = Variable []
+#else
+instance Monoid Variable where
+  mappend (Variable xs) (Variable ys) = Variable (mappend xs ys)
+  mempty = Variable []
+#endif
 
--- | A convenience function for passing in an association
--- list of string values instead of a JSON 'Value'.
-varListToJSON :: [(String, String)] -> Value
-varListToJSON assoc = toJSON $ M.fromList assoc'
-  where assoc' = [(T.pack k, toVal [T.pack z | (y,z) <- assoc,
-                                                not (null z),
-                                                y == k])
-                        | k <- ordNub $ map fst assoc ]
-        toVal [x] = toJSON x
-        toVal []  = Null
-        toVal xs  = toJSON xs
+renderTemplate :: ToJSON a => Template -> a -> Text
+renderTemplate t context = evalState (renderer t (toJSON context)) 0
 
--- An efficient specialization of nub.
-ordNub :: (Ord a) => [a] -> [a]
-ordNub l = go Set.empty l
+it :: Variable
+it = Variable ["it"]
+
+renderer :: Template -> Value -> State Int Text
+renderer (Template xs) val = mconcat <$> mapM renderPart xs
   where
-    go _ [] = []
-    go s (x:xs) = if x `Set.member` s then go s xs
-                                      else x : go (Set.insert x s) xs
+   modifyIndent t = do
+     ind <- get
+     put $ T.foldl' (\cur c ->
+                 case c of
+                   '\n' -> 0
+                   _    -> cur + 1) ind t
+   renderPart x =
+     case x of
+       Literal t -> do
+         modifyIndent t
+         return t
+       Interpolate v -> do
+         ind <- get
+         let t = indent ind $ resolveVar v val
+         modifyIndent t
+         return t
+       Conditional v ift elset -> renderer branch val
+         where branch = case resolveVar v val of
+                          "" -> elset
+                          _  -> ift
+       Iterate v t sep ->
+         case multiLookup (unVariable v) val of
+           Just (Array vec) -> do
+             sep' <- renderer sep val
+             iters <- mapM (\iterval -> renderer t .
+                                replaceVar v iterval .
+                                replaceVar it iterval $
+                                val) (toList vec)
+             return $ mconcat $ intersperse sep' iters
+           Just val' -> renderer t $ replaceVar it val' val
+           Nothing -> return mempty
+       Partial t -> renderer t val
 
--- | Compile a template.
-compileTemplate :: Text -> Either String Template
-compileTemplate template =
-  case P.parse (pTemplate <* P.eof) "template" template of
-       Left e   -> Left (show e)
-       Right x  -> Right x
+class Monad m => TemplateMonad m where
+  getPartial  :: FilePath -> Parser m Text
 
--- | Render a compiled template using @context@ to resolve variables.
-renderTemplate :: (ToJSON a, TemplateTarget b) => Template -> a -> b
-renderTemplate (Template f) context = toTarget $ f $ toJSON context
+instance TemplateMonad Identity where
+  getPartial s  = fail $ "Could not get partial: " <> s
 
--- | Combines `renderTemplate` and `compileTemplate`.
-applyTemplate :: (ToJSON a, TemplateTarget b) => Text -> a -> Either String b
-applyTemplate t context =
-  case compileTemplate t of
-         Left e   -> Left e
-         Right f  -> Right $ renderTemplate f context
+instance TemplateMonad IO where
+  getPartial s  = do
+    res <- liftIO $ tryJust (guard . isDoesNotExistError)
+              (TIO.readFile s)
+    case res of
+      Left _  -> fail $ "Could not get partial " ++ s
+      Right x -> return x
 
-var :: Variable -> Template
-var = Template . resolveVar
+compileTemplate :: TemplateMonad m
+                => FilePath -> Text -> m (Either String Template)
+compileTemplate templPath template = do
+  res <- P.runParserT (pTemplate <* P.eof)
+           PState{ templatePath   = templPath
+                 , partialNesting = 1 } "template" template
+  case res of
+       Left e   -> return $ Left $ show e
+       Right x  -> return $ Right x
 
-resolveVar :: Variable -> Value -> Text
-resolveVar var' val =
-  case multiLookup var' val of
-       Just (Array vec) -> maybe mempty (resolveVar []) $ vec !? 0
-       Just (String t)  -> T.stripEnd t
-       Just (Number n)  -> case floatingOrInteger n of
-                                   Left (r :: Double)   -> T.pack $ show r
-                                   Right (i :: Integer) -> T.pack $ show i
-       Just (Bool True) -> "true"
-       Just (Object _)  -> "true"
-       Just _           -> mempty
-       Nothing          -> mempty
+applyTemplate :: (TemplateMonad m, ToJSON a)
+              => FilePath -> Text -> a -> m (Either String Text)
+applyTemplate fp t val =
+  fmap (`renderTemplate` val) <$> compileTemplate fp t
 
-multiLookup :: [Text] -> Value -> Maybe Value
-multiLookup [] x = Just x
-multiLookup (v:vs) (Object o) = H.lookup v o >>= multiLookup vs
-multiLookup _ _ = Nothing
+data PState =
+  PState { templatePath   :: FilePath
+         , partialNesting :: Int }
 
-lit :: Text -> Template
-lit = Template . const
+type Parser = P.ParsecT Text PState
 
-cond :: Variable -> Template -> Template -> Template
-cond var' (Template ifyes) (Template ifno) = Template $ \val ->
-  case resolveVar var' val of
-       "" -> ifno val
-       _  -> ifyes val
+pTemplate :: TemplateMonad m => Parser m Template
+pTemplate = do
+  ts <- many $ P.try
+         (P.skipMany pComment *> (pLit <|> pDirective <|> pEscape))
+  P.skipMany pComment
+  return $ Template ts
 
-iter :: Variable -> Template -> Template -> Template
-iter var' template sep = Template $ \val -> unTemplate
-  (case multiLookup var' val of
-           Just (Array vec) -> mconcat $ intersperse sep
-                                       $ map (setVar template var')
-                                       $ toList vec
-           Just x           -> cond var' (setVar template var' x) mempty
-           Nothing          -> mempty) val
+pLit :: Monad m => Parser m TemplatePart
+pLit = Literal . mconcat <$>
+  P.many1 (T.pack <$> P.many1 (P.satisfy (/= '$')))
 
-setVar :: Template -> Variable -> Value -> Template
-setVar (Template f) var' val = Template $ f . replaceVar var' val
+backupSourcePos :: Monad m => Int -> Parser m ()
+backupSourcePos n = do
+  pos <- P.getPosition
+  P.setPosition $ P.incSourceColumn pos (- n)
 
-replaceVar :: Variable -> Value -> Value -> Value
-replaceVar []     new _          = new
-replaceVar (v:vs) new (Object o) =
-  Object $ H.adjust (\x -> replaceVar vs new x) v o
-replaceVar _ _ old = old
+pEscape :: Monad m => Parser m TemplatePart
+pEscape = Literal "$" <$ P.try (P.string "$$" <* backupSourcePos 1)
 
---- parsing
+pDirective :: TemplateMonad m => Parser m TemplatePart
+pDirective = pConditional <|> pForLoop <|> pInterpolate <|> pBarePartial
 
-pTemplate :: Parser Template
-pTemplate = do
-  sp <- P.option mempty pInitialSpace
-  rest <- mconcat <$> many (pConditional <|>
-                            pFor <|>
-                            pNewline <|>
-                            pVar <|>
-                            pComment <|>
-                            pLit <|>
-                            pEscapedDollar)
-  return $ sp <> rest
+pEnclosed :: Monad m => Parser m a -> Parser m a
+pEnclosed parser = P.try $ do
+  closer <- pOpen
+  P.skipMany pSpaceOrTab
+  result <- parser
+  P.skipMany pSpaceOrTab
+  closer
+  return result
 
-takeWhile1 :: (Char -> Bool) -> Parser Text
-takeWhile1 f = T.pack <$> P.many1 (P.satisfy f)
+pParens :: Monad m => Parser m a -> Parser m a
+pParens parser = do
+  P.char '('
+  result <- parser
+  P.char ')'
+  return result
 
-pLit :: Parser Template
-pLit = lit <$> takeWhile1 (\x -> x /='$' && x /= '\n')
+pConditional :: TemplateMonad m => Parser m TemplatePart
+pConditional = do
+  v <- pEnclosed $ P.try $ P.string "if" *> pParens pVar
+  -- if newline after the "if", then a newline after "endif" will be swallowed
+  multiline <- P.option False (True <$ skipEndline)
+  ifContents <- pTemplate
+  elseContents <- P.option mempty $
+                    do pEnclosed (P.string "else")
+                       when multiline $ P.option () skipEndline
+                       pTemplate
+  pEnclosed (P.string "endif")
+  when multiline $ P.option () skipEndline
+  return $ Conditional v ifContents elseContents
 
-pNewline :: Parser Template
-pNewline = do
-  P.char '\n'
-  sp <- P.option mempty pInitialSpace
-  return $ lit "\n" <> sp
+skipEndline :: Monad m => Parser m ()
+skipEndline = P.try $ P.skipMany pSpaceOrTab <* P.char '\n'
 
-pInitialSpace :: Parser Template
-pInitialSpace = do
-  sps <- takeWhile1 (==' ')
-  let indentVar = if T.null sps
-                     then id
-                     else indent (T.length sps)
-  v <- P.option mempty $ indentVar <$> pVar
-  return $ lit sps <> v
+pForLoop :: TemplateMonad m => Parser m TemplatePart
+pForLoop = do
+  v <- pEnclosed $ P.try $ P.string "for" *> pParens pVar
+  -- if newline after the "for", then a newline after "endfor" will be swallowed
+  multiline <- P.option False $ skipEndline >> return True
+  contents <- pTemplate
+  sep <- P.option mempty $
+           do pEnclosed (P.string "sep")
+              when multiline $ P.option () skipEndline
+              pTemplate
+  pEnclosed (P.string "endfor")
+  when multiline $ P.option () skipEndline
+  return $ Iterate v contents sep
 
-pEscapedDollar :: Parser Template
-pEscapedDollar = lit "$" <$ P.try (P.string "$$")
+pInterpolate :: TemplateMonad m => Parser m TemplatePart
+pInterpolate = pEnclosed $ do
+  var <- pVar
+  (P.char ':' *> pPartial (Just var))
+    <|> do separ <- pSep
+           return (Iterate var (Template [Interpolate it])
+                    separ)
+    <|> return (Interpolate var)
 
-pComment :: Parser Template
+pBarePartial :: TemplateMonad m => Parser m TemplatePart
+pBarePartial = pEnclosed $ pPartial Nothing
+
+pPartial :: TemplateMonad m => Maybe Variable -> Parser m TemplatePart
+pPartial mbvar = do
+  fp <- P.many1 (P.alphaNum <|> P.oneOf ['_','-','.','/','\\'])
+  P.string "()"
+  separ <- P.option mempty pSep
+  tp <- templatePath <$> P.getState
+  let fp' = case takeExtension fp of
+               "" -> replaceBaseName tp fp
+               _  -> replaceFileName tp fp
+  partial <- removeFinalNewline <$> getPartial fp'
+  nesting <- partialNesting <$> P.getState
+  t <- if nesting > 50
+          then return $ Template [Literal "(loop)"]
+          else do
+            oldInput <- P.getInput
+            oldPos <- P.getPosition
+            P.setPosition $ P.initialPos fp
+            P.setInput partial
+            P.updateState $ \st -> st{ partialNesting = nesting + 1 }
+            res <- pTemplate <* P.eof
+            P.updateState $ \st -> st{ partialNesting = nesting }
+            P.setInput oldInput
+            P.setPosition oldPos
+            return res
+  case mbvar of
+    Just var -> return $ Iterate var t separ
+    Nothing  -> return $ Partial t
+
+pSep :: Monad m => Parser m Template
+pSep = do
+    P.char '['
+    xs <- P.many (P.satisfy (/= ']'))
+    P.char ']'
+    return $ Template [Literal (T.pack xs)]
+
+removeFinalNewline :: Text -> Text
+removeFinalNewline t =
+  case T.unsnoc t of
+    Just (t', '\n') -> t'
+    _               -> t
+
+pSpaceOrTab :: Monad m => Parser m Char
+pSpaceOrTab = P.satisfy (\c -> c == ' ' || c == '\t')
+
+pComment :: Monad m => Parser m ()
 pComment = do
   pos <- P.getPosition
   P.try (P.string "$--")
@@ -253,65 +507,70 @@
   -- If the comment begins in the first column, the line ending
   -- will be consumed; otherwise not.
   when (P.sourceColumn pos == 1) $ () <$ P.char '\n'
-  return mempty
 
-pVar :: Parser Template
-pVar = var <$> (P.try $ P.char '$' *> pIdent <* P.char '$')
+pOpenDollar :: Monad m => Parser m (Parser m ())
+pOpenDollar =
+  pCloseDollar <$ P.try (P.char '$' <*
+                   P.notFollowedBy (P.char '$' <|> P.char '{'))
+  where
+   pCloseDollar = () <$ P.char '$'
 
-pIdent :: Parser [Text]
-pIdent = do
-  first <- pIdentPart
-  rest <- many (P.char '.' *> pIdentPart)
-  return (first:rest)
+pOpenBraces :: Monad m => Parser m (Parser m ())
+pOpenBraces =
+  pCloseBraces <$ P.try (P.string "${" <* P.notFollowedBy (P.char '}'))
+  where
+   pCloseBraces = () <$ P.try (P.char '}')
 
-pIdentPart :: Parser Text
+pOpen :: Monad m => Parser m (Parser m ())
+pOpen = pOpenDollar <|> pOpenBraces
+
+pVar :: Monad m => Parser m Variable
+pVar = do
+  first <- pIdentPart <|> "it" <$ P.try (P.string "it")
+  rest <- P.many $ P.char '.' *> pIdentPart
+  return $ Variable (first:rest)
+
+pIdentPart :: Monad m => Parser m Text
 pIdentPart = P.try $ do
   first <- P.letter
-  rest <- T.pack <$> P.many (P.satisfy (\c -> isAlphaNum c || c == '_' || c == '-'))
-  let id' = T.singleton first <> rest
-  guard $ id' `notElem` reservedWords
-  return id'
+  rest <- T.pack <$>
+            P.many (P.satisfy (\c -> isAlphaNum c || c == '_' || c == '-'))
+  let part = T.singleton first <> rest
+  guard $ part `notElem` reservedWords
+  return part
 
 reservedWords :: [Text]
-reservedWords = ["else","endif","for","endfor","sep"]
+reservedWords = ["else","endif","for","endfor","sep","it"]
 
-skipEndline :: Parser ()
-skipEndline = P.try $ P.skipMany (P.satisfy (`elem` (" \t" :: String))) >> P.char '\n' >> return ()
+resolveVar :: Variable -> Value -> Text
+resolveVar (Variable var') val =
+  case multiLookup var' val of
+       Just (Array vec) -> mconcat $ map (resolveVar mempty) $ V.toList vec
+       Just (String t)  -> T.stripEnd t
+       Just (Number n)  -> case floatingOrInteger n of
+                                   Left (r :: Double)   -> T.pack $ show r
+                                   Right (i :: Integer) -> T.pack $ show i
+       Just (Bool True) -> "true"
+       Just (Object _)  -> "true"
+       Just _           -> mempty
+       Nothing          -> mempty
 
-pConditional :: Parser Template
-pConditional = do
-  P.try $ P.string "$if("
-  id' <- pIdent
-  P.string ")$"
-  -- if newline after the "if", then a newline after "endif" will be swallowed
-  multiline <- P.option False (True <$ skipEndline)
-  ifContents <- pTemplate
-  elseContents <- P.option mempty $ P.try $
-                      do P.string "$else$"
-                         when multiline $ P.option () skipEndline
-                         pTemplate
-  P.string "$endif$"
-  when multiline $ P.option () skipEndline
-  return $ cond id' ifContents elseContents
+multiLookup :: [Text] -> Value -> Maybe Value
+multiLookup [] x = Just x
+multiLookup (v:vs) (Object o) = H.lookup v o >>= multiLookup vs
+multiLookup _ _ = Nothing
 
-pFor :: Parser Template
-pFor = do
-  P.try $ P.string "$for("
-  id' <- pIdent
-  P.string ")$"
-  -- if newline after the "for", then a newline after "endfor" will be swallowed
-  multiline <- P.option False $ skipEndline >> return True
-  contents <- pTemplate
-  sep <- P.option mempty $
-           do P.try $ P.string "$sep$"
-              when multiline $ P.option () skipEndline
-              pTemplate
-  P.string "$endfor$"
-  when multiline $ P.option () skipEndline
-  return $ iter id' contents sep
+replaceVar :: Variable -- ^ Field
+           -> Value -- ^ New value
+           -> Value -- ^ Old object
+           -> Value -- ^ New object
+replaceVar (Variable [])     new _          = new
+replaceVar (Variable (v:vs)) new (Object o) = Object $ H.alter f v o
+    where f Nothing  = Just new
+          f (Just x) = Just (replaceVar (Variable vs) new x)
+replaceVar _ _ old = old
 
-indent :: Int -> Template -> Template
-indent 0   x            = x
-indent ind (Template f) = Template $ \val -> indent' (f val)
-  where indent' t = T.concat
-                    $ intersperse ("\n" <> T.replicate ind " ") $ T.lines t
+indent :: Int -> Text -> Text
+indent 0   = id
+indent ind = T.intercalate ("\n" <> T.replicate ind " ") . T.lines
+
diff --git a/test/Spec.hs b/test/Spec.hs
deleted file mode 100644
--- a/test/Spec.hs
+++ /dev/null
@@ -1,46 +0,0 @@
-{-# LANGUAGE OverloadedStrings #-}
-
-import Text.DocTemplates
-import Test.Hspec
-import Data.Text
-import Data.Aeson
-
-data Employee = Employee { firstName :: String
-                         , lastName  :: String
-                         , salary    :: Maybe Integer }
-instance ToJSON Employee where
-  toJSON e = object [ "name" .= object [ "first" .= firstName e
-                                       , "last"  .= lastName e ]
-                    , "salary" .= salary e ]
-
-employees :: [Employee]
-employees = [ Employee "John" "Doe" Nothing
-            , Employee "Omar" "Smith" (Just 30000)
-            , Employee "Sara" "Chen" (Just 60000) ]
-
-template :: Text
-template =
-  "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $$$employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"
-
-main :: IO ()
-main = hspec $ do
-  describe "applyTemplate" $ do
-    it "works" $ do
-      applyTemplate template (object ["employee" .= employees])
-        `shouldBe`
-        (Right "Hi, John. No salary data.\nHi, Omar. You make $30000.\nHi, Sara. You make $60000." :: Either String Text)
-    it "renders numbers appropriately as integer or floating" $ do
-      applyTemplate "$m$ and $n$"
-        (object ["m" .= (5 :: Integer), "n" .= (7.3 :: Double)])
-        `shouldBe`
-        (Right "5 and 7.3" :: Either String Text)
-    it "handles comments" $ do
-      applyTemplate "hello $--there and $m$\n$-- comment\nbar"
-        (object ["m" .= (5 :: Integer)])
-        `shouldBe`
-        (Right "hello \nbar" :: Either String Text)
-    it "fails with an incorrect template" $ do
-      applyTemplate "$if(x$and$endif$" (object [])
-        `shouldBe`
-        (Left "\"template\" (line 1, column 6):\nunexpected \"$\"\nexpecting \".\" or \")$\"" :: Either String Text)
-
diff --git a/test/basic-with-braces.test b/test/basic-with-braces.test
new file mode 100644
--- /dev/null
+++ b/test/basic-with-braces.test
@@ -0,0 +1,16 @@
+{ "employee":
+  [ { "name": { "first": "John", "last": "Doe" } }
+  , { "name": { "first": "Omar", "last": "Smith" }
+    , "salary": "30000" }
+  , { "name": { "first": "Sara", "last": "Chen" }
+    , "salary": "60000" }
+  ]
+}
+.
+${ for(employee) }
+Hi, ${employee.name.first}. ${ if(employee.salary) }You make $$${ employee.salary }.${ else }No salary data.${ endif }
+${ endfor }
+.
+Hi, John. No salary data.
+Hi, Omar. You make $30000.
+Hi, Sara. You make $60000.
diff --git a/test/basic-with-it.test b/test/basic-with-it.test
new file mode 100644
--- /dev/null
+++ b/test/basic-with-it.test
@@ -0,0 +1,16 @@
+{ "employee":
+  [ { "name": { "first": "John", "last": "Doe" } }
+  , { "name": { "first": "Omar", "last": "Smith" }
+    , "salary": "30000" }
+  , { "name": { "first": "Sara", "last": "Chen" }
+    , "salary": "60000" }
+  ]
+}
+.
+$for(employee)$
+Hi, $it.name.first$. $if(it.salary)$You make $$$it.salary$.$else$No salary data.$endif$
+$endfor$
+.
+Hi, John. No salary data.
+Hi, Omar. You make $30000.
+Hi, Sara. You make $60000.
diff --git a/test/basic.test b/test/basic.test
new file mode 100644
--- /dev/null
+++ b/test/basic.test
@@ -0,0 +1,16 @@
+{ "employee":
+  [ { "name": { "first": "John", "last": "Doe" } }
+  , { "name": { "first": "Omar", "last": "Smith" }
+    , "salary": "30000" }
+  , { "name": { "first": "Sara", "last": "Chen" }
+    , "salary": "60000" }
+  ]
+}
+.
+$for(employee)$
+Hi, $employee.name.first$. $if(employee.salary)$You make $$$employee.salary$.$else$No salary data.$endif$
+$ endfor $
+.
+Hi, John. No salary data.
+Hi, Omar. You make $30000.
+Hi, Sara. You make $60000.
diff --git a/test/boilerplate.txt b/test/boilerplate.txt
new file mode 100644
--- /dev/null
+++ b/test/boilerplate.txt
@@ -0,0 +1,1 @@
+BOILERPLATE HERE
diff --git a/test/comments.test b/test/comments.test
new file mode 100644
--- /dev/null
+++ b/test/comments.test
@@ -0,0 +1,9 @@
+{ "foo": 3 }
+.
+$-- a comment
+$--${foo} more comment
+$$-- not a comment
+a$-- comment
+.
+$-- not a comment
+a
diff --git a/test/conditionals.test b/test/conditionals.test
new file mode 100644
--- /dev/null
+++ b/test/conditionals.test
@@ -0,0 +1,37 @@
+{ "foo": 1,
+  "bar": null,
+  "baz": ["a", "b"],
+  "bim": { "zub": "sim" },
+  "sup": [ { "biz": "qux" }
+         , { "sax": "" }
+         ]
+}
+.
+${if(sup.sax)}
+XXX
+${else}
+YYY
+${endif}
+${if(bar)}
+BAR
+${endif}
+${if(bar)}BAR${endif}
+${if(foo)}
+FOO
+${endif}
+${if(baz)}
+BAZ
+${endif}
+${if(bim)}
+BIM
+${endif}
+${if(sup)}
+SUP
+${endif}
+.
+YYY
+
+FOO
+BAZ
+BIM
+SUP
diff --git a/test/forloop.test b/test/forloop.test
new file mode 100644
--- /dev/null
+++ b/test/forloop.test
@@ -0,0 +1,21 @@
+{ "employee":
+  [ { "name": { "first": "John", "last": "Doe" } }
+  , { "name": { "first": "Omar", "last": "Smith" }
+    , "salary": "30000" }
+  , { "name": { "first": "Sara", "last": "Chen" }
+    , "salary": "60000" }
+  ]
+}
+.
+$for(employee)$
+$employee.name.first$ $employee.name.last$$sep$;
+$endfor$
+
+
+$for(employee)$$employee.salary$$sep$:$endfor$
+.
+John Doe;
+Omar Smith;
+Sara Chen
+
+:30000:60000
diff --git a/test/indent.test b/test/indent.test
new file mode 100644
--- /dev/null
+++ b/test/indent.test
@@ -0,0 +1,18 @@
+{ "foo": "FOO"
+, "bar": [ "LINE1\n LINE2"
+         , "LINE3\n LINE4"
+         ]
+}
+.
+$if(foo)$
+   $foo$
+$endif$
+$for(bar)$
+  $bar$
+$endfor$
+.
+   FOO
+  LINE1
+   LINE2
+  LINE3
+   LINE4
diff --git a/test/inparens.txt b/test/inparens.txt
new file mode 100644
--- /dev/null
+++ b/test/inparens.txt
@@ -0,0 +1,1 @@
+($it$)
diff --git a/test/loop-in-object.test b/test/loop-in-object.test
new file mode 100644
--- /dev/null
+++ b/test/loop-in-object.test
@@ -0,0 +1,19 @@
+{ "worksite":
+  { "name": "canyon"
+  , "workers":
+       [ { "name": { "first": "John", "last": "Doe" } }
+       , { "name": { "first": "Omar", "last": "Smith" }
+         , "salary": "30000" }
+       , { "name": { "first": "Sara", "last": "Chen" }
+         , "salary": "60000" }
+       ]
+  }
+}
+.
+${ for(worksite.workers) }
+${it.name.last}, ${it.name.first}
+${ endfor }
+.
+Doe, John
+Smith, Omar
+Chen, Sara
diff --git a/test/loop-in-partial.test b/test/loop-in-partial.test
new file mode 100644
--- /dev/null
+++ b/test/loop-in-partial.test
@@ -0,0 +1,5 @@
+{}
+.
+$loop1()$
+.
+(loop)
diff --git a/test/loop1.txt b/test/loop1.txt
new file mode 100644
--- /dev/null
+++ b/test/loop1.txt
@@ -0,0 +1,1 @@
+$loop2()$
diff --git a/test/loop2.txt b/test/loop2.txt
new file mode 100644
--- /dev/null
+++ b/test/loop2.txt
@@ -0,0 +1,1 @@
+$loop1()$
diff --git a/test/name.tex b/test/name.tex
new file mode 100644
--- /dev/null
+++ b/test/name.tex
@@ -0,0 +1,1 @@
+\name{$it.name.first$}{$it.name.last$}
diff --git a/test/name.txt b/test/name.txt
new file mode 100644
--- /dev/null
+++ b/test/name.txt
@@ -0,0 +1,1 @@
+$it.name.first:inparens()$ $it.name.last$
diff --git a/test/numbers.test b/test/numbers.test
new file mode 100644
--- /dev/null
+++ b/test/numbers.test
@@ -0,0 +1,7 @@
+{ "m": 5
+, "n": 7.3
+}
+.
+$m$ and $n$
+.
+5 and 7.3
diff --git a/test/partials.test b/test/partials.test
new file mode 100644
--- /dev/null
+++ b/test/partials.test
@@ -0,0 +1,28 @@
+{ "employee":
+  [ { "name": { "first": "John", "last": "Doe" } }
+  , { "name": { "first": "Omar", "last": "Smith" }
+    , "salary": "30000" }
+  , { "name": { "first": "Sara", "last": "Chen" }
+    , "salary": "60000" }
+  ]
+}
+.
+$for(employee)$
+$it:name()$
+$endfor$
+
+$employee:name()[, ]$
+
+$employee:name.tex()[; ]$
+
+$boilerplate()$
+.
+(John) Doe
+(Omar) Smith
+(Sara) Chen
+
+(John) Doe, (Omar) Smith, (Sara) Chen
+
+\name{John}{Doe}; \name{Omar}{Smith}; \name{Sara}{Chen}
+
+BOILERPLATE HERE
diff --git a/test/test.hs b/test/test.hs
new file mode 100644
--- /dev/null
+++ b/test/test.hs
@@ -0,0 +1,63 @@
+{-# LANGUAGE OverloadedStrings #-}
+{-# LANGUAGE ScopedTypeVariables #-}
+
+import Text.DocTemplates
+import Test.Tasty.Golden
+import Test.Tasty
+import Test.Tasty.HUnit
+import qualified Data.Text as T
+import qualified Data.Text.IO as T
+import qualified Data.Text.Encoding as T
+import System.FilePath
+import System.IO.Temp
+import Data.Aeson
+import Control.Monad.Identity
+import System.FilePath.Glob
+import qualified Data.ByteString.Lazy as BL
+
+main :: IO ()
+main = withTempDirectory "test" "out." $ \tmpdir -> do
+  testFiles <- glob "test/*.test"
+  goldenTests <- mapM (getTest tmpdir) testFiles
+  defaultMain $ testGroup "Tests"
+    [ testGroup "Golden tests" goldenTests
+    , testGroup "Unit tests" unitTests
+    ]
+
+unitTests :: [TestTree]
+unitTests = [
+    testCase "compile failure" $ do
+      res <- compileTemplate "" "$if(x$and$endif$"
+      res @?= Left "\"template\" (line 1, column 6):\nunexpected \"$\"\nexpecting \".\" or \")\""
+  , testCase "compile failure (keyword as variable)" $ do
+      res <- compileTemplate "" "$sep$"
+      res @?= Left "\"template\" (line 1, column 5):\nunexpected \"$\"\nexpecting letter or digit or \"()\""
+  ]
+
+{- The test "golden" files are structured as follows:
+
+{ "foo": ["bar", "baz"] }
+.
+A template with $foo$.
+.
+A template with bar, baz.
+
+-}
+
+diff :: FilePath -> FilePath -> [String]
+diff ref new = ["diff", "-u", ref, new]
+
+getTest :: FilePath -> FilePath -> IO TestTree
+getTest tmpdir fp = do
+  let actual = tmpdir </> takeFileName fp
+  return $ goldenVsFileDiff fp diff fp actual $ do
+    inp <- T.readFile fp
+    let [json', template', _expected] = T.splitOn "\n.\n" inp
+    let json = json' <> "\n"
+    let template = template' <> "\n"
+    let templatePath = replaceExtension fp ".txt"
+    let Just (context :: Value) = decode' . BL.fromStrict . T.encodeUtf8 $ json
+    res <- applyTemplate templatePath template (context :: Value)
+    case res of
+      Left e -> error e
+      Right x -> T.writeFile actual $ json <> ".\n" <> template <> ".\n" <> x
diff --git a/test/values.test b/test/values.test
new file mode 100644
--- /dev/null
+++ b/test/values.test
@@ -0,0 +1,20 @@
+{ "foo": 1,
+  "bar": null,
+  "baz": ["a", "b"],
+  "bim": { "zub": "sim" },
+  "sup": [ { "biz": "qux" }
+         , { "sax": 2 }
+         ]
+}
+.
+$foo$
+$bar$
+$baz$
+$bim$
+$sup$
+.
+1
+
+ab
+true
+truetrue
