packages feed

doctemplates 0.5 → 0.6

raw patch · 11 files changed

+430/−107 lines, 11 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

- Text.DocTemplates.Internal: Indented :: !Int -> Indented
- Text.DocTemplates.Internal: Unindented :: Indented
- Text.DocTemplates.Internal: data Indented
- Text.DocTemplates.Internal: instance (Text.DocLayout.HasChars a, Data.String.IsString a) => Text.DocTemplates.Internal.TemplateTarget (Text.DocLayout.Doc a)
- Text.DocTemplates.Internal: instance Data.Data.Data Text.DocTemplates.Internal.Indented
- Text.DocTemplates.Internal: instance GHC.Classes.Eq Text.DocTemplates.Internal.Indented
- Text.DocTemplates.Internal: instance GHC.Classes.Ord Text.DocTemplates.Internal.Indented
- Text.DocTemplates.Internal: instance GHC.Generics.Generic Text.DocTemplates.Internal.Indented
- Text.DocTemplates.Internal: instance GHC.Read.Read Text.DocTemplates.Internal.Indented
- Text.DocTemplates.Internal: instance GHC.Show.Show Text.DocTemplates.Internal.Indented
+ Text.DocTemplates: breakingSpace :: TemplateTarget a => a
+ Text.DocTemplates.Internal: BreakingSpace :: Template
+ Text.DocTemplates.Internal: Nested :: Int -> Template -> Template
+ Text.DocTemplates.Internal: breakingSpace :: TemplateTarget a => a
+ Text.DocTemplates.Internal: instance (Text.DocLayout.HasChars a, Data.String.IsString a, GHC.Classes.Eq a) => Text.DocTemplates.Internal.TemplateTarget (Text.DocLayout.Doc a)
- Text.DocTemplates.Internal: Interpolate :: Indented -> Variable -> Template
+ Text.DocTemplates.Internal: Interpolate :: Variable -> Template

Files

README.md view
@@ -1,9 +1,22 @@ # doctemplates -This is the templating system used by pandoc.  It was formerly-a module in pandoc. It has been split off to make it easier-to use independently.+This is the text templating system used by pandoc.  Its basic+function is to fill holes in a template in a "Context" that+provides values for variables.  Control structures are provided+to test that a variable has a non-blank value and to iterate+over the items of a list.  Partials---that is, subtemplates+defined in different files---are also supported. +Templates may be rendered to lazy or strict `Text`, `String`,+or a doclayout `Doc`.  (Using a `Doc` allows rendered documents+to wrap flexibly on breaking spaces.)  A Context can be+constructed manually or an aeson `Value` may be used.++Unlike the various HTML-centered template engines,+doctemplates is output-format agnostic, so no automatic escaping+is done on interpolated values.  Values are assumed to be+escaped properly in the Context.+ ## Example of use  ``` haskell@@ -74,24 +87,27 @@ ${ foo } ``` -The values of variables are determined by a JSON object that is+The values of variables are determined by the `Context` 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+- If the value of the variable is simple value, it will be+  rendered verbatim.  (Note that no escaping is done;+  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+- If the value is a list, the values will be concatenated.+- If the value is a map, the string `true` will be rendered.+- Every other value will be rendered as the empty string.++When a `Context` is derived from an aeson (JSON) `Value`,+the following conversions are done:++- If the value is a 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 that occurs by itself on a line will be indented to the same level as the opening delimiter of@@ -133,9 +149,33 @@ ${endif} ``` -Conditional keywords should not be indented, or unexpected spacing-problems may occur.+The keyword `elseif` may be used to simplify complex nested+conditionals.  Thus +```+$if(foo)$+XXX+$elseif(bar)$+YYY+$else$+ZZZ+$endif$+```++is equivalent to++```+$if(foo)$+XXX+$else$+$if(bar)$+YYY+$else$+ZZZ+$endif$+$endif$+```+ ## For loops  A for loop begins with `for(variable)` (enclosed in@@ -243,4 +283,40 @@ The separator in this case is literal and (unlike with `sep` in an explicit `for` loop) cannot contain interpolated variables or other template directives.++## Breakable spaces++When rendering to a `Doc`, a distinction can be made between+breakable and unbreakable spaces.  Normally, spaces in the+template itself (as opposed to values of the interpolated+variables) are not breakable, but they can be made breakable+in part of the template by using the `+reflow` keyword (ended+with `-reflow`).++```+${ +reflow }This long line may break if the document is rendered+with a short line length.${ -reflow }+```++The `+` keyword has no effect when rendering to `Text`+or `String`.++## Nesting++As noted above, the value of a variable that occurs by itself on+a line will be indented to the same level as the opening+delimiter of the variable.++In addition, any part of a template can be marked explicitly for+indented rendering, using the `+nest` keyword (o start nesting at+the column where it appears) and `-nest` to stop nesting.++Example:++```+$for(article)$+- $+nest$$article.author$, "$article.title$," in $article.book$+  ($article.year$).$-nest$+$endfor$+``` 
bench/bench.hs view
@@ -1,10 +1,12 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE OverloadedStrings #-} import Text.DocTemplates+import qualified Data.Text as T import Data.Text (Text) import Criterion.Main import Criterion.Types (Config (..)) import Control.Monad.Identity+import Data.Semigroup ((<>)) import Data.Aeson (object, (.=), Value)  main :: IO ()@@ -21,7 +23,9 @@    ]  bigtext :: Text-bigtext = "Hello there $foo$. This is a big text.\n$for(bar)$$bar.baz$$endfor$"+bigtext = T.replicate 150 $+  "Hello there $foo$. This is a big text.\n$for(bar)$$bar.baz$$endfor$\n"+  <> "$if(foo)$Hi $foo$.$endif$\n"  val :: Value val = object [ "foo" .= (22 :: Int)
changelog.md view
@@ -1,5 +1,25 @@ # doctemplates +## 0.6++  * Add `+nest`/`-nest` keywords.++  * Add `+reflow`/`-reflow` keywords.++  * Add Nested constructor to Template, remove Indented+    and Indented parameter for Interpolate.++  * More expansive description of library.++## 0.5.1++  * Add elseif keyword.++  * Improve compile error source locations with partials.++  * Handle templates that don't end in newlines.  Previously+    this caused problems in some cases.+ ## 0.5    * Add toText method to TemplateTarget class.
doctemplates.cabal view
@@ -1,7 +1,13 @@ name:                doctemplates-version:             0.5+version:             0.6 synopsis:            Pandoc-style document templates-description:         A simple text templating system used by pandoc.+description:         This is the text templating system used by pandoc.+                     It supports variable interpolation, iteration,+                     tests for non-blank values, and partials.  Templates+                     may be rendered to a variety of formats, including+                     doclayout Docs, and variable values may come from+                     a variety of different sources, including aeson+                     Values. homepage:            https://github.com/jgm/doctemplates#readme license:             BSD3 license-file:        LICENSE@@ -46,6 +52,7 @@   main-is:             test.hs   build-depends:       base,                        doctemplates,+                       doclayout >= 0.1 && < 0.2,                        aeson,                        Glob,                        tasty,
src/Text/DocTemplates.hs view
@@ -7,8 +7,21 @@    Stability   : alpha    Portability : portable -The text templating system used by pandoc.+This is the text templating system used by pandoc. Its basic function is+to fill holes in a template in a 'Context' that provides values for+variables. Control structures are provided to test that a variable has a+non-blank value and to iterate over the items of a list. Partials—that+is, subtemplates defined in different files—are also supported. +Templates may be rendered to lazy or strict 'Text', 'String', or a+doclayout 'Doc'. (Using a 'Doc' allows rendered documents to wrap+flexibly on breaking spaces.) A 'Context' can be constructed manually or+an aeson 'Value' may be used.++Unlike the various HTML-centered template engines, doctemplates is+output-format agnostic, so no automatic escaping is done on interpolated+values. Values are assumed to be escaped properly in the Context.+ == Example of use  > {-# LANGUAGE OverloadedStrings #-}@@ -73,27 +86,30 @@ > ${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 values of variables are determined by the 'Context' 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+-   If the value of the variable is simple value, it will be rendered+    verbatim. (Note that no escaping is done; the assumption is that the+    calling program will escape the strings appropriately for the output+    format.)+-   If the value is a list, the values will be concatenated.+-   If the value is a map, the string @true@ will be rendered.+-   Every other value will be rendered as the empty string.++When a 'Context' is derived from an aeson (JSON) 'Value', the following+conversions are done:++-   If the value is a 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 that occurs by itself on a line-will be indented to the same level as the opening delimiter of-the variable.+The value of a variable that occurs by itself on a line will be indented+to the same level as the opening delimiter of the variable.  == Conditionals @@ -128,9 +144,29 @@ > no foo! > ${endif} -Conditional keywords should not be indented, or unexpected spacing-problems may occur.+The keyword @elseif@ may be used to simplify complex nested+conditionals. Thus +> $if(foo)$+> XXX+> $elseif(bar)$+> YYY+> $else$+> ZZZ+> $endif$++is equivalent to++> $if(foo)$+> XXX+> $else$+> $if(bar)$+> YYY+> $else$+> ZZZ+> $endif$+> $endif$+ == For loops  A for loop begins with @for(variable)@ (enclosed in matched delimiters)@@ -197,10 +233,9 @@ > ${ it:bibentry() } > ${ endfor } -Note that the anaphoric keyword @it@ must be used when-iterating over partials.  In the above examples,-the @bibentry@ partial should contain @it.title@-(and so on) instead of @articles.title@.+Note that the anaphoric keyword @it@ must be used when iterating over+partials. In the above examples, the @bibentry@ partial should contain+@it.title@ (and so on) instead of @articles.title@.  Final newlines are omitted from included partials. @@ -219,6 +254,36 @@ explicit @for@ loop) cannot contain interpolated variables or other template directives. +== Breakable spaces++When rendering to a @Doc@, a distinction can be made between breakable+and unbreakable spaces. Normally, spaces in the template itself (as+opposed to values of the interpolated variables) are not breakable, but+they can be made breakable in part of the template by using the+@+reflow@ keyword (ended with @-reflow@).++> ${ +reflow }This long line may break if the document is rendered+> with a short line length.${ -reflow }++The @+@ keyword has no effect when rendering to @Text@ or @String@.++== Nesting++As noted above, the value of a variable that occurs by itself on a line+will be indented to the same level as the opening delimiter of the+variable.++In addition, any part of a template can be marked explicitly for+indented rendering, using the @+nest@ keyword (o start nesting at the+column where it appears) and @-nest@ to stop nesting.++Example:++> $for(article)$+> - $+nest$$article.author$, "$article.title$," in $article.book$+>   ($article.year$).$-nest$+> $endfor$+ -}  module Text.DocTemplates ( renderTemplate@@ -238,8 +303,8 @@ import Data.Text (Text) import Text.DocTemplates.Parser (compileTemplate) import Text.DocTemplates.Internal ( TemplateMonad(..), Context(..),-            Val(..), ToContext(..), FromContext(..), TemplateTarget(..), Template,-            renderTemplate )+            Val(..), ToContext(..), FromContext(..), TemplateTarget(..),+            Template, renderTemplate )  -- | Compile a template from a file.  IO errors will be -- raised as exceptions; template parsing errors result in
src/Text/DocTemplates/Internal.hs view
@@ -32,7 +32,6 @@       , TemplateTarget(..)       , Template(..)       , Variable(..)-      , Indented(..)       ) where  import Safe (lastMay, initDef)@@ -57,19 +56,16 @@ import Data.Semigroup #endif --- | Determines whether an interpolated variable is rendered with--- indentation.-data Indented = Indented !Int | Unindented-     deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)- -- | A template. data Template =-       Interpolate Indented Variable+       Interpolate Variable      | Conditional Variable Template Template      | Iterate Variable Template Template+     | Nested Int Template      | Partial Template      | Literal Text      | Concat Template Template+     | BreakingSpace      | Empty      deriving (Show, Read, Data, Typeable, Generic, Eq, Ord) @@ -100,6 +96,7 @@   removeFinalNewline :: a -> a   isEmpty            :: a -> Bool   indent             :: Int -> a -> a+  breakingSpace      :: a  instance TemplateTarget Text where   fromText   = id@@ -111,6 +108,7 @@   isEmpty    = T.null   indent 0   = id   indent ind = T.intercalate ("\n" <> T.replicate ind " ") . T.lines+  breakingSpace = " "  instance TemplateTarget TL.Text where   fromText   = TL.fromStrict@@ -123,6 +121,7 @@   indent 0   = id   indent ind = TL.intercalate ("\n" <> TL.replicate (fromIntegral ind) " ")                . TL.lines+  breakingSpace = " "  instance TemplateTarget String where   fromText   = T.unpack@@ -134,8 +133,10 @@   isEmpty    = null   indent 0   = id   indent ind = intercalate ("\n" <> replicate ind ' ') . lines+  breakingSpace = " " -instance (DL.HasChars a, IsString a) => TemplateTarget (DL.Doc a) where+instance (DL.HasChars a, IsString a, Eq a)+    => TemplateTarget (DL.Doc a) where   fromText = DL.text . T.unpack   toText   = T.pack . DL.foldrChar (:) [] . DL.render Nothing   removeFinalNewline = DL.chomp@@ -144,7 +145,7 @@   isEmpty (DL.Text 0 _)   = True   isEmpty (DL.Concat x y) = isEmpty x && isEmpty y   isEmpty _               = False-+  breakingSpace  = DL.space  -- | A 'Context' defines values for template's variables. newtype Context a = Context { unContext :: M.Map Text (Val a) }@@ -258,8 +259,8 @@  multiLookup :: [Text] -> Val a -> Val a multiLookup [] x = x-multiLookup (v:vs) (MapVal (Context o)) =-  case M.lookup v o of+multiLookup (t:vs) (MapVal (Context o)) =+  case M.lookup t o of     Nothing -> NullVal     Just v' -> multiLookup vs v' multiLookup _ _ = NullVal@@ -295,13 +296,8 @@ renderTemp :: forall a . TemplateTarget a            => Template -> Context a -> a renderTemp (Literal t) _ = fromText t-renderTemp (Interpolate indented v) ctx =-  let vals = resolveVariable v ctx-   in if null vals-         then mempty-         else case indented of-                Indented ind -> indent ind $ mconcat vals-                _            -> mconcat vals+renderTemp BreakingSpace _ = breakingSpace+renderTemp (Interpolate v) ctx = mconcat $ resolveVariable v ctx renderTemp (Conditional v ift elset) ctx =   let res = resolveVariable v ctx    in case res of@@ -310,6 +306,7 @@ renderTemp (Iterate v t sep) ctx =   let sep' = renderTemp sep ctx    in mconcat . intersperse sep' $ withVariable v ctx (renderTemp t)+renderTemp (Nested n t) ctx = indent n $ renderTemp t ctx renderTemp (Partial t) ctx = renderTemp t ctx renderTemp (Concat t1 t2) ctx =   mappend (renderTemp t1 ctx) (renderTemp t2 ctx)
src/Text/DocTemplates/Parser.hs view
@@ -38,7 +38,9 @@   res <- P.runParserT (pTemplate <* P.eof)            PState{ templatePath   = templPath                  , partialNesting = 1-                 , beginsLine = True } "template" template+                 , indentLevel = 0+                 , beginsLine = True+                 , breakingSpaces = False } templPath template   case res of        Left e   -> return $ Left $ show e        Right x  -> return $ Right x@@ -47,28 +49,58 @@ data PState =   PState { templatePath   :: FilePath          , partialNesting :: Int-         , beginsLine     :: Bool }+         , indentLevel    :: Int+         , beginsLine     :: Bool+         , breakingSpaces :: Bool }  type Parser = P.ParsecT Text PState  pTemplate :: TemplateMonad m => Parser m Template pTemplate = do-  ts <- many $ P.try-         (P.skipMany pComment *> (pLit <|> pDirective <|> pEscape))   P.skipMany pComment-  return $ mconcat ts+  mconcat <$> many+    ((pLit <|> pNewline <|> pDirective <|> pEscape) <* P.skipMany pComment) +pNewline :: Monad m => Parser m Template+pNewline = do+  nls <- P.string "\n" <|> P.string "\r" <|> P.string "\r\n"+  breakspaces <- breakingSpaces <$> P.getState+  return $+   if breakspaces+      then BreakingSpace+      else Literal $ fromString nls+ pLit :: Monad m => Parser m Template pLit = do-  cs <- mconcat <$> P.many1 (P.many1 (P.satisfy (/= '$')))-  P.updateState $ \st ->-    st{ beginsLine =-          case dropWhile (\c -> c == ' ' || c == '\t') $ reverse cs of-            ('\n':_) -> True-            []       -> beginsLine st-            _        -> False }-  return $ Literal $ fromString cs+  col <- P.sourceColumn <$> P.getPosition+  ind <- indentLevel <$> P.getState+  -- eat up indentlevel spaces+  P.skipMany $ do+    P.getPosition >>= guard . (< (ind - 1)) . P.sourceColumn+    P.satisfy (\c -> c == ' ' || c == '\t')+  cs <- P.many1 (P.satisfy (\c -> c /= '$' && c /= '\n' && c /= '\r'))+  P.updateState $ \st -> st{ beginsLine = col == 1 && all (==' ') cs }+  breakspaces <- breakingSpaces <$> P.getState+  if breakspaces+     then return $ toBreakable cs+     else return $ Literal $ fromString cs +toBreakable :: String -> Template+toBreakable [] = Empty+toBreakable xs =+  case break isSpacy xs of+    ([], []) -> Empty+    ([], zs) -> BreakingSpace <> toBreakable (dropWhile isSpacy zs)+    (ys, []) -> Literal (fromString ys)+    (ys, zs) -> Literal (fromString ys) <> toBreakable zs++isSpacy :: Char -> Bool+isSpacy ' '  = True+isSpacy '\n' = True+isSpacy '\r' = True+isSpacy '\t' = True+isSpacy _    = False+ backupSourcePos :: Monad m => Int -> Parser m () backupSourcePos n = do   pos <- P.getPosition@@ -80,7 +112,8 @@ pDirective :: TemplateMonad m            => Parser m Template pDirective = do-  res <- pConditional <|> pForLoop <|> pInterpolate <|> pBarePartial+  res <- pConditional <|> pForLoop <|> pReflow <|> pNest <|>+         pInterpolate <|> pBarePartial   col <- P.sourceColumn <$> P.getPosition   P.updateState $ \st -> st{ beginsLine = col == 1 }   return res@@ -108,19 +141,50 @@   -- 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+  elseContents <- P.option mempty (pElse multiline <|> pElseIf)   pEnclosed (P.string "endif")   when multiline $ P.option () skipEndline   return $ Conditional v ifContents elseContents +pElse :: TemplateMonad m => Bool -> Parser m Template+pElse multiline = do+  pEnclosed (P.string "else")+  when multiline $ P.option () skipEndline+  pTemplate++pElseIf :: TemplateMonad m => Parser m Template+pElseIf = do+  v <- pEnclosed $ P.try $ P.string "elseif" *> pParens pVar+  multiline <- P.option False (True <$ skipEndline)+  ifContents <- pTemplate+  elseContents <- P.option mempty (pElse multiline <|> pElseIf)+  return $ Conditional v ifContents elseContents+ skipEndline :: Monad m => Parser m ()-skipEndline = P.try $ P.skipMany pSpaceOrTab <* P.char '\n'+skipEndline = P.try $+      (P.skipMany pSpaceOrTab <* P.char '\n')+  <|> (P.skipMany1 pSpaceOrTab <* P.eof) -pForLoop :: TemplateMonad m-         => Parser m Template+pNest :: TemplateMonad m => Parser m Template+pNest = do+  col <- P.sourceColumn <$> P.getPosition+  pEnclosed $ P.string "+nest"+  P.updateState $ \st -> st{ indentLevel = col - 1 }+  t <- pTemplate+  P.optional $ pEnclosed $ P.string "-nest"+  return $ Nested (col - 1) t++pReflow :: TemplateMonad m => Parser m Template+pReflow = mempty <$ (pReflowOn <|> pReflowOff)+ where+  pReflowOn = do+    pEnclosed $ P.string "+reflow"+    P.modifyState $ \st -> st{ breakingSpaces = True }+  pReflowOff = do+    pEnclosed $ P.string "-reflow"+    P.modifyState $ \st -> st{ breakingSpaces = False }++pForLoop :: TemplateMonad m => Parser m Template pForLoop = do   v <- pEnclosed $ P.try $ P.string "for" *> pParens pVar   -- if newline after the "for", then a newline after "endfor" will be swallowed@@ -137,15 +201,15 @@ changeToIt :: Variable -> Template -> Template changeToIt v = go  where-  go (Interpolate i w) = Interpolate i (reletter v w)+  go (Interpolate w) = Interpolate (reletter v w)   go (Conditional w t1 t2) = Conditional (reletter v w)         (changeToIt v t1) (changeToIt v t2)   go (Iterate w t1 t2) = Iterate (reletter v w)         (changeToIt v t1) (changeToIt v t2)-  go (Partial t) = Partial t  -- don't reletter inside partial-  go (Literal x) = Literal x   go (Concat t1 t2) = changeToIt v t1 <> changeToIt v t2-  go Empty = mempty+  go (Partial t) = Partial t  -- don't reletter inside partial+  go (Nested n t) = Nested n (go t)+  go x = x   reletter (Variable vs) (Variable ws) =     if vs `isPrefixOf` ws        then Variable ("it" : drop (length vs) ws)@@ -156,30 +220,56 @@ pInterpolate = do   begins <- beginsLine <$> P.getState   pos <- P.getPosition-  res <- pEnclosed $ do-    var <- pVar-    (P.char ':' *> pPartial (Just var))-      <|> Iterate var (Interpolate Unindented (Variable ["it"])) <$> pSep-      <|> return (Interpolate Unindented var)+  -- we don't used pEnclosed here, to get better error messages:+  (closer, var) <- P.try $ do+    cl <- pOpen+    P.skipMany pSpaceOrTab+    v <- pVar+    P.notFollowedBy (P.char '(') -- bare partial+    return (cl, v)+  res <- (P.char ':' *> (pPartialName >>= pPartial (Just var)))+      <|> Iterate var (Interpolate (Variable ["it"])) <$> pSep+      <|> return (Interpolate var)+  P.skipMany pSpaceOrTab+  closer   ends <- P.lookAhead $ P.option False $-             True <$ P.try (P.skipMany pSpaceOrTab *> P.newline)+             True <$ P.try (P.skipMany pSpaceOrTab *> pNewlineOrEof)+  let toNested = case P.sourceColumn pos - 1 of+                   0 -> id+                   i -> Nested i   case (begins && ends, res) of-    (True, Interpolate _ v)-               -> return $ Interpolate (Indented (P.sourceColumn pos - 1)) v-    (True, Iterate v (Interpolate _ v') s)-               -> return $ Iterate v-                    (Interpolate (Indented (P.sourceColumn pos - 1)) v') s+    (True, Interpolate v)+               -> return $ toNested $ Interpolate v+    (True, Iterate v (Interpolate v') s)+               -> return $ toNested $ Iterate v (Interpolate v') s     _ -> return res +pNewlineOrEof :: Monad m => Parser m ()+pNewlineOrEof = () <$ P.newline <|> P.eof+ pBarePartial :: TemplateMonad m              => Parser m Template-pBarePartial = pEnclosed $ pPartial Nothing+pBarePartial = do+  (closer, fp) <- P.try $ do+    closer <- pOpen+    P.skipMany pSpaceOrTab+    fp <- pPartialName+    return (closer, fp)+  res <- pPartial Nothing fp+  P.skipMany pSpaceOrTab+  closer+  return res -pPartial :: TemplateMonad m-         => Maybe Variable -> Parser m Template-pPartial mbvar = do+pPartialName :: TemplateMonad m+             => Parser m FilePath+pPartialName = P.try $ do   fp <- P.many1 (P.alphaNum <|> P.oneOf ['_','-','.','/','\\'])   P.string "()"+  return fp++pPartial :: TemplateMonad m+         => Maybe Variable -> FilePath -> Parser m Template+pPartial mbvar fp = do   separ <- P.option mempty pSep   tp <- templatePath <$> P.getState   let fp' = case takeExtension fp of@@ -192,7 +282,7 @@           else do             oldInput <- P.getInput             oldPos <- P.getPosition-            P.setPosition $ P.initialPos fp+            P.setPosition $ P.initialPos fp'             P.setInput partial             P.updateState $ \st -> st{ partialNesting = nesting + 1 }             res' <- pTemplate <* P.eof@@ -222,7 +312,7 @@   -- If the comment begins in the first column, the line ending   -- will be consumed; otherwise not.   when (P.sourceColumn pos == 1) $ () <$ do-    P.char '\n'+    pNewlineOrEof     P.updateState $ \st -> st{ beginsLine = True }  pOpenDollar :: Monad m => Parser m (Parser m ())@@ -243,10 +333,13 @@  pVar :: Monad m => Parser m Variable pVar = do-  first <- pIdentPart <|> "it" <$ P.try (P.string "it")-  rest <- P.many $ P.char '.' *> pIdentPart+  first <- pIdentPart <|> pIt+  rest <- P.many $ (P.char '.' *> pIdentPart)   return $ Variable (first:rest) +pIt :: Monad m => Parser m Text+pIt = fromString <$> P.try (P.string "it")+ pIdentPart :: Monad m => Parser m Text pIdentPart = P.try $ do   first <- P.letter@@ -256,4 +349,4 @@   return $ fromString part  reservedWords :: [String]-reservedWords = ["else","endif","for","endfor","sep","it"]+reservedWords = ["if","else","endif","elseif","for","endfor","sep","it"]
+ test/bad.txt view
@@ -0,0 +1,2 @@+partial+$with syntax error
+ test/elseif.test view
@@ -0,0 +1,30 @@+{ "foo": 1,+  "bar": null,+  "baz": ["a", "b"],+  "bim": { "zub": "sim" },+  "sup": [ { "biz": "qux" }+         , { "sax": "" }+         ]+}+.+$if(sup.sax)$+XXX+$elseif(baz)$+YYY+$else$+ZZZ+$endif$++$if(sup.sax)$+XXX+$elseif(baz.nonexist)$+YYY+$elseif(sup.sax)$+ZZZ+$else$+WWW+$endif$+.+YYY++WWW
test/partials.test view
@@ -13,6 +13,8 @@  $employee:name()[, ]$ +$employee:name()$+ $employee:name.tex()[; ]$  $boilerplate()$@@ -22,6 +24,8 @@ (Sara) Chen  (John) Doe, (Omar) Smith, (Sara) Chen++(John) Doe(Omar) Smith(Sara) Chen  \name{John}{Doe}; \name{Omar}{Smith}; \name{Sara}{Chen} 
test/test.hs view
@@ -1,6 +1,7 @@ {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} +import Text.DocLayout (render) import Text.DocTemplates import Test.Tasty.Golden import Test.Tasty@@ -28,12 +29,36 @@ unitTests :: [TestTree] unitTests = [     testCase "compile failure" $ do-      (res :: Either String Template)-        <- compileTemplate "" "$if(x$and$endif$"-      res @?= Left "\"template\" (line 1, column 6):\nunexpected \"$\"\nexpecting \".\" or \")\""+      res <- compileTemplate "" "$if(x$and$endif$"+      res @?= Left "(line 1, column 6):\nunexpected \"$\"\nexpecting \".\" or \")\""   , testCase "compile failure (keyword as variable)" $ do-      (res :: Either String Template) <- compileTemplate "" "$sep$"-      res @?= Left "\"template\" (line 1, column 5):\nunexpected \"$\"\nexpecting letter or digit or \"()\""+      res <- compileTemplate "foobar.txt" "$sep$"+      res @?= Left "\"foobar.txt\" (line 1, column 5):\nunexpected \"$\"\nexpecting letter or digit or \"()\""+  , testCase "compile failure (error in partial)" $ do+      res <- compileTemplate "test/foobar.txt" "$bad()$"+      res @?= Left "\"test/bad.txt\" (line 2, column 7):\nunexpected \"s\"\nexpecting \"$\""+  , testCase "comment with no newline" $ do+      res <- compileTemplate "foo" "$-- hi"+      res @?= Right mempty+  , testCase "reflow" $ do+      templ <- compileTemplate "foo" "not breakable and $+reflow$ this is breakable\nok? $foo$$-reflow$"+      let res :: T.Text+          res = case templ of+                  Right t -> render (Just 10)+                   (renderTemplate t (object ["foo" .= ("42" :: T.Text)]))+                  Left e  -> T.pack e+      res @?= "not breakable and \nthis is\nbreakable\nok? 42"+  , testCase "nest" $ do+      res <- applyTemplate "foo" "- $+nest$aaa\nbbb$-nest$" (object [])+      res @?= Right ("- aaa\n  bbb" :: T.Text)+  , testCase "implicitly closed nest" $ do+      templ <- compileTemplate "foo" "$if(foo)$\n- $+nest$a\nb$endif$"+      let res :: T.Text+          res = case templ of+                  Right t -> render (Just 10)+                   (renderTemplate t (object ["foo" .= ("42" :: T.Text)]))+                  Left e  -> T.pack e+      res @?= "- a\n  b"   ]  {- The test "golden" files are structured as follows: