packages feed

doctemplates 0.2.2.1 → 0.3

raw patch · 24 files changed

+1010/−283 lines, 24 filesdep +Globdep +filepathdep +mtldep −blaze-htmldep −blaze-markupdep −hspecPVP ok

version bump matches the API change (PVP)

Dependencies added: Glob, filepath, mtl, tasty, tasty-golden, tasty-hunit, temporary

Dependencies removed: blaze-html, blaze-markup, hspec

API changes (from Hackage documentation)

- Text.DocTemplates: class TemplateTarget a
- Text.DocTemplates: data Template
- Text.DocTemplates: instance Data.Semigroup.Semigroup Text.DocTemplates.Template
- Text.DocTemplates: instance Text.DocTemplates.TemplateTarget Data.ByteString.Lazy.Internal.ByteString
- Text.DocTemplates: instance Text.DocTemplates.TemplateTarget Data.Text.Internal.Text
- Text.DocTemplates: instance Text.DocTemplates.TemplateTarget GHC.Base.String
- Text.DocTemplates: instance Text.DocTemplates.TemplateTarget Text.Blaze.Html.Html
- Text.DocTemplates: toTarget :: TemplateTarget a => Text -> a
- Text.DocTemplates: varListToJSON :: [(String, String)] -> Value
+ Text.DocTemplates: Conditional :: Variable -> Template -> Template -> TemplatePart
+ Text.DocTemplates: Interpolate :: Variable -> TemplatePart
+ Text.DocTemplates: Iterate :: Variable -> Template -> Template -> TemplatePart
+ Text.DocTemplates: Literal :: Text -> TemplatePart
+ Text.DocTemplates: Partial :: Template -> TemplatePart
+ Text.DocTemplates: Template :: [TemplatePart] -> Template
+ Text.DocTemplates: Variable :: [Text] -> Variable
+ Text.DocTemplates: [unTemplate] :: Template -> [TemplatePart]
+ Text.DocTemplates: [unVariable] :: Variable -> [Text]
+ Text.DocTemplates: class Monad m => TemplateMonad m
+ Text.DocTemplates: data TemplatePart
+ Text.DocTemplates: getPartial :: TemplateMonad m => FilePath -> Parser m Text
+ Text.DocTemplates: instance Data.Data.Data Text.DocTemplates.Template
+ Text.DocTemplates: instance Data.Data.Data Text.DocTemplates.TemplatePart
+ Text.DocTemplates: instance Data.Data.Data Text.DocTemplates.Variable
+ Text.DocTemplates: instance GHC.Base.Monoid Text.DocTemplates.Variable
+ Text.DocTemplates: instance GHC.Base.Semigroup Text.DocTemplates.Template
+ Text.DocTemplates: instance GHC.Base.Semigroup Text.DocTemplates.Variable
+ Text.DocTemplates: instance GHC.Classes.Eq Text.DocTemplates.Template
+ Text.DocTemplates: instance GHC.Classes.Eq Text.DocTemplates.TemplatePart
+ Text.DocTemplates: instance GHC.Classes.Eq Text.DocTemplates.Variable
+ Text.DocTemplates: instance GHC.Classes.Ord Text.DocTemplates.Template
+ Text.DocTemplates: instance GHC.Classes.Ord Text.DocTemplates.TemplatePart
+ Text.DocTemplates: instance GHC.Classes.Ord Text.DocTemplates.Variable
+ Text.DocTemplates: instance GHC.Generics.Generic Text.DocTemplates.Template
+ Text.DocTemplates: instance GHC.Generics.Generic Text.DocTemplates.TemplatePart
+ Text.DocTemplates: instance GHC.Generics.Generic Text.DocTemplates.Variable
+ Text.DocTemplates: instance GHC.Read.Read Text.DocTemplates.Template
+ Text.DocTemplates: instance GHC.Read.Read Text.DocTemplates.TemplatePart
+ Text.DocTemplates: instance GHC.Read.Read Text.DocTemplates.Variable
+ Text.DocTemplates: instance GHC.Show.Show Text.DocTemplates.Template
+ Text.DocTemplates: instance GHC.Show.Show Text.DocTemplates.TemplatePart
+ Text.DocTemplates: instance GHC.Show.Show Text.DocTemplates.Variable
+ Text.DocTemplates: instance Text.DocTemplates.TemplateMonad Data.Functor.Identity.Identity
+ Text.DocTemplates: instance Text.DocTemplates.TemplateMonad GHC.Types.IO
+ Text.DocTemplates: newtype Template
+ Text.DocTemplates: newtype Variable
- Text.DocTemplates: applyTemplate :: (ToJSON a, TemplateTarget b) => Text -> a -> Either String b
+ Text.DocTemplates: applyTemplate :: (TemplateMonad m, ToJSON a) => FilePath -> Text -> a -> m (Either String Text)
- Text.DocTemplates: compileTemplate :: Text -> Either String Template
+ Text.DocTemplates: compileTemplate :: TemplateMonad m => FilePath -> Text -> m (Either String Template)
- Text.DocTemplates: renderTemplate :: (ToJSON a, TemplateTarget b) => Template -> a -> b
+ Text.DocTemplates: renderTemplate :: ToJSON a => Template -> a -> Text

Files

README.md view
@@ -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.+
+ changelog.md view
@@ -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.+
doctemplates.cabal view
@@ -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
src/Text/DocTemplates.hs view
@@ -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+
− test/Spec.hs
@@ -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)-
+ test/basic-with-braces.test view
@@ -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.
+ test/basic-with-it.test view
@@ -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.
+ test/basic.test view
@@ -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.
+ test/boilerplate.txt view
@@ -0,0 +1,1 @@+BOILERPLATE HERE
+ test/comments.test view
@@ -0,0 +1,9 @@+{ "foo": 3 }+.+$-- a comment+$--${foo} more comment+$$-- not a comment+a$-- comment+.+$-- not a comment+a
+ test/conditionals.test view
@@ -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
+ test/forloop.test view
@@ -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
+ test/indent.test view
@@ -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
+ test/inparens.txt view
@@ -0,0 +1,1 @@+($it$)
+ test/loop-in-object.test view
@@ -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
+ test/loop-in-partial.test view
@@ -0,0 +1,5 @@+{}+.+$loop1()$+.+(loop)
+ test/loop1.txt view
@@ -0,0 +1,1 @@+$loop2()$
+ test/loop2.txt view
@@ -0,0 +1,1 @@+$loop1()$
+ test/name.tex view
@@ -0,0 +1,1 @@+\name{$it.name.first$}{$it.name.last$}
+ test/name.txt view
@@ -0,0 +1,1 @@+$it.name.first:inparens()$ $it.name.last$
+ test/numbers.test view
@@ -0,0 +1,7 @@+{ "m": 5+, "n": 7.3+}+.+$m$ and $n$+.+5 and 7.3
+ test/partials.test view
@@ -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
+ test/test.hs view
@@ -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
+ test/values.test view
@@ -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