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 +193/−19
- changelog.md +49/−0
- doctemplates.cabal +16/−6
- src/Text/DocTemplates.hs +471/−212
- test/Spec.hs +0/−46
- test/basic-with-braces.test +16/−0
- test/basic-with-it.test +16/−0
- test/basic.test +16/−0
- test/boilerplate.txt +1/−0
- test/comments.test +9/−0
- test/conditionals.test +37/−0
- test/forloop.test +21/−0
- test/indent.test +18/−0
- test/inparens.txt +1/−0
- test/loop-in-object.test +19/−0
- test/loop-in-partial.test +5/−0
- test/loop1.txt +1/−0
- test/loop2.txt +1/−0
- test/name.tex +1/−0
- test/name.txt +1/−0
- test/numbers.test +7/−0
- test/partials.test +28/−0
- test/test.hs +63/−0
- test/values.test +20/−0
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