stache 2.1.1 → 2.2.0
raw patch · 16 files changed
+901/−611 lines, 16 filesdep +gitrevdep +optparse-applicativedep ~basedep ~bytestringdep ~megaparsecnew-component:exe:stache
Dependencies added: gitrev, optparse-applicative
Dependency ranges changed: base, bytestring, megaparsec, stache, text
Files
- CHANGELOG.md +4/−0
- README.md +1/−1
- Text/Mustache.hs +16/−14
- Text/Mustache/Compile.hs +67/−54
- Text/Mustache/Compile/TH.hs +49/−43
- Text/Mustache/Parser.hs +54/−52
- Text/Mustache/Render.hs +94/−96
- Text/Mustache/Type.hs +62/−51
- app/Main.hs +135/−0
- bench/Main.hs +99/−63
- mustache-spec/Spec.hs +25/−25
- stache.cabal +151/−108
- tests/Text/Mustache/Compile/THSpec.hs +17/−14
- tests/Text/Mustache/ParserSpec.hs +32/−28
- tests/Text/Mustache/RenderSpec.hs +65/−40
- tests/Text/Mustache/TypeSpec.hs +30/−22
CHANGELOG.md view
@@ -1,3 +1,7 @@+## Stache 2.2.0++* Added the executable program.+ ## Stache 2.1.1 * Fixed the bug related to incorrect indentation with nested partials.
README.md view
@@ -4,7 +4,7 @@ [](https://hackage.haskell.org/package/stache) [](http://stackage.org/nightly/package/stache) [](http://stackage.org/lts/package/stache)-[](https://travis-ci.org/stackbuilders/stache)+ This is a Haskell implementation of Mustache templates. The implementation conforms to the version 1.1.3 of the official [Mustache
Text/Mustache.hs view
@@ -70,24 +70,26 @@ -- * The manual: <https://mustache.github.io/mustache.5.html> -- * The specification: <https://github.com/mustache/spec> -- * Stack Builders Stache tutorial: <https://www.stackbuilders.com/tutorials/haskell/mustache-templates/>- module Text.Mustache ( -- * Types- Template (..)- , Node (..)- , Key (..)- , PName (..)- , MustacheException (..)- , MustacheWarning (..)- , displayMustacheWarning+ Template (..),+ Node (..),+ Key (..),+ PName (..),+ MustacheException (..),+ MustacheWarning (..),+ displayMustacheWarning,+ -- * Compiling- , compileMustacheDir- , compileMustacheDir'- , compileMustacheFile- , compileMustacheText+ compileMustacheDir,+ compileMustacheDir',+ compileMustacheFile,+ compileMustacheText,+ -- * Rendering- , renderMustache- , renderMustacheW )+ renderMustache,+ renderMustacheW,+ ) where import Text.Mustache.Compile
Text/Mustache/Compile.hs view
@@ -10,29 +10,29 @@ -- Mustache 'Template' creation from file or a 'Text' value. You don't -- usually need to import the module, because "Text.Mustache" re-exports -- everything you may need, import that module instead.- module Text.Mustache.Compile- ( compileMustacheDir- , compileMustacheDir'- , getMustacheFilesInDir- , getMustacheFilesInDir'- , isMustacheFile- , compileMustacheFile- , compileMustacheText )+ ( compileMustacheDir,+ compileMustacheDir',+ getMustacheFilesInDir,+ getMustacheFilesInDir',+ isMustacheFile,+ compileMustacheFile,+ compileMustacheText,+ ) where import Control.Exception import Control.Monad.Except+import qualified Data.Map as M import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.IO as T import Data.Void import System.Directory+import qualified System.FilePath as F import Text.Megaparsec import Text.Mustache.Parser import Text.Mustache.Type-import qualified Data.Map as M-import qualified Data.Text as T-import qualified Data.Text.IO as T-import qualified System.FilePath as F -- | Compile all templates in specified directory and select one. Template -- files should have the extension @mustache@, (e.g. @foo.mustache@) to be@@ -47,28 +47,35 @@ -- 'getDirectoryContents', and 'T.readFile'. -- -- > compileMustacheDir = complieMustacheDir' isMustacheFile--compileMustacheDir :: MonadIO m- => PName -- ^ Which template to select after compiling- -> FilePath -- ^ Directory with templates- -> m Template -- ^ The resulting template+compileMustacheDir ::+ MonadIO m =>+ -- | Which template to select after compiling+ PName ->+ -- | Directory with templates+ FilePath ->+ -- | The resulting template+ m Template compileMustacheDir = compileMustacheDir' isMustacheFile -- | The same as 'compileMustacheDir', but allows using a custom predicate -- for template selection. -- -- @since 1.2.0--compileMustacheDir' :: MonadIO m- => (FilePath -> Bool) -- ^ Template selection predicate- -> PName -- ^ Which template to select after compiling- -> FilePath -- ^ Directory with templates- -> m Template -- ^ The resulting template+compileMustacheDir' ::+ MonadIO m =>+ -- | Template selection predicate+ (FilePath -> Bool) ->+ -- | Which template to select after compiling+ PName ->+ -- | Directory with templates+ FilePath ->+ -- | The resulting template+ m Template compileMustacheDir' predicate pname path =- getMustacheFilesInDir' predicate path >>=- fmap selectKey . foldM f (Template undefined M.empty)+ getMustacheFilesInDir' predicate path+ >>= fmap selectKey . foldM f (Template undefined M.empty) where- selectKey t = t { templateActual = pname }+ selectKey t = t {templateActual = pname} f (Template _ old) fp = do Template _ new <- compileMustacheFile fp return (Template undefined (M.union new old))@@ -77,32 +84,35 @@ -- are absolute. -- -- @since 0.2.2--getMustacheFilesInDir :: MonadIO m- => FilePath -- ^ Directory with templates- -> m [FilePath]+getMustacheFilesInDir ::+ MonadIO m =>+ -- | Directory with templates+ FilePath ->+ m [FilePath] getMustacheFilesInDir = getMustacheFilesInDir' isMustacheFile -- | Return a list of templates found via a predicate in given directory. -- The returned paths are absolute. -- -- @since 1.2.0--getMustacheFilesInDir' :: MonadIO m- => (FilePath -> Bool) -- ^ Mustache file selection predicate- -> FilePath -- ^ Directory with templates- -> m [FilePath]-getMustacheFilesInDir' predicate path = liftIO $- getDirectoryContents path >>=- filterM f . fmap (F.combine path) >>=- mapM makeAbsolute+getMustacheFilesInDir' ::+ MonadIO m =>+ -- | Mustache file selection predicate+ (FilePath -> Bool) ->+ -- | Directory with templates+ FilePath ->+ m [FilePath]+getMustacheFilesInDir' predicate path =+ liftIO $+ getDirectoryContents path+ >>= filterM f . fmap (F.combine path)+ >>= mapM makeAbsolute where f p = (&& predicate p) <$> doesFileExist p -- | The default Mustache file predicate. -- -- @since 1.2.0- isMustacheFile :: FilePath -> Bool isMustacheFile path = F.takeExtension path == ".mustache" @@ -110,10 +120,11 @@ -- -- The action can throw 'MustacheParserException' and the same exceptions as -- 'T.readFile'.--compileMustacheFile :: MonadIO m- => FilePath -- ^ Location of the file- -> m Template+compileMustacheFile ::+ MonadIO m =>+ -- | Location of the file+ FilePath ->+ m Template compileMustacheFile path = liftIO $ do input <- T.readFile path withException (compile input)@@ -123,11 +134,13 @@ -- | Compile Mustache template from a lazy 'Text' value. The cache will -- contain only this template named according to given 'PName'.--compileMustacheText- :: PName -- ^ How to name the template?- -> Text -- ^ The template to compile- -> Either (ParseErrorBundle Text Void) Template -- ^ The result+compileMustacheText ::+ -- | How to name the template?+ PName ->+ -- | The template to compile+ Text ->+ -- | The result+ Either (ParseErrorBundle Text Void) Template compileMustacheText pname txt = Template pname . M.singleton pname <$> parseMustache "" txt @@ -135,14 +148,14 @@ -- Helpers -- | Build a 'PName' from given 'FilePath'.- pathToPName :: FilePath -> PName pathToPName = PName . T.pack . F.takeBaseName -- | Throw 'MustacheException' if argument is 'Left' or return the result -- inside 'Right'.--withException- :: Either (ParseErrorBundle Text Void) Template -- ^ Value to process- -> IO Template -- ^ The result+withException ::+ -- | Value to process+ Either (ParseErrorBundle Text Void) Template ->+ -- | The result+ IO Template withException = either (throwIO . MustacheParserException) return
Text/Mustache/Compile/TH.hs view
@@ -1,3 +1,7 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TemplateHaskell #-}+ -- | -- Module : Text.Mustache.Compile.TH -- Copyright : © 2016–present Stack Builders@@ -13,28 +17,24 @@ -- -- At the moment, functions in this module only work with GHC 8 (they -- require at least @template-haskell-2.11@).--{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RankNTypes #-}-{-# LANGUAGE TemplateHaskell #-}- module Text.Mustache.Compile.TH- ( compileMustacheDir- , compileMustacheDir'- , compileMustacheFile- , compileMustacheText- , mustache )+ ( compileMustacheDir,+ compileMustacheDir',+ compileMustacheFile,+ compileMustacheText,+ mustache,+ ) where import Control.Exception import Data.Text (Text)+import qualified Data.Text as T import Language.Haskell.TH hiding (Dec) import Language.Haskell.TH.Quote (QuasiQuoter (..))-import Language.Haskell.TH.Syntax (lift, addDependentFile)+import Language.Haskell.TH.Syntax (addDependentFile, lift) import System.Directory-import Text.Mustache.Type-import qualified Data.Text as T import qualified Text.Mustache.Compile as C+import Text.Mustache.Type -- | Compile all templates in specified directory and select one. Template -- files should have the extension @mustache@, (e.g. @foo.mustache@) to be@@ -43,11 +43,13 @@ -- This version compiles the templates at compile time. -- -- > compileMustacheDir = compileMustacheDir' isMustacheFile--compileMustacheDir- :: PName -- ^ Which template to select after compiling- -> FilePath -- ^ Directory with templates- -> Q Exp -- ^ The resulting template+compileMustacheDir ::+ -- | Which template to select after compiling+ PName ->+ -- | Directory with templates+ FilePath ->+ -- | The resulting template+ Q Exp compileMustacheDir = compileMustacheDir' C.isMustacheFile -- | The same as 'compileMustacheDir', but allows using a custom predicate@@ -56,12 +58,15 @@ -- This version compiles the templates at compile time. -- -- @since 1.2.0--compileMustacheDir'- :: (FilePath -> Bool) -- ^ Template selection predicate- -> PName -- ^ Which template to select after compiling- -> FilePath -- ^ Directory with templates- -> Q Exp -- ^ The resulting template+compileMustacheDir' ::+ -- | Template selection predicate+ (FilePath -> Bool) ->+ -- | Which template to select after compiling+ PName ->+ -- | Directory with templates+ FilePath ->+ -- | The resulting template+ Q Exp compileMustacheDir' predicate pname path = do runIO (C.getMustacheFilesInDir' predicate path) >>= mapM_ addDependentFile (runIO . try) (C.compileMustacheDir' predicate pname path) >>= handleEither@@ -69,10 +74,10 @@ -- | Compile single Mustache template and select it. -- -- This version compiles the template at compile time.--compileMustacheFile- :: FilePath -- ^ Location of the file- -> Q Exp+compileMustacheFile ::+ -- | Location of the file+ FilePath ->+ Q Exp compileMustacheFile path = do runIO (makeAbsolute path) >>= addDependentFile (runIO . try) (C.compileMustacheFile path) >>= handleEither@@ -81,14 +86,15 @@ -- only this template named according to given 'Key'. -- -- This version compiles the template at compile time.--compileMustacheText- :: PName -- ^ How to name the template?- -> Text -- ^ The template to compile- -> Q Exp+compileMustacheText ::+ -- | How to name the template?+ PName ->+ -- | The template to compile+ Text ->+ Q Exp compileMustacheText pname text = (handleEither . either (Left . MustacheParserException) Right)- (C.compileMustacheText pname text)+ (C.compileMustacheText pname text) -- | Compile Mustache using QuasiQuoter. Usage: --@@ -103,17 +109,17 @@ -- with partials as usual. -- -- @since 0.1.7- mustache :: QuasiQuoter-mustache = QuasiQuoter- { quoteExp = compileMustacheText "quasi-quoted" . T.pack- , quotePat = error "This usage is not supported."- , quoteType = error "This usage is not supported."- , quoteDec = error "This usage is not supported." }+mustache =+ QuasiQuoter+ { quoteExp = compileMustacheText "quasi-quoted" . T.pack,+ quotePat = error "This usage is not supported.",+ quoteType = error "This usage is not supported.",+ quoteDec = error "This usage is not supported."+ } -- | Given an 'Either' result return 'Right' and signal pretty-printed error -- if we have a 'Left'.- handleEither :: Either MustacheException Template -> Q Exp handleEither val = case val of@@ -126,5 +132,5 @@ -- tools to parse the errors correctly. indentNicely x' = case lines x' of- [] -> ""- (x:xs) -> unlines (x : fmap (replicate 8 ' ' ++) xs)+ [] -> ""+ (x : xs) -> unlines (x : fmap (replicate 8 ' ' ++) xs)
Text/Mustache/Parser.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+ -- | -- Module : Text.Mustache.Parser -- Copyright : © 2016–present Stack Builders@@ -10,25 +13,22 @@ -- Megaparsec parser for Mustache templates. You don't usually need to -- import the module, because "Text.Mustache" re-exports everything you may -- need, import that module instead.--{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-}- module Text.Mustache.Parser- ( parseMustache )+ ( parseMustache,+ ) where import Control.Monad import Control.Monad.State.Strict-import Data.Char (isSpace, isAlphaNum)+import Data.Char (isAlphaNum, isSpace) import Data.Maybe (catMaybes) import Data.Text (Text)+import qualified Data.Text as T import Data.Void import Text.Megaparsec import Text.Megaparsec.Char-import Text.Mustache.Type-import qualified Data.Text as T import qualified Text.Megaparsec.Char.Lexer as L+import Text.Mustache.Type #if !MIN_VERSION_base(4,13,0) import Data.Semigroup ((<>))@@ -38,31 +38,32 @@ -- Parser -- | Parse a given Mustache template.--parseMustache- :: FilePath- -- ^ Location of the file to parse- -> Text- -- ^ File contents (Mustache template)- -> Either (ParseErrorBundle Text Void) [Node]- -- ^ Parsed nodes or parse error-parseMustache = parse $- evalStateT (pMustache eof) (St "{{" "}}" 0)+parseMustache ::+ -- | Location of the file to parse+ FilePath ->+ -- | File contents (Mustache template)+ Text ->+ -- | Parsed nodes or parse error+ Either (ParseErrorBundle Text Void) [Node]+parseMustache =+ parse $+ evalStateT (pMustache eof) (St "{{" "}}" 0) pMustache :: Parser () -> Parser [Node] pMustache = fmap catMaybes . manyTill (choice alts) where alts =- [ Nothing <$ withStandalone pComment- , Just <$> pSection "#" Section- , Just <$> pSection "^" InvertedSection- , Just <$> pStandalone (pPartial Just)- , Just <$> pPartial (const Nothing)- , Nothing <$ withStandalone pSetDelimiters- , Just <$> pUnescapedVariable- , Just <$> pUnescapedSpecial- , Just <$> pEscapedVariable- , Just <$> pTextBlock ]+ [ Nothing <$ withStandalone pComment,+ Just <$> pSection "#" Section,+ Just <$> pSection "^" InvertedSection,+ Just <$> pStandalone (pPartial Just),+ Just <$> pPartial (const Nothing),+ Nothing <$ withStandalone pSetDelimiters,+ Just <$> pUnescapedVariable,+ Just <$> pUnescapedSpecial,+ Just <$> pEscapedVariable,+ Just <$> pTextBlock+ ] {-# INLINE pMustache #-} pTextBlock :: Parser Node@@ -85,14 +86,14 @@ pUnescapedSpecial :: Parser Node pUnescapedSpecial = do start <- gets openingDel- end <- gets closingDel+ end <- gets closingDel between (symbol $ start <> "{") (string $ "}" <> end) $ UnescapedVar <$> pKey {-# INLINE pUnescapedSpecial #-} pSection :: Text -> (Key -> [Node] -> Node) -> Parser Node pSection suffix f = do- key <- withStandalone (pTag suffix)+ key <- withStandalone (pTag suffix) nodes <- (pMustache . withStandalone . pClosingTag) key return (f key nodes) {-# INLINE pSection #-}@@ -108,7 +109,7 @@ pComment :: Parser () pComment = void $ do start <- gets openingDel- end <- gets closingDel+ end <- gets closingDel (void . symbol) (start <> "!") manyTill (anySingle <?> "character") (string end) {-# INLINE pComment #-}@@ -116,14 +117,16 @@ pSetDelimiters :: Parser () pSetDelimiters = void $ do start <- gets openingDel- end <- gets closingDel+ end <- gets closingDel (void . symbol) (start <> "=") start' <- pDelimiter <* scn- end' <- pDelimiter <* scn+ end' <- pDelimiter <* scn (void . string) ("=" <> end)- modify' $ \st -> st { openingDel = start'- , closingDel = end'- }+ modify' $ \st ->+ st+ { openingDel = start',+ closingDel = end'+ } {-# INLINE pSetDelimiters #-} pEscapedVariable :: Parser Node@@ -141,14 +144,14 @@ pTag :: Text -> Parser Key pTag suffix = do start <- gets openingDel- end <- gets closingDel+ end <- gets closingDel between (symbol $ start <> suffix) (string end) pKey {-# INLINE pTag #-} pClosingTag :: Key -> Parser () pClosingTag key = do start <- gets openingDel- end <- gets closingDel+ end <- gets closingDel let str = keyToText key void $ between (symbol $ start <> "/") (string end) (symbol str) {-# INLINE pClosingTag #-}@@ -157,19 +160,20 @@ pKey = (fmap Key . lexeme . label "key") (implicit <|> other) where implicit = [] <$ char '.'- other = sepBy1 (takeWhile1P (Just lbl) f) (char '.')- lbl = "alphanumeric char or '-' or '_'"- f x = isAlphaNum x || x == '-' || x == '_'+ other = sepBy1 (takeWhile1P (Just lbl) f) (char '.')+ lbl = "alphanumeric char or '-' or '_'"+ f x = isAlphaNum x || x == '-' || x == '_' {-# INLINE pKey #-} pDelimiter :: Parser Text pDelimiter = takeWhile1P (Just "delimiter char") delChar <?> "delimiter"- where delChar x = not (isSpace x) && x /= '='+ where+ delChar x = not (isSpace x) && x /= '=' {-# INLINE pDelimiter #-} pBol :: Parser () pBol = do- o <- getOffset+ o <- getOffset o' <- gets newlineOffset unless (o == o') empty {-# INLINE pBol #-}@@ -178,18 +182,16 @@ -- Auxiliary types -- | Type of Mustache parser monad stack.- type Parser = StateT St (Parsec Void Text) -- | State used in the parser.- data St = St- { openingDel :: Text- -- ^ Opening delimiter- , closingDel :: Text- -- ^ Closing delimiter- , newlineOffset :: !Int- -- ^ The offset at which last newline character was parsed+ { -- | Opening delimiter+ openingDel :: Text,+ -- | Closing delimiter+ closingDel :: Text,+ -- | The offset at which last newline character was parsed+ newlineOffset :: !Int } ----------------------------------------------------------------------------@@ -222,6 +224,6 @@ eol' = do x <- eol o <- getOffset- modify' (\st -> st { newlineOffset = o } )+ modify' (\st -> st {newlineOffset = o}) return x {-# INLINE eol' #-}
Text/Mustache/Render.hs view
@@ -1,3 +1,6 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE OverloadedStrings #-}+ -- | -- Module : Text.Mustache.Render -- Copyright : © 2016–present Stack Builders@@ -10,32 +13,29 @@ -- Functions for rendering Mustache templates. You don't usually need to -- import the module, because "Text.Mustache" re-exports everything you may -- need, import that module instead.--{-# LANGUAGE CPP #-}-{-# LANGUAGE OverloadedStrings #-}- module Text.Mustache.Render- ( renderMustache- , renderMustacheW )+ ( renderMustache,+ renderMustacheW,+ ) where import Control.Monad.Reader-import Control.Monad.State.Strict (State, modify', execState)+import Control.Monad.State.Strict (State, execState, modify') import Data.Aeson import Data.Foldable (asum)+import qualified Data.HashMap.Strict as H import Data.List (tails) import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import qualified Data.Map as M import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL+import qualified Data.Text.Lazy.Builder as B+import qualified Data.Text.Lazy.Encoding as TL+import qualified Data.Vector as V import Text.Megaparsec.Pos (Pos, mkPos, unPos) import Text.Mustache.Type-import qualified Data.HashMap.Strict as H-import qualified Data.List.NonEmpty as NE-import qualified Data.Map as M-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL-import qualified Data.Text.Lazy.Builder as B-import qualified Data.Text.Lazy.Encoding as TL-import qualified Data.Vector as V #if !MIN_VERSION_base(4,13,0) import Data.Semigroup ((<>))@@ -47,19 +47,22 @@ -- | Synonym for the monad we use for rendering. It allows to share context -- and accumulate the result as 'B.Builder' data which is then turned into a -- lazy 'TL.Text'.- type Render a = ReaderT RenderContext (State S) a data S = S ([MustacheWarning] -> [MustacheWarning]) B.Builder -- | The render monad context.- data RenderContext = RenderContext- { rcIndent :: Maybe Pos -- ^ Actual indentation level- , rcContext :: NonEmpty Value -- ^ The context stack- , rcPrefix :: Key -- ^ Prefix accumulated by entering sections- , rcTemplate :: Template -- ^ The template to render- , rcLastNode :: Bool -- ^ Is this last node in this partial?+ { -- | Actual indentation level+ rcIndent :: Maybe Pos,+ -- | The context stack+ rcContext :: NonEmpty Value,+ -- | Prefix accumulated by entering sections+ rcPrefix :: Key,+ -- | The template to render+ rcTemplate :: Template,+ -- | Is this last node in this partial?+ rcLastNode :: Bool } ----------------------------------------------------------------------------@@ -67,20 +70,17 @@ -- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values -- for interpolation.- renderMustache :: Template -> Value -> TL.Text renderMustache t = snd . renderMustacheW t -- | Like 'renderMustache', but also returns a collection of warnings. -- -- @since 1.1.1- renderMustacheW :: Template -> Value -> ([MustacheWarning], TL.Text) renderMustacheW t = runRender (renderPartial (templateActual t) Nothing renderNode) t -- | Render a single 'Node'.- renderNode :: Node -> Render () renderNode (TextBlock txt) = outputIndented txt renderNode (EscapedVar k) =@@ -109,33 +109,31 @@ -- | Run 'Render' monad given template to render and a 'Value' to take -- values from.- runRender :: Render a -> Template -> Value -> ([MustacheWarning], TL.Text) runRender m t v = (ws [], B.toLazyText b) where S ws b = execState (runReaderT m rc) (S id mempty)- rc = RenderContext- { rcIndent = Nothing- , rcContext = v :| []- , rcPrefix = mempty- , rcTemplate = t- , rcLastNode = True }+ rc =+ RenderContext+ { rcIndent = Nothing,+ rcContext = v :| [],+ rcPrefix = mempty,+ rcTemplate = t,+ rcLastNode = True+ } {-# INLINE runRender #-} -- | Output a piece of strict 'Text'.- outputRaw :: Text -> Render () outputRaw = tellBuilder . B.fromText {-# INLINE outputRaw #-} -- | Output indentation consisting of appropriate number of spaces.- outputIndent :: Render () outputIndent = asks rcIndent >>= outputRaw . buildIndent {-# INLINE outputIndent #-} -- | Output piece of strict 'Text' with added indentation.- outputIndented :: Text -> Render () outputIndented txt = do level <- asks rcIndent@@ -147,24 +145,27 @@ {-# INLINE outputIndented #-} -- | Render a partial.--renderPartial- :: PName -- ^ Name of partial to render- -> Maybe Pos -- ^ Indentation level to use- -> (Node -> Render ()) -- ^ How to render nodes in that partial- -> Render ()+renderPartial ::+ -- | Name of partial to render+ PName ->+ -- | Indentation level to use+ Maybe Pos ->+ -- | How to render nodes in that partial+ (Node -> Render ()) ->+ Render () renderPartial pname i f = local u (outputIndent >> getNodes >>= renderMany f) where- u rc = rc- { rcIndent = addIndents i (rcIndent rc)- , rcPrefix = mempty- , rcTemplate = (rcTemplate rc) { templateActual = pname }- , rcLastNode = True }+ u rc =+ rc+ { rcIndent = addIndents i (rcIndent rc),+ rcPrefix = mempty,+ rcTemplate = (rcTemplate rc) {templateActual = pname},+ rcLastNode = True+ } {-# INLINE renderPartial #-} -- | Get collection of 'Node's for actual template.- getNodes :: Render [Node] getNodes = do Template actual cache <- asks rcTemplate@@ -172,21 +173,21 @@ {-# INLINE getNodes #-} -- | Render many nodes.--renderMany- :: (Node -> Render ()) -- ^ How to render a node- -> [Node] -- ^ The collection of nodes to render- -> Render ()+renderMany ::+ -- | How to render a node+ (Node -> Render ()) ->+ -- | The collection of nodes to render+ [Node] ->+ Render () renderMany _ [] = return () renderMany f [n] = do ln <- asks rcLastNode- local (\rc -> rc { rcLastNode = ln && rcLastNode rc }) (f n)-renderMany f (n:ns) = do- local (\rc -> rc { rcLastNode = False }) (f n)+ local (\rc -> rc {rcLastNode = ln && rcLastNode rc}) (f n)+renderMany f (n : ns) = do+ local (\rc -> rc {rcLastNode = False}) (f n) renderMany f ns -- | Lookup a 'Value' by its 'Key'.- lookupKey :: Key -> Render Value lookupKey (Key []) = NE.head <$> asks rcContext lookupKey k = do@@ -201,85 +202,79 @@ -- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as -- “path”.--simpleLookup- :: Bool- -- ^ At least one part of the path matched, in this case we are- -- “committed” to this lookup and cannot say “there is nothing, try- -- other level”. This is necessary to pass the “Dotted Names—Context- -- Precedence” test from the “interpolation.yml” spec.- -> Key -- ^ The key to lookup- -> Value -- ^ Source value- -> Maybe Value -- ^ Looked-up value-simpleLookup _ (Key []) obj = return obj-simpleLookup c (Key (k:ks)) (Object m) =+simpleLookup ::+ -- | At least one part of the path matched, in this case we are+ -- “committed” to this lookup and cannot say “there is nothing, try+ -- other level”. This is necessary to pass the “Dotted Names—Context+ -- Precedence” test from the “interpolation.yml” spec.+ Bool ->+ -- | The key to lookup+ Key ->+ -- | Source value+ Value ->+ -- | Looked-up value+ Maybe Value+simpleLookup _ (Key []) obj = return obj+simpleLookup c (Key (k : ks)) (Object m) = case H.lookup k m of Nothing -> if c then Just Null else Nothing- Just v -> simpleLookup True (Key ks) v+ Just v -> simpleLookup True (Key ks) v simpleLookup _ _ _ = Nothing {-# INLINE simpleLookup #-} -- | Enter the section by adding given 'Key' prefix to current prefix.- enterSection :: Key -> Render a -> Render a enterSection p =- local (\rc -> rc { rcPrefix = p <> rcPrefix rc })+ local (\rc -> rc {rcPrefix = p <> rcPrefix rc}) {-# INLINE enterSection #-} -- | Add new value on the top of context. The new value has the highest -- priority when lookup takes place.- addToLocalContext :: Value -> Render a -> Render a addToLocalContext v =- local (\rc -> rc { rcContext = NE.cons v (rcContext rc) })+ local (\rc -> rc {rcContext = NE.cons v (rcContext rc)}) {-# INLINE addToLocalContext #-} ---------------------------------------------------------------------------- -- Helpers -- | Register a warning.- tellWarning :: MustacheWarning -> Render ()-tellWarning w = modify' $ \(S ws b) -> S (ws . (w:)) b+tellWarning w = modify' $ \(S ws b) -> S (ws . (w :)) b -- | Register a piece of output.- tellBuilder :: B.Builder -> Render () tellBuilder b' = modify' $ \(S ws b) -> S ws (b <> b') -- | Add two @'Maybe' 'Pos'@ values together.- addIndents :: Maybe Pos -> Maybe Pos -> Maybe Pos-addIndents Nothing Nothing = Nothing-addIndents Nothing (Just x) = Just x-addIndents (Just x) Nothing = Just x+addIndents Nothing Nothing = Nothing+addIndents Nothing (Just x) = Just x+addIndents (Just x) Nothing = Just x addIndents (Just x) (Just y) = Just (mkPos $ unPos x + unPos y - 1) {-# INLINE addIndents #-} -- | Build indentation of specified length by repeating the space character.- buildIndent :: Maybe Pos -> Text buildIndent Nothing = "" buildIndent (Just p) = let n = fromIntegral (unPos p) - 1 in T.replicate n " " {-# INLINE buildIndent #-} -- | Select invisible values.- isBlank :: Value -> Bool-isBlank Null = True+isBlank Null = True isBlank (Bool False) = True-isBlank (Object m) = H.null m-isBlank (Array a) = V.null a-isBlank (String s) = T.null s-isBlank _ = False+isBlank (Object m) = H.null m+isBlank (Array a) = V.null a+isBlank (String s) = T.null s+isBlank _ = False {-# INLINE isBlank #-} -- | Render Aeson's 'Value' /without/ HTML escaping.- renderValue :: Key -> Value -> Render Text renderValue k v = case v of- Null -> return ""+ Null -> return "" String str -> return str Object _ -> do tellWarning (MustacheDirectlyRenderedValue k)@@ -293,12 +288,15 @@ {-# INLINE renderValue #-} -- | Escape HTML represented as strict 'Text'.- escapeHtml :: Text -> Text-escapeHtml txt = foldr (uncurry T.replace) txt- [ ("\"", """)- , ("'", "'")- , ("<", "<")- , (">", ">")- , ("&", "&") ]+escapeHtml txt =+ foldr+ (uncurry T.replace)+ txt+ [ ("\"", """),+ ("'", "'"),+ ("<", "<"),+ (">", ">"),+ ("&", "&")+ ] {-# INLINE escapeHtml #-}
Text/Mustache/Type.hs view
@@ -1,3 +1,10 @@+{-# LANGUAGE CPP #-}+{-# LANGUAGE DeriveDataTypeable #-}+{-# LANGUAGE DeriveGeneric #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell #-}+ -- | -- Module : Text.Mustache.Type -- Copyright : © 2016–present Stack Buliders@@ -10,37 +17,31 @@ -- Types used in the package. You don't usually need to import the module, -- because "Text.Mustache" re-exports everything you may need, import that -- module instead.--{-# LANGUAGE DeriveDataTypeable #-}-{-# LANGUAGE DeriveGeneric #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE TemplateHaskell #-}- module Text.Mustache.Type- ( Template (..)- , Node (..)- , Key (..)- , showKey- , PName (..)- , MustacheException (..)- , MustacheWarning (..)- , displayMustacheWarning )+ ( Template (..),+ Node (..),+ Key (..),+ showKey,+ PName (..),+ MustacheException (..),+ MustacheWarning (..),+ displayMustacheWarning,+ ) where import Control.DeepSeq-import Control.Exception (Exception(..))+import Control.Exception (Exception (..)) import Data.Data (Data) import Data.Map (Map)+import qualified Data.Map as M import Data.String (IsString (..)) import Data.Text (Text)+import qualified Data.Text as T import Data.Typeable (Typeable, cast) import Data.Void import GHC.Generics-import Text.Megaparsec-import qualified Data.Map as M-import qualified Data.Text as T import qualified Language.Haskell.TH.Syntax as TH+import Text.Megaparsec -- | Mustache template as name of “top-level” template and a collection of -- all available templates (partials).@@ -49,41 +50,51 @@ -- (and their caches) using the @('<>')@ operator, the resulting 'Template' -- will have the same currently selected template as the left one. Union of -- caches is also left-biased.- data Template = Template- { templateActual :: PName- -- ^ Name of currently “selected” template (top-level one).- , templateCache :: Map PName [Node]- -- ^ Collection of all templates that are available for interpolation+ { -- | Name of currently “selected” template (top-level one).+ templateActual :: PName,+ -- | Collection of all templates that are available for interpolation -- (as partials). The top-level one is also contained here and the -- “focus” can be switched easily by modifying 'templateActual'.- } deriving (Eq, Ord, Show, Data, Typeable, Generic)+ templateCache :: Map PName [Node]+ }+ deriving (Eq, Ord, Show, Data, Typeable, Generic) instance Semigroup Template where (Template pname x) <> (Template _ y) = Template pname (M.union x y) -- | @since 2.1.0- instance TH.Lift Template where lift = liftData --- | Structural element of template.+#if MIN_VERSION_template_haskell(2,16,0)+ liftTyped = TH.unsafeTExpCoerce . TH.lift+#endif +-- | Structural element of template. data Node- = TextBlock Text -- ^ Plain text contained between tags- | EscapedVar Key -- ^ HTML-escaped variable- | UnescapedVar Key -- ^ Unescaped variable- | Section Key [Node] -- ^ Mustache section- | InvertedSection Key [Node] -- ^ Inverted section- | Partial PName (Maybe Pos)- -- ^ Partial with indentation level ('Nothing' means it was inlined)+ = -- | Plain text contained between tags+ TextBlock Text+ | -- | HTML-escaped variable+ EscapedVar Key+ | -- | Unescaped variable+ UnescapedVar Key+ | -- | Mustache section+ Section Key [Node]+ | -- | Inverted section+ InvertedSection Key [Node]+ | -- | Partial with indentation level ('Nothing' means it was inlined)+ Partial PName (Maybe Pos) deriving (Eq, Ord, Show, Data, Typeable, Generic) -- | @since 2.1.0- instance TH.Lift Node where lift = liftData +#if MIN_VERSION_template_haskell(2,16,0)+ liftTyped = TH.unsafeTExpCoerce . TH.lift+#endif+ -- | Identifier for values to interpolate. -- -- The representation is the following:@@ -91,30 +102,30 @@ -- * @[]@—empty list means implicit iterators; -- * @[text]@—single key is a normal identifier; -- * @[text1, text2]@—multiple keys represent dotted names.--newtype Key = Key { unKey :: [Text] }+newtype Key = Key {unKey :: [Text]} deriving (Eq, Ord, Show, Semigroup, Monoid, Data, Typeable, Generic) instance NFData Key -- | @since 2.1.0- instance TH.Lift Key where lift = liftData +#if MIN_VERSION_template_haskell(2,16,0)+ liftTyped = TH.unsafeTExpCoerce . TH.lift+#endif+ -- | Pretty-print a key, this is helpful, for example, if you want to -- display an error message. -- -- @since 0.2.0- showKey :: Key -> Text showKey (Key []) = "<implicit>" showKey (Key xs) = T.intercalate "." xs -- | Identifier for partials. Note that with the @OverloadedStrings@ -- extension you can use just string literals to create values of this type.--newtype PName = PName { unPName :: Text }+newtype PName = PName {unPName :: Text} deriving (Eq, Ord, Show, Data, Typeable, Generic) instance IsString PName where@@ -123,20 +134,22 @@ instance NFData PName -- | @since 2.1.0- instance TH.Lift PName where lift = liftData +#if MIN_VERSION_template_haskell(2,16,0)+ liftTyped = TH.unsafeTExpCoerce . TH.lift+#endif+ -- | Exception that is thrown when parsing of a template fails or referenced -- values are not provided.- newtype MustacheException- = MustacheParserException (ParseErrorBundle Text Void)- -- ^ Template parser has failed. This contains the parse error.+ = -- | Template parser has failed. This contains the parse error. -- -- /Before version 0.2.0 it was called 'MustacheException'./ -- -- /The 'Text' field was added in version 1.0.0./+ MustacheParserException (ParseErrorBundle Text Void) deriving (Eq, Show, Typeable, Generic) instance Exception MustacheException where@@ -145,20 +158,18 @@ -- | Warning that may be generated during rendering of a 'Template'. -- -- @since 1.1.1- data MustacheWarning- = MustacheVariableNotFound Key- -- ^ The template contained a variable for which there was no data+ = -- | The template contained a variable for which there was no data -- counterpart in the current context.- | MustacheDirectlyRenderedValue Key- -- ^ A complex value such as an 'Object' or 'Array' was directly+ MustacheVariableNotFound Key+ | -- | A complex value such as an 'Object' or 'Array' was directly -- rendered into the template.+ MustacheDirectlyRenderedValue Key deriving (Eq, Show, Typeable, Generic) -- | Pretty-print a 'MustacheWarning'. -- -- @since 1.1.1- displayMustacheWarning :: MustacheWarning -> String displayMustacheWarning (MustacheVariableNotFound key) = "Referenced value was not provided, key: " ++ T.unpack (showKey key)
+ app/Main.hs view
@@ -0,0 +1,135 @@+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-}++module Main (main) where++import Data.Aeson (Value (..))+import qualified Data.Aeson as Aeson+import qualified Data.HashMap.Strict as HM+import Data.List (foldl')+import Data.List.NonEmpty (NonEmpty (..))+import qualified Data.List.NonEmpty as NE+import Data.Semigroup (sconcat)+import qualified Data.Text.Lazy.IO as T+import Data.Version (showVersion)+import qualified Data.Yaml as Yaml+import Development.GitRev+import Options.Applicative+import Paths_stache (version)+import System.Exit (exitFailure)+import System.FilePath (takeExtension)+import System.IO (hPutStrLn, stderr)+import Text.Mustache++main :: IO ()+main = do+ Opts {..} <- execParser optsParserInfo+ template <-+ sconcat+ <$> mapM (compileMustacheDir optTarget) optTemplateDirs+ context <-+ foldl' mergeContexts emptyContext+ <$> mapM loadContext optContextFiles+ let rendered = renderMustache template context+ case optOutputFile of+ Nothing -> T.putStrLn rendered+ Just ofile -> T.writeFile ofile rendered++----------------------------------------------------------------------------+-- Command line options parsing++-- | Command line options.+data Opts = Opts+ { -- | Context files.+ optContextFiles :: [FilePath],+ -- | Where to save the result.+ optOutputFile :: Maybe FilePath,+ -- | Name of the template to render.+ optTarget :: PName,+ -- | Directories with templates.+ optTemplateDirs :: NonEmpty FilePath+ }++optsParserInfo :: ParserInfo Opts+optsParserInfo =+ info (helper <*> ver <*> optsParser) . mconcat $+ [ fullDesc,+ progDesc "Command line interface to the Stache template processor",+ header "stache—a simple implementation of Mustache templates"+ ]+ where+ ver :: Parser (a -> a)+ ver =+ infoOption verStr . mconcat $+ [ long "version",+ short 'v',+ help "Print version of the program"+ ]+ verStr =+ unwords+ [ "stache",+ showVersion version,+ $gitBranch,+ $gitHash+ ]++optsParser :: Parser Opts+optsParser =+ Opts+ <$> many+ ( (strOption . mconcat)+ [ long "context",+ short 'c',+ metavar "CONTEXT",+ help "Context file in YAML or JSON format"+ ]+ )+ <*> (optional . strOption . mconcat)+ [ long "ofile",+ short 'o',+ metavar "OFILE",+ help "Save the rendered document to this file (otherwise write to stdout)"+ ]+ <*> (argument str . mconcat)+ [ metavar "TARGET",+ help "Name of the template to render"+ ]+ <*> (fmap NE.fromList . some)+ ( (argument str . mconcat)+ [ metavar "DIR",+ help "Template directories"+ ]+ )++----------------------------------------------------------------------------+-- Helpers++-- | File context from a YAML or JSON file.+loadContext :: FilePath -> IO Value+loadContext file = do+ let readYaml =+ either+ (Left . Yaml.prettyPrintParseException)+ Right+ <$> Yaml.decodeFileEither file+ econtext <- case takeExtension file of+ ".yml" -> readYaml+ ".yaml" -> readYaml+ _ -> Aeson.eitherDecodeFileStrict file+ case econtext of+ Left err -> spitErrorAndDie err+ Right v@(Aeson.Object _) -> return v+ Right _ -> spitErrorAndDie "context file should contain an object"++mergeContexts :: Value -> Value -> Value+mergeContexts (Aeson.Object m0) (Aeson.Object m1) =+ Aeson.Object (HM.union m0 m1)+mergeContexts _ _ = error "context merge failed"++emptyContext :: Value+emptyContext = Aeson.object []++spitErrorAndDie :: String -> IO a+spitErrorAndDie err = do+ hPutStrLn stderr err+ exitFailure
bench/Main.hs view
@@ -1,4 +1,4 @@-{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE OverloadedStrings #-} {-# OPTIONS_GHC -fno-warn-orphans #-} module Main (main) where@@ -7,88 +7,124 @@ import Criterion.Main import Data.Aeson import Data.Text (Text)+import qualified Data.Text.IO as T import Text.Megaparsec import Text.Mustache.Compile import Text.Mustache.Parser import Text.Mustache.Render import Text.Mustache.Type-import qualified Data.Text.IO as T ---------------------------------------------------------------------------- -- Benchmarks main :: IO ()-main = defaultMain [ parserBench, renderBench ]+main = defaultMain [parserBench, renderBench] parserBench :: Benchmark-parserBench = bgroup "parser"- [ bparser "simple block of text"- "bench-data/lorem-ipsum.mustache"- , bparser "text with escaped var"- "bench-data/escaped-var.mustache"- , bparser "text with unescaped var"- "bench-data/unescaped-var.mustache"- , bparser "text with unescaped var special"- "bench-data/unescaped-var-spec.mustache"- , bparser "a section"- "bench-data/section.mustache"- , bparser "an inverted section"- "bench-data/inverted-section.mustache"- , bparser "nested sections"- "bench-data/nested-sections.mustache"- , bparser "text with partial"- "bench-data/partial.mustache"- , bparser "comprehensive template"- "bench-data/comprehensive.mustache"- ]+parserBench =+ bgroup+ "parser"+ [ bparser+ "simple block of text"+ "bench-data/lorem-ipsum.mustache",+ bparser+ "text with escaped var"+ "bench-data/escaped-var.mustache",+ bparser+ "text with unescaped var"+ "bench-data/unescaped-var.mustache",+ bparser+ "text with unescaped var special"+ "bench-data/unescaped-var-spec.mustache",+ bparser+ "a section"+ "bench-data/section.mustache",+ bparser+ "an inverted section"+ "bench-data/inverted-section.mustache",+ bparser+ "nested sections"+ "bench-data/nested-sections.mustache",+ bparser+ "text with partial"+ "bench-data/partial.mustache",+ bparser+ "comprehensive template"+ "bench-data/comprehensive.mustache"+ ] renderBench :: Benchmark-renderBench = bgroup "render"- [ brender "simple block of text"- "bench-data/lorem-ipsum.mustache"- Null- , brender "text with escaped var"- "bench-data/escaped-var.mustache"- (object ["escaped-variable" .= ("escaped variable" :: Text)])- , brender "text with unescaped var"- "bench-data/unescaped-var.mustache"- (object ["unescaped-variable" .= ("unescaped variable" :: Text)])- , brender "text with unescaped var special"- "bench-data/unescaped-var-spec.mustache"- (object ["unescaped-variable" .= ("unescaped variable" :: Text)])- , brender "a section"- "bench-data/section.mustache"- (object ["section" .= True])- , brender "an inverted section"- "bench-data/inverted-section.mustache"- (object ["section" .= False])- , brender "nested sections"- "bench-data/nested-sections.mustache"- (object [ "section-a" .= True- , "section-b" .= True- , "section-c" .= True ])- , brender' "text with partial"- "bench-data/" "partial"- Null- , brender' "comprehensive template"- "bench-data/" "comprehensive"- (object [ "first-thing" .= ("the first thing" :: Text)- , "second-thing" .= ("the second thing" :: Text)- , "third-thing" .= ("the third thing" :: Text)- , "items" .= [1..10 :: Int] ])- ]+renderBench =+ bgroup+ "render"+ [ brender+ "simple block of text"+ "bench-data/lorem-ipsum.mustache"+ Null,+ brender+ "text with escaped var"+ "bench-data/escaped-var.mustache"+ (object ["escaped-variable" .= ("escaped variable" :: Text)]),+ brender+ "text with unescaped var"+ "bench-data/unescaped-var.mustache"+ (object ["unescaped-variable" .= ("unescaped variable" :: Text)]),+ brender+ "text with unescaped var special"+ "bench-data/unescaped-var-spec.mustache"+ (object ["unescaped-variable" .= ("unescaped variable" :: Text)]),+ brender+ "a section"+ "bench-data/section.mustache"+ (object ["section" .= True]),+ brender+ "an inverted section"+ "bench-data/inverted-section.mustache"+ (object ["section" .= False]),+ brender+ "nested sections"+ "bench-data/nested-sections.mustache"+ ( object+ [ "section-a" .= True,+ "section-b" .= True,+ "section-c" .= True+ ]+ ),+ brender'+ "text with partial"+ "bench-data/"+ "partial"+ Null,+ brender'+ "comprehensive template"+ "bench-data/"+ "comprehensive"+ ( object+ [ "first-thing" .= ("the first thing" :: Text),+ "second-thing" .= ("the second thing" :: Text),+ "third-thing" .= ("the third thing" :: Text),+ "items" .= [1 .. 10 :: Int]+ ]+ )+ ] bparser :: String -> FilePath -> Benchmark-bparser desc path = env (T.readFile path)- (bench desc . nf (either (error . errorBundlePretty) id . parseMustache path))+bparser desc path =+ env+ (T.readFile path)+ (bench desc . nf (either (error . errorBundlePretty) id . parseMustache path)) brender :: String -> FilePath -> Value -> Benchmark-brender desc path value = env (compileMustacheFile path)- (bench desc . nf (`renderMustache` value))+brender desc path value =+ env+ (compileMustacheFile path)+ (bench desc . nf (`renderMustache` value)) brender' :: String -> FilePath -> PName -> Value -> Benchmark-brender' desc path pname value = env (compileMustacheDir pname path)- (bench desc . nf (`renderMustache` value))+brender' desc path pname value =+ env+ (compileMustacheDir pname path)+ (bench desc . nf (`renderMustache` value)) ---------------------------------------------------------------------------- -- Orphan instances
mustache-spec/Spec.hs view
@@ -1,6 +1,6 @@ {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE RecordWildCards #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE RecordWildCards #-}+{-# LANGUAGE TemplateHaskell #-} module Main (main) where @@ -9,45 +9,45 @@ import Data.ByteString (ByteString) import Data.FileEmbed (embedFile) import Data.Map (Map, (!))+import qualified Data.Map as M import Data.Text (Text)+import qualified Data.Text as T+import qualified Data.Text.Lazy as TL import Data.Yaml import Test.Hspec import Text.Megaparsec import Text.Mustache import Text.Mustache.Parser-import qualified Data.Map as M-import qualified Data.Text as T-import qualified Data.Text.Lazy as TL -- | Representation of information contained in a Mustache spec file.- data SpecFile = SpecFile- { specOverview :: Text -- ^ Textual overview of this spec file- , specTests :: [Test] -- ^ The actual collection of tests+ { -- | Textual overview of this spec file+ specOverview :: Text,+ -- | The actual collection of tests+ specTests :: [Test] } instance FromJSON SpecFile where parseJSON = withObject "Mustache spec file" $ \o -> do specOverview <- o .: "overview"- specTests <- o .: "tests"+ specTests <- o .: "tests" return SpecFile {..} -- | Representation of a single test.- data Test = Test- { testName :: String- , testDesc :: String- , testData :: Value- , testTemplate :: Text- , testExpected :: Text- , testPartials :: Map Text Text+ { testName :: String,+ testDesc :: String,+ testData :: Value,+ testTemplate :: Text,+ testExpected :: Text,+ testPartials :: Map Text Text } instance FromJSON Test where parseJSON = withObject "Test" $ \o -> do- testName <- o .: "name"- testDesc <- o .: "desc"- testData <- o .: "data"+ testName <- o .: "name"+ testDesc <- o .: "desc"+ testData <- o .: "data" testTemplate <- o .: "template" testExpected <- o .: "expected" testPartials <- o .:? "partials" .!= M.empty@@ -58,12 +58,12 @@ spec :: Spec spec = do- specData "Comments" $(embedFile "specification/comments.yml")- specData "Delimiters" $(embedFile "specification/delimiters.yml")+ specData "Comments" $(embedFile "specification/comments.yml")+ specData "Delimiters" $(embedFile "specification/delimiters.yml") specData "Interpolation" $(embedFile "specification/interpolation.yml")- specData "Inverted" $(embedFile "specification/inverted.yml")- specData "Partials" $(embedFile "specification/partials.yml")- specData "Sections" $(embedFile "specification/sections.yml")+ specData "Inverted" $(embedFile "specification/inverted.yml")+ specData "Partials" $(embedFile "specification/partials.yml")+ specData "Sections" $(embedFile "specification/sections.yml") specData :: String -> ByteString -> Spec specData aspect bytes = describe aspect $ do@@ -82,7 +82,7 @@ let pname = PName k case parseMustache (T.unpack k) (testPartials ! k) of Left perr -> handleError perr >> undefined- Right ns -> return (pname, ns)+ Right ns -> return (pname, ns) let ps2 = M.fromList ps1 `M.union` templateCache TL.toStrict (renderMustache (Template templateActual ps2) testData) `shouldBe` testExpected
stache.cabal view
@@ -1,120 +1,163 @@-name: stache-version: 2.1.1-cabal-version: 1.18-tested-with: GHC==8.4.4, GHC==8.6.5, GHC==8.8.3-license: BSD3-license-file: LICENSE.md-author: Mark Karpov <markkarpov92@gmail.com>-maintainer: Mark Karpov <markkarpov92@gmail.com>-homepage: https://github.com/stackbuilders/stache-bug-reports: https://github.com/stackbuilders/stache/issues-category: Text-synopsis: Mustache templates for Haskell-build-type: Simple-description: Mustache templates for Haskell.-extra-doc-files: CHANGELOG.md- , README.md-data-files: bench-data/*.mustache- , specification/*.yml- , templates/*.mustache+cabal-version: 1.18+name: stache+version: 2.2.0+license: BSD3+license-file: LICENSE.md+maintainer: Mark Karpov <markkarpov92@gmail.com>+author: Mark Karpov <markkarpov92@gmail.com>+tested-with: ghc ==8.6.5 ghc ==8.8.3 ghc ==8.10.1+homepage: https://github.com/stackbuilders/stache+bug-reports: https://github.com/stackbuilders/stache/issues+synopsis: Mustache templates for Haskell+description: Mustache templates for Haskell.+category: Text+build-type: Simple+data-files:+ bench-data/*.mustache+ specification/*.yml+ templates/*.mustache +extra-doc-files:+ CHANGELOG.md+ README.md+ source-repository head- type: git- location: https://github.com/stackbuilders/stache.git+ type: git+ location: https://github.com/stackbuilders/stache.git flag dev- description: Turn on development settings.- manual: True- default: False+ description: Turn on development settings.+ default: False+ manual: True library- build-depends: aeson >= 0.11 && < 1.5- , base >= 4.11 && < 5.0- , bytestring >= 0.10 && < 0.11- , containers >= 0.5 && < 0.7- , deepseq >= 1.4 && < 1.5- , directory >= 1.2 && < 1.4- , filepath >= 1.2 && < 1.5- , megaparsec >= 7.0 && < 9.0- , mtl >= 2.1 && < 3.0- , template-haskell >= 2.11 && < 2.16- , text >= 1.2 && < 1.3- , unordered-containers >= 0.2.5 && < 0.3- , vector >= 0.11 && < 0.13- exposed-modules: Text.Mustache- , Text.Mustache.Compile- , Text.Mustache.Compile.TH- , Text.Mustache.Parser- , Text.Mustache.Render- , Text.Mustache.Type- if flag(dev)- ghc-options: -O0 -Wall -Werror -fsimpl-tick-factor=150- else- ghc-options: -O2 -Wall -fsimpl-tick-factor=150- if flag(dev)- ghc-options: -Wcompat- -Wincomplete-record-updates- -Wincomplete-uni-patterns- -Wnoncanonical-monad-instances- default-language: Haskell2010+ exposed-modules:+ Text.Mustache+ Text.Mustache.Compile+ Text.Mustache.Compile.TH+ Text.Mustache.Parser+ Text.Mustache.Render+ Text.Mustache.Type + default-language: Haskell2010+ build-depends:+ aeson >=0.11 && <1.6,+ base >=4.12 && <5.0,+ bytestring >=0.10 && <0.11,+ containers >=0.5 && <0.7,+ deepseq >=1.4 && <1.5,+ directory >=1.2 && <1.4,+ filepath >=1.2 && <1.5,+ megaparsec >=7.0 && <9.0,+ mtl >=2.1 && <3.0,+ template-haskell >=2.11 && <2.17,+ text >=1.2 && <1.3,+ unordered-containers >=0.2.5 && <0.3,+ vector >=0.11 && <0.13++ if flag(dev)+ ghc-options: -O0 -Wall -Werror -fsimpl-tick-factor=150++ else+ ghc-options: -O2 -Wall -fsimpl-tick-factor=150++ if flag(dev)+ ghc-options:+ -Wcompat -Wincomplete-record-updates -Wincomplete-uni-patterns+ -Wnoncanonical-monad-instances++executable stache+ main-is: Main.hs+ hs-source-dirs: app+ other-modules: Paths_stache+ default-language: Haskell2010+ build-depends:+ aeson >=0.11 && <1.6,+ base >=4.12 && <5.0,+ gitrev >=1.3 && <1.4,+ optparse-applicative >=0.14 && <0.16,+ stache >=2.0 && <3.0,+ text >=0.2 && <1.3,+ unordered-containers >=0.2.5 && <0.3,+ yaml >=0.8 && <0.12,+ filepath >=1.2 && <1.5++ if flag(dev)+ ghc-options:+ -Wall -Werror -Wcompat -Wincomplete-record-updates+ -Wincomplete-uni-patterns -Wnoncanonical-monad-instances++ else+ ghc-options: -O2 -Wall+ test-suite tests- main-is: Spec.hs- hs-source-dirs: tests- type: exitcode-stdio-1.0- build-depends: aeson >= 0.11 && < 1.5- , base >= 4.11 && < 5.0- , containers >= 0.5 && < 0.7- , hspec >= 2.0 && < 3.0- , hspec-megaparsec >= 2.0 && < 3.0- , megaparsec >= 7.0 && < 9.0- , stache- , template-haskell >= 2.11 && < 2.16- , text >= 1.2 && < 1.3- other-modules: Text.Mustache.Compile.THSpec- , Text.Mustache.ParserSpec- , Text.Mustache.RenderSpec- , Text.Mustache.TypeSpec- build-tools: hspec-discover >= 2.0 && < 3.0- if flag(dev)- ghc-options: -O0 -Wall -Werror- else- ghc-options: -O2 -Wall- default-language: Haskell2010+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ build-tools: hspec-discover >=2.0 && <3.0+ hs-source-dirs: tests+ other-modules:+ Text.Mustache.Compile.THSpec+ Text.Mustache.ParserSpec+ Text.Mustache.RenderSpec+ Text.Mustache.TypeSpec + default-language: Haskell2010+ build-depends:+ aeson >=0.11 && <1.6,+ base >=4.12 && <5.0,+ containers >=0.5 && <0.7,+ hspec >=2.0 && <3.0,+ hspec-megaparsec >=2.0 && <3.0,+ megaparsec >=7.0 && <9.0,+ stache -any,+ template-haskell >=2.11 && <2.17,+ text >=1.2 && <1.3++ if flag(dev)+ ghc-options: -O0 -Wall -Werror++ else+ ghc-options: -O2 -Wall+ test-suite mustache-spec- main-is: Spec.hs- hs-source-dirs: mustache-spec- type: exitcode-stdio-1.0- build-depends: aeson >= 0.11 && < 1.5- , base >= 4.11 && < 5.0- , bytestring >= 0.10 && < 0.11- , containers >= 0.5 && < 0.7- , file-embed- , hspec >= 2.0 && < 3.0- , megaparsec >= 7.0 && < 9.0- , stache- , text >= 1.2 && < 1.3- , yaml >= 0.8 && < 0.12- if flag(dev)- ghc-options: -Wall -Werror- else- ghc-options: -O2 -Wall- default-language: Haskell2010+ type: exitcode-stdio-1.0+ main-is: Spec.hs+ hs-source-dirs: mustache-spec+ default-language: Haskell2010+ build-depends:+ aeson >=0.11 && <1.6,+ base >=4.12 && <5.0,+ bytestring >=0.10 && <0.11,+ containers >=0.5 && <0.7,+ file-embed -any,+ hspec >=2.0 && <3.0,+ megaparsec >=7.0 && <9.0,+ stache -any,+ text >=1.2 && <1.3,+ yaml >=0.8 && <0.12 + if flag(dev)+ ghc-options: -Wall -Werror++ else+ ghc-options: -O2 -Wall+ benchmark bench- main-is: Main.hs- hs-source-dirs: bench- type: exitcode-stdio-1.0- build-depends: aeson >= 0.11 && < 1.5- , base >= 4.10 && < 5.0- , criterion >= 0.6.2.1 && < 1.6- , deepseq >= 1.4 && < 1.5- , megaparsec >= 7.0 && < 9.0- , stache- , text >= 1.2 && < 1.3- if flag(dev)- ghc-options: -O2 -Wall -Werror- else- ghc-options: -O2 -Wall- default-language: Haskell2010+ type: exitcode-stdio-1.0+ main-is: Main.hs+ hs-source-dirs: bench+ default-language: Haskell2010+ build-depends:+ aeson >=0.11 && <1.6,+ base >=4.12 && <5.0,+ criterion >=0.6.2.1 && <1.6,+ deepseq >=1.4 && <1.5,+ megaparsec >=7.0 && <9.0,+ stache -any,+ text >=1.2 && <1.3++ if flag(dev)+ ghc-options: -O2 -Wall -Werror++ else+ ghc-options: -O2 -Wall
tests/Text/Mustache/Compile/THSpec.hs view
@@ -1,18 +1,18 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-}-{-# LANGUAGE QuasiQuotes #-}-{-# LANGUAGE TemplateHaskell #-}+{-# LANGUAGE QuasiQuotes #-}+{-# LANGUAGE TemplateHaskell #-} module Text.Mustache.Compile.THSpec- ( main- , spec )+ ( main,+ spec,+ ) where +import qualified Data.Map as M import Test.Hspec--import Text.Mustache.Type-import qualified Data.Map as M import qualified Text.Mustache.Compile.TH as TH+import Text.Mustache.Type #if !MIN_VERSION_base(4,13,0) import Data.Semigroup ((<>))@@ -42,13 +42,16 @@ `shouldBe` (fooTemplate <> barTemplate) qqTemplate :: Template-qqTemplate = Template "quasi-quoted" $- M.singleton "quasi-quoted" [TextBlock "From Quasi-Quote!\n"]+qqTemplate =+ Template "quasi-quoted" $+ M.singleton "quasi-quoted" [TextBlock "From Quasi-Quote!\n"] fooTemplate :: Template-fooTemplate = Template "foo" $- M.singleton "foo" [TextBlock "This is the ‘foo’.\n"]+fooTemplate =+ Template "foo" $+ M.singleton "foo" [TextBlock "This is the ‘foo’.\n"] barTemplate :: Template-barTemplate = Template "bar" $- M.singleton "bar" [TextBlock "And this is the ‘bar’.\n"]+barTemplate =+ Template "bar" $+ M.singleton "bar" [TextBlock "And this is the ‘bar’.\n"]
tests/Text/Mustache/ParserSpec.hs view
@@ -1,9 +1,10 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Text.Mustache.ParserSpec- ( main- , spec )+ ( main,+ spec,+ ) where import Test.Hspec@@ -49,44 +50,47 @@ it "parses empty section" $ p "{{#section}}{{/section}}" `shouldParse` [Section (key "section") []] it "parses non-empty section" $- p "{{# section }}Hi, {{name}}!\n{{/section}}" `shouldParse`- [Section (key "section")- [ TextBlock "Hi, "- , EscapedVar (key "name")- , TextBlock "!\n"]]+ p "{{# section }}Hi, {{name}}!\n{{/section}}"+ `shouldParse` [ Section+ (key "section")+ [ TextBlock "Hi, ",+ EscapedVar (key "name"),+ TextBlock "!\n"+ ]+ ] context "when parsing an inverted section" $ do it "parses empty inverted section" $- p "{{^section}}{{/section}}" `shouldParse`- [InvertedSection (key "section") []]+ p "{{^section}}{{/section}}"+ `shouldParse` [InvertedSection (key "section") []] it "parses non-empty inverted section" $- p "{{^ section }}No one here?!\n{{/section}}" `shouldParse`- [InvertedSection (key "section") [TextBlock "No one here?!\n"]]+ p "{{^ section }}No one here?!\n{{/section}}"+ `shouldParse` [InvertedSection (key "section") [TextBlock "No one here?!\n"]] context "when parsing a partial" $ do it "parses a partial with white space" $- p "{{> that-s_my-partial }}" `shouldParse`- [Partial "that-s_my-partial" (Just $ mkPos 1)]+ p "{{> that-s_my-partial }}"+ `shouldParse` [Partial "that-s_my-partial" (Just $ mkPos 1)] it "parses a partial without white space" $- p "{{>that-s_my-partial}}" `shouldParse`- [Partial "that-s_my-partial" (Just $ mkPos 1)]+ p "{{>that-s_my-partial}}"+ `shouldParse` [Partial "that-s_my-partial" (Just $ mkPos 1)] it "handles indented partial correctly" $- p " {{> next_one }}" `shouldParse`- [Partial "next_one" (Just $ mkPos 4)]+ p " {{> next_one }}"+ `shouldParse` [Partial "next_one" (Just $ mkPos 4)] context "when running into delimiter change" $ do it "has effect" $- p "{{=<< >>=}}<<var>>{{var}}" `shouldParse`- [EscapedVar (key "var"), TextBlock "{{var}}"]+ p "{{=<< >>=}}<<var>>{{var}}"+ `shouldParse` [EscapedVar (key "var"), TextBlock "{{var}}"] it "handles whitespace just as well" $- p "{{=<< >>=}}<< var >>{{ var }}" `shouldParse`- [EscapedVar (key "var"), TextBlock "{{ var }}"]+ p "{{=<< >>=}}<< var >>{{ var }}"+ `shouldParse` [EscapedVar (key "var"), TextBlock "{{ var }}"] it "affects {{{s" $- p "{{=<< >>=}}<<{var}>>" `shouldParse`- [UnescapedVar (key "var")]+ p "{{=<< >>=}}<<{var}>>"+ `shouldParse` [UnescapedVar (key "var")] it "parses two subsequent delimiter changes" $- p "{{=(( ))=}}(( var ))((=-- $-=))--#section$---/section$-" `shouldParse`- [EscapedVar (key "var"), Section (key "section") []]+ p "{{=(( ))=}}(( var ))((=-- $-=))--#section$---/section$-"+ `shouldParse` [EscapedVar (key "var"), Section (key "section") []] it "propagates delimiter change from a nested scope" $- p "{{#section}}{{=<< >>=}}<</section>><<var>>" `shouldParse`- [Section (key "section") [], EscapedVar (key "var")]+ p "{{#section}}{{=<< >>=}}<</section>><<var>>"+ `shouldParse` [Section (key "section") [], EscapedVar (key "var")] context "when given malformed input" $ do it "rejects unclosed tags" $ do let s = "{{ name "
tests/Text/Mustache/RenderSpec.hs view
@@ -1,17 +1,18 @@ {-# LANGUAGE OverloadedStrings #-} module Text.Mustache.RenderSpec- ( main- , spec )+ ( main,+ spec,+ ) where -import Data.Aeson (object, KeyValue (..), Value (..))+import Data.Aeson (KeyValue (..), Value (..), object)+import qualified Data.Map as M import Data.Text (Text) import Test.Hspec import Text.Megaparsec import Text.Mustache.Render import Text.Mustache.Type-import qualified Data.Map as M main :: IO () main = hspec spec@@ -20,30 +21,32 @@ spec = describe "renderMustache" $ do let w ns value = let template = Template "test" (M.singleton "test" ns)- in fst (renderMustacheW template value)+ in fst (renderMustacheW template value) r ns value = let template = Template "test" (M.singleton "test" ns)- in renderMustache template value+ in renderMustache template value key = Key . pure it "leaves text block “as is”" $ r [TextBlock "a text block"] Null `shouldBe` "a text block" it "renders escaped variables correctly" $- r [EscapedVar (key "foo")]+ r+ [EscapedVar (key "foo")] (object ["foo" .= ("<html>&\"'something'\"</html>" :: Text)]) `shouldBe` "<html>&"'something'"</html>" it "renders unescaped variables “as is”" $- r [UnescapedVar (key "foo")]+ r+ [UnescapedVar (key "foo")] (object ["foo" .= ("<html>&\"something\"</html>" :: Text)]) `shouldBe` "<html>&\"something\"</html>" context "when rendering a variable" $ do context "when variable does not exist" $ it "generates correct warning" $- w [EscapedVar (key "foo")] (object []) `shouldBe`- [MustacheVariableNotFound (key "foo")]+ w [EscapedVar (key "foo")] (object [])+ `shouldBe` [MustacheVariableNotFound (key "foo")] context "when variable is not non-scalar" $ it "generates correct warning" $- w [EscapedVar (key "foo")] (object ["foo" .= object []]) `shouldBe`- [MustacheDirectlyRenderedValue (key "foo")]+ w [EscapedVar (key "foo")] (object ["foo" .= object []])+ `shouldBe` [MustacheDirectlyRenderedValue (key "foo")] context "when rendering a section" $ do let nodes = [Section (key "foo") [UnescapedVar (key "bar"), TextBlock "*"]] context "when the key is not present" $ do@@ -65,7 +68,8 @@ r nodes (object ["foo" .= ("" :: Text)]) `shouldBe` "" context "when the key is a Boolean true" $ it "renders the section without interpolation" $- r [Section (key "foo") [TextBlock "brr"]]+ r+ [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= object ["bar" .= True]]) `shouldBe` "brr" context "when the key is an object" $@@ -78,29 +82,34 @@ `shouldBe` "huh!*" context "when the key is a list of Boolean trues" $ it "renders the section as many times as there are elements" $- r [Section (key "foo") [TextBlock "brr"]]+ r+ [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= [True, True]]) `shouldBe` "brrbrr" context "when the key is a list of objects" $ it "renders the section many times changing context" $- r nodes (object ["foo" .= [object ["bar" .= x] | x <- [1..4] :: [Int]]])+ r nodes (object ["foo" .= [object ["bar" .= x] | x <- [1 .. 4] :: [Int]]]) `shouldBe` "1*2*3*4*" context "when the key is a number" $ do it "renders the section" $- r [Section (key "foo") [TextBlock "brr"]]+ r+ [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= (5 :: Int)]) `shouldBe` "brr" it "uses the key as context" $- r [Section (key "foo") [EscapedVar (Key [])]]+ r+ [Section (key "foo") [EscapedVar (Key [])]] (object ["foo" .= (5 :: Int)]) `shouldBe` "5" context "when the key is a non-empty string" $ do it "renders the section" $- r [Section (key "foo") [TextBlock "brr"]]+ r+ [Section (key "foo") [TextBlock "brr"]] (object ["foo" .= ("x" :: Text)]) `shouldBe` "brr" it "uses the key as context" $- r [Section (key "foo") [EscapedVar (Key [])]]+ r+ [Section (key "foo") [EscapedVar (Key [])]] (object ["foo" .= ("x" :: Text)]) `shouldBe` "x" context "when rendering an inverted section" $ do@@ -128,32 +137,48 @@ it "skips non-empty list" $ r nodes (object ["foo" .= [True]]) `shouldBe` "" context "when rendering a partial" $ do- let nodes = [ Partial "partial" (Just $ mkPos 4)- , TextBlock "*" ]+ let nodes =+ [ Partial "partial" (Just $ mkPos 4),+ TextBlock "*"+ ] it "skips missing partial" $ r nodes Null `shouldBe` " *" it "renders partial correctly" $- let template = Template "test" $- M.fromList [ ("test", nodes)- , ("partial", [TextBlock "one\ntwo\nthree"]) ]- in renderMustache template Null `shouldBe`- " one\n two\n three*"+ let template =+ Template "test" $+ M.fromList+ [ ("test", nodes),+ ("partial", [TextBlock "one\ntwo\nthree"])+ ]+ in renderMustache template Null+ `shouldBe` " one\n two\n three*" context "when rendering a nested partial" $ it "renders outer partial correctly" $- let template = Template "outer" $- M.fromList [ ("inner", [TextBlock "x"])- , ("middle", [Partial "inner" (Just $ mkPos 1)])- , ("outer", [Partial "middle" (Just $ mkPos 1)])- ]- in renderMustache template Null `shouldBe` "x"+ let template =+ Template "outer" $+ M.fromList+ [ ("inner", [TextBlock "x"]),+ ("middle", [Partial "inner" (Just $ mkPos 1)]),+ ("outer", [Partial "middle" (Just $ mkPos 1)])+ ]+ in renderMustache template Null `shouldBe` "x" context "when using dotted keys inside a section" $ it "it should be equivalent to access via one more section" $- r [ Section (key "things")- [ EscapedVar (Key ["atts", "color"])- , TextBlock " == "- , Section (key "atts") [EscapedVar (key "color")] ] ]- (object ["things" .=- [object- ["atts" .=- object ["color" .= ("blue" :: Text)]]]])+ r+ [ Section+ (key "things")+ [ EscapedVar (Key ["atts", "color"]),+ TextBlock " == ",+ Section (key "atts") [EscapedVar (key "color")]+ ]+ ]+ ( object+ [ "things"+ .= [ object+ [ "atts"+ .= object ["color" .= ("blue" :: Text)]+ ]+ ]+ ]+ ) `shouldBe` "blue == blue"
tests/Text/Mustache/TypeSpec.hs view
@@ -1,14 +1,15 @@-{-# LANGUAGE CPP #-}+{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} module Text.Mustache.TypeSpec- ( main- , spec )+ ( main,+ spec,+ ) where +import qualified Data.Map as M import Test.Hspec import Text.Mustache.Type-import qualified Data.Map as M #if !MIN_VERSION_base(4,13,0) import Data.Semigroup ((<>))@@ -21,24 +22,25 @@ spec = do describe "Template instances" $ context "the Semigroup instance" $ do- it "the resulting template inherits focus of the left one" $- templateActual (templateA <> templateB)- `shouldBe` templateActual templateA- it "the resulting template merges caches with left bias" $- templateCache (templateA <> templateB)- `shouldBe` M.fromList- [ ("c", [TextBlock "foo"])- , ("d", [TextBlock "bar"])- , ("e", [TextBlock "baz"]) ]+ it "the resulting template inherits focus of the left one" $+ templateActual (templateA <> templateB)+ `shouldBe` templateActual templateA+ it "the resulting template merges caches with left bias" $+ templateCache (templateA <> templateB)+ `shouldBe` M.fromList+ [ ("c", [TextBlock "foo"]),+ ("d", [TextBlock "bar"]),+ ("e", [TextBlock "baz"])+ ] describe "showKey" $ do context "when the key has no elements in it" $ it "is rendered correctly" $ showKey (Key []) `shouldBe` "<implicit>" context "when the key has some elements" $ it "is rendered correctly" $ do- showKey (Key ["boo"]) `shouldBe` "boo"- showKey (Key ["foo","bar"]) `shouldBe` "foo.bar"- showKey (Key ["baz","baz","quux"]) `shouldBe` "baz.baz.quux"+ showKey (Key ["boo"]) `shouldBe` "boo"+ showKey (Key ["foo", "bar"]) `shouldBe` "foo.bar"+ showKey (Key ["baz", "baz", "quux"]) `shouldBe` "baz.baz.quux" describe "displayMustacheWarning" $ do it "renders “not found” warning correctly" $ displayMustacheWarning (MustacheVariableNotFound (Key ["foo"]))@@ -48,11 +50,17 @@ `shouldBe` "Complex value rendered as such, key: foo" templateA :: Template-templateA = Template "a" $ M.fromList- [ ("c", [TextBlock "foo"])- , ("d", [TextBlock "bar"]) ]+templateA =+ Template "a" $+ M.fromList+ [ ("c", [TextBlock "foo"]),+ ("d", [TextBlock "bar"])+ ] templateB :: Template-templateB = Template "b" $ M.fromList- [ ("c", [TextBlock "bar"])- , ("e", [TextBlock "baz"]) ]+templateB =+ Template "b" $+ M.fromList+ [ ("c", [TextBlock "bar"]),+ ("e", [TextBlock "baz"])+ ]