packages feed

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 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 @@ [![Hackage](https://img.shields.io/hackage/v/stache.svg?style=flat)](https://hackage.haskell.org/package/stache) [![Stackage Nightly](http://stackage.org/package/stache/badge/nightly)](http://stackage.org/nightly/package/stache) [![Stackage LTS](http://stackage.org/package/stache/badge/lts)](http://stackage.org/lts/package/stache)-[![Build Status](https://travis-ci.org/stackbuilders/stache.svg?branch=master)](https://travis-ci.org/stackbuilders/stache)+![CI](https://github.com/stackbuilders/stache/workflows/CI/badge.svg?branch=master)  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-  [ ("\"", "&quot;")-  , ("'",  "&#39;")-  , ("<",  "&lt;")-  , (">",  "&gt;")-  , ("&",  "&amp;") ]+escapeHtml txt =+  foldr+    (uncurry T.replace)+    txt+    [ ("\"", "&quot;"),+      ("'", "&#39;"),+      ("<", "&lt;"),+      (">", "&gt;"),+      ("&", "&amp;")+    ] {-# 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` "&lt;html&gt;&amp;&quot;&#39;something&#39;&quot;&lt;/html&gt;"   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"])+      ]