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 +90/−14
- bench/bench.hs +5/−1
- changelog.md +20/−0
- doctemplates.cabal +9/−2
- src/Text/DocTemplates.hs +87/−22
- src/Text/DocTemplates/Internal.hs +15/−18
- src/Text/DocTemplates/Parser.hs +138/−45
- test/bad.txt +2/−0
- test/elseif.test +30/−0
- test/partials.test +4/−0
- test/test.hs +30/−5
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: