packages feed

stache (empty) → 0.1.0

raw patch · 25 files changed

+2561/−0 lines, 25 filesdep +aesondep +basedep +bytestringsetup-changed

Dependencies added: aeson, base, bytestring, containers, directory, exceptions, file-embed, filepath, hspec, hspec-megaparsec, megaparsec, mtl, semigroups, stache, template-haskell, text, unordered-containers, vector, yaml

Files

+ CHANGELOG.md view
@@ -0,0 +1,3 @@+## Stache 0.1.0++* Initial release.
+ LICENSE.md view
@@ -0,0 +1,28 @@+Copyright © 2016 Stack Builders++All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++* Redistributions of source code must retain the above copyright notice,+  this list of conditions and the following disclaimer.++* Redistributions in binary form must reproduce the above copyright+  notice, this list of conditions and the following disclaimer in the+  documentation and/or other materials provided with the distribution.++* Neither the name Mark Karpov nor the names of contributors may be used to+  endorse or promote products derived from this software without specific+  prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY EXPRESS+OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES+OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN+NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY DIRECT, INDIRECT,+INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA,+OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF+LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING+NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE,+EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README.md view
@@ -0,0 +1,83 @@+# Stache++[![License BSD3](https://img.shields.io/badge/license-BSD3-brightgreen.svg)](http://opensource.org/licenses/BSD-3-Clause)+[![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)+[![Coverage Status](https://coveralls.io/repos/github/stackbuilders/stache/badge.svg?branch=master)](https://coveralls.io/github/stackbuilders/stache?branch=master)++This is a Haskell implementation of Mustache templates. The implementation+conforms to the version 1.1.3 of official [Mustache specification]+(https://github.com/mustache/spec). It's also faster than some alternative+Mustache implementations in Haskell because it uses `Data.Text.Lazy.Builder`+under the hood. It is extremely simple and straightforward to use with+minimal but complete API — three functions to compile templates (from+directory, from file, and from lazy text) and one to render them.++The implementation uses the Megaparsec parsing library to parse the+templates which is results in superior quality of error messages and is also+faster than Parsec-based ones.++For rendering you only need to create Aeson's `Value` where you put the data+to interpolate. Since the library re-uses Aeson's instances and most data+types in Haskell ecosystem are instances of classes like+`Data.Aeson.ToJSON`, the whole process is very simple for end user.++Template Haskell helpers for compilation of templates at compile time are+available in the `Text.Mustache.Compile.TH` module. The helpers are+currently available only for GHC 8 users though.++One feature that is not currently supported is lambdas. The feature is+marked as optional in the spec and can be emulated via processing of parsed+template representation. The decision to drop lambdas is intentional, for+the sake of simplicity and better integration with Aeson.++## Quick start++Here is an example of basic usage:++```haskell+{-# LANGUAGE OverloadedStrings #-}++module Main (main) where++import Data.Aeson+import Data.Text+import Text.Megaparsec+import Text.Mustache+import qualified Data.Text.Lazy.IO as TIO++main :: IO ()+main = do+  let res = compileMustacheText "foo"+        "Hi, {{name}}! You have:\n{{#things}}\n  * {{.}}\n{{/things}}\n"+  case res of+    Left err -> putStrLn (parseErrorPretty err)+    Right template -> TIO.putStr $ renderMustache template $ object+      [ "name"   .= ("John" :: Text)+      , "things" .= ["pen" :: Text, "candle", "egg"]+      ]+```++If I run the program, it prints the following:++```+Hi, John! You have:+  * pen+  * candle+  * egg+```++For more information about Mustache templates the following links may be+helpful:++* The official Mustache site: https://mustache.github.io/+* The manual: https://mustache.github.io/mustache.5.html+* The specification: https://github.com/mustache/spec++## License++Copyright © 2016 Stack Builders++Distributed under BSD 3 clause license.
+ Setup.hs view
@@ -0,0 +1,6 @@+module Main (main) where++import Distribution.Simple++main :: IO ()+main = defaultMain
+ Text/Mustache.hs view
@@ -0,0 +1,93 @@+-- |+-- Module      :  Text.Mustache+-- Copyright   :  © 2016 Stack Builders+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>+-- Stability   :  experimental+-- Portability :  portable+--+-- This is a Haskell implementation of Mustache templates. The+-- implementation conforms to the version 1.1.3 of official Mustache+-- specification <https://github.com/mustache/spec>. It's also faster than+-- some alternative Mustache implementations in Haskell because it uses+-- "Data.Text.Lazy.Builder" under the hood. It is extremely simple and+-- straightforward to use with minimal but complete API — three functions to+-- compile templates (from directory, from file, and from lazy text) and one+-- to render them.+--+-- The implementation uses the Megaparsec parsing library to parse the+-- templates which is results in superior quality of error messages and is+-- also faster than Parsec-based ones.+--+-- For rendering you only need to create Aeson's 'Data.Aeson.Value' where+-- you put the data to interpolate. Since the library re-uses Aeson's+-- instances and most data types in Haskell ecosystem are instances of+-- classes like 'Data.Aeson.ToJSON', the whole process is very simple for+-- end user.+--+-- Template Haskell helpers for compilation of templates at compile time are+-- available in the "Text.Mustache.Compile.TH" module. The helpers are+-- currently available only for GHC 8 users though.+--+-- One feature that is not currently supported is lambdas. The feature is+-- marked as optional in the spec and can be emulated via processing of+-- parsed template representation. The decision to drop lambdas is+-- intentional, for the sake of simplicity and better integration with+-- Aeson.+--+-- Here is an example of basic usage:+--+-- > {-# LANGUAGE OverloadedStrings #-}+-- >+-- > module Main (main) where+-- >+-- > import Data.Aeson+-- > import Data.Text+-- > import Text.Megaparsec+-- > import Text.Mustache+-- > import qualified Data.Text.Lazy.IO as TIO+-- >+-- > main :: IO ()+-- > main = do+-- >   let res = compileMustacheText "foo"+-- >         "Hi, {{name}}! You have:\n{{#things}}\n  * {{.}}\n{{/things}}\n"+-- >   case res of+-- >     Left err -> putStrLn (parseErrorPretty err)+-- >     Right template -> TIO.putStr $ renderMustache template $ object+-- >       [ "name"   .= ("John" :: Text)+-- >       , "things" .= ["pen" :: Text, "candle", "egg"]+-- >       ]+--+-- If I run the program, it prints the following:+--+-- > Hi, John! You have:+-- >   * pen+-- >   * candle+-- >   * egg+--+-- For more information about Mustache templates the following links may be+-- helpful:+--+--     * The official Mustache site: <https://mustache.github.io/>+--     * The manual: <https://mustache.github.io/mustache.5.html>+--     * The specification: <https://github.com/mustache/spec>++module Text.Mustache+  ( -- * Types+    Template (..)+  , Node (..)+  , Key (..)+  , PName (..)+  , MustacheException (..)+    -- * Compiling+  , compileMustacheDir+  , compileMustacheFile+  , compileMustacheText+    -- * Rendering+  , renderMustache )+where++import Text.Mustache.Compile+import Text.Mustache.Render+import Text.Mustache.Type
+ Text/Mustache/Compile.hs view
@@ -0,0 +1,103 @@+-- |+-- Module      :  Text.Mustache.Compile+-- Copyright   :  © 2016 Stack Builders+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>+-- Stability   :  experimental+-- Portability :  portable+--+-- 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.++{-# LANGUAGE CPP #-}++module Text.Mustache.Compile+  ( compileMustacheDir+  , compileMustacheFile+  , compileMustacheText )+where++import Control.Monad.Catch (MonadThrow (..))+import Control.Monad.Except+import Data.Text.Lazy (Text)+import System.Directory+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.Lazy.IO as TL+import qualified System.FilePath   as F++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative ((<$>))+#endif++-- | Compile all templates in specified directory and select one. Template+-- files should have extension @mustache@, (e.g. @foo.mustache@) to be+-- recognized. This function /does not/ scan the directory recursively.+--+-- The action can throw the same exceptions as 'getDirectoryContents', and+-- 'T.readFile'.++compileMustacheDir :: (MonadIO m, MonadThrow m)+  => PName             -- ^ Which template to select after compiling+  -> FilePath          -- ^ Directory with templates+  -> m Template        -- ^ The resulting template+compileMustacheDir pname path =+  liftIO (getDirectoryContents path) >>=+  filterM isMustacheFile . liftM (F.combine (F.takeDirectory path)) >>=+  liftM selectKey . foldM f (Template undefined M.empty)+  where+    selectKey t = t { templateActual = pname }+    f (Template _ old) fp = do+      Template _ new <- compileMustacheFile fp+      return (Template undefined (M.union new old))++-- | Compile single Mustache template and select it.+--+-- The action can throw the same exceptions as 'T.readFile'.++compileMustacheFile :: (MonadIO m, MonadThrow m)+  => FilePath          -- ^ Location of the file+  -> m Template+compileMustacheFile path = liftIO (TL.readFile path) >>=+  withException . compileMustacheText (pathToPName path)++-- | 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 (ParseError Char Dec) Template -- ^ The result+compileMustacheText pname txt =+  Template pname . M.singleton pname <$> parseMustache "" txt++----------------------------------------------------------------------------+-- Helpers++-- | Check if given 'FilePath' point to a mustache file.++isMustacheFile :: MonadIO m => FilePath -> m Bool+isMustacheFile path = do+  exists <- liftIO (doesFileExist path)+  let rightExtension = F.takeExtension path == ".mustache"+  return (exists && rightExtension)++-- | Build a 'PName' from given 'FilePath'.++pathToPName :: FilePath -> PName+pathToPName = PName . T.pack . F.takeBaseName+{-# INLINE pathToPName #-}++-- | Throw 'MustacheException' if argument is 'Left' or return the result+-- inside 'Right'.++withException :: MonadThrow m+  => Either (ParseError Char Dec) Template -- ^ Value to process+  -> m Template        -- ^ The result+withException = either (throwM . MustacheException) return+{-# INLINE withException #-}
+ Text/Mustache/Compile/TH.hs view
@@ -0,0 +1,96 @@+-- |+-- Module      :  Text.Mustache.Compile.TH+-- Copyright   :  © 2016 Stack Builders+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>+-- Stability   :  experimental+-- Portability :  portable+--+-- Template Haskell helpers to compile Mustache templates at compile time.+-- This module is not imported as part of "Text.Mustache", so you need to+-- import it yourself. Qualified import is recommended, but not necessary.+--+-- At the moment, functions in this module only work with GHC 8 (they+-- require at least @template-haskell-2.11@).++{-# LANGUAGE CPP             #-}+{-# LANGUAGE RankNTypes      #-}+{-# LANGUAGE TemplateHaskell #-}++module Text.Mustache.Compile.TH+  ( compileMustacheDir+  , compileMustacheFile+  , compileMustacheText )+where++import Control.Monad.Catch (try)+import Data.Text.Lazy (Text)+import Data.Typeable (cast)+import Language.Haskell.TH hiding (Dec)+import Language.Haskell.TH.Syntax (lift)+import Text.Megaparsec hiding (try)+import Text.Mustache.Type+import qualified Data.Text             as T+import qualified Text.Mustache.Compile as C++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative+#endif++#if MIN_VERSION_template_haskell(2,11,0)+import Language.Haskell.TH.Syntax (dataToExpQ)+#else+import Data.Data (Data)+dataToExpQ :: Data a => (forall b. Data b => b -> Maybe (Q Exp)) -> a -> Q Exp+dataToExpQ _ _ = fail "The feature requires at least GHC 8 to work"+#endif++-- | Compile all templates in specified directory and select one. Template+-- files should have extension @mustache@, (e.g. @foo.mustache@) to be+-- recognized. This function /does not/ scan the directory recursively.+--+-- This version compiles the templates at compile time.++compileMustacheDir+  :: PName             -- ^ Which template to select after compiling+  -> FilePath          -- ^ Directory with templates+  -> Q Exp             -- ^ The resulting template+compileMustacheDir pname path =+  (runIO . try) (C.compileMustacheDir pname path) >>= handleEither++-- | Compile single Mustache template and select it.+--+-- This version compiles the template at compile time.++compileMustacheFile+  :: FilePath          -- ^ Location of the file+  -> Q Exp+compileMustacheFile path =+  (runIO . try) (C.compileMustacheFile path) >>= handleEither++-- | Compile Mustache template from 'Text' value. The cache will contain+-- 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 pname text =+  handleEither (C.compileMustacheText pname text)++-- | Given an 'Either' result return 'Right' and signal pretty-printed error+-- if we have a 'Left'.++handleEither :: Either (ParseError Char Dec) Template -> Q Exp+handleEither val =+  case val of+    Left err -> fail (parseErrorPretty err)+    Right template -> dataToExpQ (fmap liftText . cast) template++-- | Lift strict 'T.Text' to 'Q' 'Exp'.++liftText :: T.Text -> Q Exp+liftText txt = AppE (VarE 'T.pack) <$> lift (T.unpack txt)
+ Text/Mustache/Parser.hs view
@@ -0,0 +1,197 @@+-- |+-- Module      :  Text.Mustache.Parser+-- Copyright   :  © 2016 Stack Builders+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>+-- Stability   :  experimental+-- Portability :  portable+--+-- 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.++module Text.Mustache.Parser+  ( parseMustache )+where++import Control.Applicative+import Control.Monad+import Control.Monad.State.Lazy+import Data.Char (isSpace)+import Data.List (intercalate)+import Data.Maybe (catMaybes)+import Data.Text.Lazy (Text)+import Text.Megaparsec+import Text.Mustache.Type+import qualified Data.Text             as T+import qualified Text.Megaparsec.Lexer as L++----------------------------------------------------------------------------+-- Parser++-- | Parse given Mustache template.++parseMustache+  :: FilePath+     -- ^ Location of file to parse+  -> Text+     -- ^ File contents (Mustache template)+  -> Either (ParseError Char Dec) [Node]+     -- ^ Parsed nodes or parse error+parseMustache = parse $+  evalStateT (pMustache eof) (Delimiters "{{" "}}")++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 ]+{-# INLINE pMustache #-}++pTextBlock :: Parser Node+pTextBlock = do+  start <- gets openingDel+  (void . notFollowedBy . string) start+  let terminator = choice+        [ (void . lookAhead . string) start+        , pBol+        , eof ]+  TextBlock . T.pack <$> someTill anyChar terminator+{-# INLINE pTextBlock #-}++pUnescapedVariable :: Parser Node+pUnescapedVariable = UnescapedVar <$> pTag "&"+{-# INLINE pUnescapedVariable #-}++pUnescapedSpecial :: Parser Node+pUnescapedSpecial =+  UnescapedVar <$> between (symbol "{{{") (string "}}}") pKey+{-# INLINE pUnescapedSpecial #-}++pSection :: String -> (Key -> [Node] -> Node) -> Parser Node+pSection suffix f = do+  key   <- withStandalone (pTag suffix)+  nodes <- (pMustache . withStandalone . pClosingTag) key+  return (f key nodes)+{-# INLINE pSection #-}++pPartial :: (Pos -> Maybe Pos) -> Parser Node+pPartial f = do+  pos <- f <$> L.indentLevel+  key <- pTag ">"+  let pname = PName $ T.intercalate (T.pack ".") (unKey key)+  return (Partial pname pos)+{-# INLINE pPartial #-}++pComment :: Parser ()+pComment = void $ do+  start <- gets openingDel+  end   <- gets closingDel+  (void . symbol) (start ++ "!")+  manyTill anyChar (string end)+{-# INLINE pComment #-}++pSetDelimiters :: Parser ()+pSetDelimiters = void $ do+  start <- gets openingDel+  end   <- gets closingDel+  (void . symbol) (start ++ "=")+  start' <- pDelimiter <* scn+  end'   <- pDelimiter <* scn+  (void . string) ("=" ++ end)+  put (Delimiters start' end')+{-# INLINE pSetDelimiters #-}++pEscapedVariable :: Parser Node+pEscapedVariable = EscapedVar <$> pTag ""+{-# INLINE pEscapedVariable #-}++withStandalone :: Parser a -> Parser a+withStandalone p = pStandalone p <|> p+{-# INLINE withStandalone #-}++pStandalone :: Parser a -> Parser a+pStandalone p = pBol *> try (between sc (sc <* (void eol <|> eof)) p)+{-# INLINE pStandalone #-}++pTag :: String -> Parser Key+pTag suffix = do+  start <- gets openingDel+  end   <- gets closingDel+  between (symbol $ start ++ suffix) (string end) pKey+{-# INLINE pTag #-}++pClosingTag :: Key -> Parser ()+pClosingTag key = do+  start <- gets openingDel+  end   <- gets closingDel+  let str = keyToString key+  void $ between (symbol $ start ++ "/") (string end) (symbol str)+{-# INLINE pClosingTag #-}++pKey :: Parser Key+pKey = (fmap Key . lexeme . label "key") (implicit <|> other)+  where+    implicit = [] <$ char '.'+    other    = sepBy1 (T.pack <$> some ch) (char '.')+    ch       = alphaNumChar <|> oneOf "-_"+{-# INLINE pKey #-}++pDelimiter :: Parser String+pDelimiter = some (satisfy delChar) <?> "delimiter"+  where delChar x = not (isSpace x) && x /= '='+{-# INLINE pDelimiter #-}++pBol :: Parser ()+pBol = do+  level <- L.indentLevel+  unless (level == unsafePos 1) empty+{-# INLINE pBol #-}++----------------------------------------------------------------------------+-- Auxiliary types++-- | Type of Mustache parser monad stack.++type Parser = StateT Delimiters (Parsec Dec Text)++-- | State used in Mustache parser. It includes currently set opening and+-- closing delimiters.++data Delimiters = Delimiters+  { openingDel :: String+  , closingDel :: String }++----------------------------------------------------------------------------+-- Lexer helpers and other++scn :: Parser ()+scn = L.space (void spaceChar) empty empty+{-# INLINE scn #-}++sc :: Parser ()+sc = L.space (void $ oneOf " \t") empty empty+{-# INLINE sc #-}++lexeme :: Parser a -> Parser a+lexeme = L.lexeme scn+{-# INLINE lexeme #-}++symbol :: String -> Parser String+symbol = L.symbol scn+{-# INLINE symbol #-}++keyToString :: Key -> String+keyToString (Key []) = "."+keyToString (Key ks) = intercalate "." (T.unpack <$> ks)+{-# INLINE keyToString #-}
+ Text/Mustache/Render.hs view
@@ -0,0 +1,270 @@+-- |+-- Module      :  Text.Mustache.Render+-- Copyright   :  © 2016 Stack Builders+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>+-- Stability   :  experimental+-- Portability :  portable+--+-- 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 GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+{-# LANGUAGE RecordWildCards            #-}++module Text.Mustache.Render+  ( renderMustache )+where++import Control.Monad.Reader+import Control.Monad.Writer.Lazy+import Data.Aeson+import Data.Foldable (asum)+import Data.List (tails)+import Data.List.NonEmpty (NonEmpty (..))+import Data.Maybe (fromMaybe)+import Data.Text (Text)+import Text.Megaparsec.Pos (Pos, unPos)+import Text.Mustache.Type+import qualified Data.ByteString.Lazy   as B+import qualified Data.HashMap.Strict    as H+import qualified Data.List.NonEmpty     as NE+import qualified Data.Map               as M+import qualified Data.Semigroup         as S+import qualified Data.Text              as T+import qualified Data.Text.Encoding     as T+import qualified Data.Text.Lazy         as TL+import qualified Data.Text.Lazy.Builder as B+import qualified Data.Vector            as V++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative+#endif++----------------------------------------------------------------------------+-- The rendering monad++-- | 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+-- lazy 'TL.Text'.++type Render a = ReaderT RenderContext (Writer B.Builder) a++-- | 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?+  }++----------------------------------------------------------------------------+-- High-level interface++-- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values+-- for interpolation.++renderMustache :: Template -> Value -> TL.Text+renderMustache t =+  runRender (renderPartial (templateActual t) Nothing renderNode) t++-- | Render a single 'Node'.++renderNode :: Node -> Render ()+renderNode (TextBlock txt) = outputIndented txt+renderNode (EscapedVar k) =+  lookupKey k >>= outputRaw . escapeHtml . renderValue+renderNode (UnescapedVar k) =+  lookupKey k >>= outputRaw . renderValue+renderNode (Section k ns) = do+  val <- lookupKey k+  enterSection k $+    unless (isBlank val) $+      case val of+        Object _ ->+          addToLocalContext val (renderMany renderNode ns)+        Array xs ->+          forM_ (V.toList xs) $ \x ->+            addToLocalContext x (renderMany renderNode ns)+        Bool True ->+          renderMany renderNode ns+        String _  ->+          renderMany renderNode ns+        _ ->+          return ()+renderNode (InvertedSection k ns) = do+  val <- lookupKey k+  when (isBlank val) $+    renderMany renderNode ns+renderNode (Partial pname indent) =+  renderPartial pname indent renderNode++----------------------------------------------------------------------------+-- The rendering monad vocabulary++-- | Run 'Render' monad given template to render and a 'Value' to take+-- values from.++runRender :: Render a -> Template -> Value -> TL.Text+runRender m t v = (B.toLazyText . execWriter) (runReaderT m rc)+  where+    rc = RenderContext+      { rcIndent   = Nothing+      , rcContext  = v :| []+      , rcPrefix   = mempty+      , rcTemplate = t+      , rcLastNode = True }+{-# INLINE runRender #-}++-- | Output a piece of strict 'Text'.++outputRaw :: Text -> Render ()+outputRaw = tell . 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+  lnode <- asks rcLastNode+  let f x = outputRaw (T.replace "\n" ("\n" <> buildIndent level) x)+  if lnode && T.isSuffixOf "\n" txt+    then f (T.init txt) >> outputRaw "\n"+    else f txt+{-# 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 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 }+{-# INLINE renderPartial #-}++-- | Get collection of 'Node's for actual template.++getNodes :: Render [Node]+getNodes = do+  Template actual cache <- asks rcTemplate+  return (M.findWithDefault [] actual cache)+{-# INLINE getNodes #-}++-- | Render many nodes.++renderMany+  :: (Node -> Render ()) -- ^ How to render a node+  -> [Node]            -- ^ The collection of nodes to render+  -> 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)+  renderMany f ns++-- | Lookup a 'Value' by its 'Key'.++lookupKey :: Key -> Render Value+lookupKey (Key []) = NE.head <$> asks rcContext+lookupKey k = do+  v <- asks rcContext+  p <- asks rcPrefix+  return . fromMaybe Null $+    if (null . drop 1 . unKey) k+      then let f x = asum (simpleLookup (x <> k) <$> v)+           in asum (fmap (f . Key) . reverse . tails $ unKey p)+      else asum (simpleLookup (p <> k) <$> v)++-- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as+-- “path”.++simpleLookup :: Key -> Value -> Maybe Value+simpleLookup (Key [])     obj        = return obj+simpleLookup (Key (k:ks)) (Object m) = H.lookup k m >>= simpleLookup (Key ks)+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 })+{-# 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) })+{-# INLINE addToLocalContext #-}++----------------------------------------------------------------------------+-- Helpers++-- | 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 (Just x) (Just y) = Just (x S.<> y)+{-# INLINE addIndents #-}++-- | Build intentation 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 (Bool False) = True+isBlank (Object   m) = H.null m+isBlank (Array    a) = V.null a+isBlank _            = False+{-# INLINE isBlank #-}++-- | Render Aeson's 'Value' /without/ HTML escaping.++renderValue :: Value -> Text+renderValue Null         = ""+renderValue (String str) = str+renderValue value        = (T.decodeUtf8 . B.toStrict . encode) value+{-# INLINE renderValue #-}++-- | Escape HTML represented as strict 'Text'.++escapeHtml :: Text -> Text+escapeHtml txt = foldr (uncurry T.replace) txt+  [ ("\"", "&quot;")+  , ("<",  "&lt;")+  , (">",  "&gt;")+  , ("&",  "&amp;") ]+{-# INLINE escapeHtml #-}
+ Text/Mustache/Type.hs view
@@ -0,0 +1,93 @@+-- |+-- Module      :  Text.Mustache.Type+-- Copyright   :  © 2016 Stack Buliders+-- License     :  BSD 3 clause+--+-- Maintainer  :  Mark Karpov <markkarpov@openmailbox.org>+-- Stability   :  experimental+-- Portability :  portable+--+-- Types used by 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 GeneralizedNewtypeDeriving #-}++module Text.Mustache.Type+  ( Template (..)+  , Node (..)+  , Key (..)+  , PName (..)+  , MustacheException (..) )+where++import Control.Monad.Catch (Exception)+import Data.Data (Data)+import Data.Map (Map)+import Data.Semigroup+import Data.String (IsString (..))+import Data.Text (Text)+import Data.Typeable (Typeable)+import Text.Megaparsec+import qualified Data.Map  as M+import qualified Data.Text as T++-- | Mustache template as name of “top-level” template and a collection of+-- all available templates (partials).+--+-- 'Template' is a 'Semigroup'. This means that you can combine 'Template's+-- (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+    -- (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)++instance Semigroup Template where+  (Template pname x) <> (Template _ y) = Template pname (M.union x y)++-- | 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)+  deriving (Eq, Ord, Show, Data, Typeable)++-- | Identifier for values to interpolate.+--+-- The representation is the following:+--+--     * @[]@ — empty list means implicit iterators;+--     * @[text]@ — single key is a normal identifier;+--     * @[text1, text2]@ — multiple keys represent dotted names.++newtype Key = Key { unKey :: [Text] }+  deriving (Eq, Ord, Show, Monoid, Data, Typeable)++-- | 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 }+  deriving (Eq, Ord, Show, Data, Typeable)++instance IsString PName where+  fromString = PName . T.pack++-- | Exception that is thrown when parsing of a template has failed.++data MustacheException = MustacheException (ParseError Char Dec)+  deriving (Eq, Show, Typeable)++instance Exception MustacheException
+ mustache-spec/Spec.hs view
@@ -0,0 +1,88 @@+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RecordWildCards   #-}+{-# LANGUAGE TemplateHaskell   #-}++module Main (main) where++import Control.Monad+import Data.Aeson+import Data.ByteString (ByteString)+import Data.FileEmbed (embedFile)+import Data.Map (Map, (!))+import Data.Text (Text)+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+  }++instance FromJSON SpecFile where+  parseJSON = withObject "Mustache spec file" $ \o -> do+    specOverview <- o .: "overview"+    specTests    <- o .: "tests"+    return SpecFile {..}++-- | Representation of a single test.++data Test = Test+  { testName     :: String+  , testDesc     :: String+  , testData     :: Value+  , testTemplate :: TL.Text+  , testExpected :: TL.Text+  , testPartials :: Map Text TL.Text+  }++instance FromJSON Test where+  parseJSON = withObject "Test" $ \o -> do+    testName     <- o .: "name"+    testDesc     <- o .: "desc"+    testData     <- o .: "data"+    testTemplate <- o .: "template"+    testExpected <- o .: "expected"+    testPartials <- o .:? "partials" .!= M.empty+    return Test {..}++main :: IO ()+main = hspec spec++spec :: Spec+spec = do+  specData "Comments"      $(embedFile "specs/comments.yml")+  specData "Delimiters"    $(embedFile "specs/delimiters.yml")+  specData "Interpolation" $(embedFile "specs/interpolation.yml")+  specData "Inverted"      $(embedFile "specs/inverted.yml")+  specData "Partials"      $(embedFile "specs/partials.yml")+  specData "Sections"      $(embedFile "specs/sections.yml")++specData :: String -> ByteString -> Spec+specData aspect bytes = describe aspect $ do+  let handleError = expectationFailure . parseErrorPretty+  case decodeEither' bytes of+    Left err ->+      it "should load YAML specs first" $+        expectationFailure (prettyPrintParseException err)+    Right SpecFile {..} ->+      forM_ specTests $ \Test {..} ->+        it (testName ++ ": " ++ testDesc) $+          case compileMustacheText (PName $ T.pack testName) testTemplate of+            Left perr -> handleError perr+            Right Template {..} -> do+              ps1 <- forM (M.keys testPartials) $ \k -> do+                let pname = PName k+                case parseMustache (T.unpack k) (testPartials ! k) of+                  Left perr -> handleError perr >> undefined+                  Right ns  -> return (pname, ns)+              let ps2 = M.fromList ps1 `M.union` templateCache+              renderMustache (Template templateActual ps2) testData+                `shouldBe` testExpected
+ specs/comments.yml view
@@ -0,0 +1,103 @@+overview: |+  Comment tags represent content that should never appear in the resulting+  output.++  The tag's content may contain any substring (including newlines) EXCEPT the+  closing delimiter.++  Comment tags SHOULD be treated as standalone when appropriate.+tests:+  - name: Inline+    desc: Comment blocks should be removed from the template.+    data: { }+    template: '12345{{! Comment Block! }}67890'+    expected: '1234567890'++  - name: Multiline+    desc: Multiline comments should be permitted.+    data: { }+    template: |+      12345{{!+        This is a+        multi-line comment...+      }}67890+    expected: |+      1234567890++  - name: Standalone+    desc: All standalone comment lines should be removed.+    data: { }+    template: |+      Begin.+      {{! Comment Block! }}+      End.+    expected: |+      Begin.+      End.++  - name: Indented Standalone+    desc: All standalone comment lines should be removed.+    data: { }+    template: |+      Begin.+        {{! Indented Comment Block! }}+      End.+    expected: |+      Begin.+      End.++  - name: Standalone Line Endings+    desc: '"\r\n" should be considered a newline for standalone tags.'+    data: { }+    template: "|\r\n{{! Standalone Comment }}\r\n|"+    expected: "|\r\n|"++  - name: Standalone Without Previous Line+    desc: Standalone tags should not require a newline to precede them.+    data: { }+    template: "  {{! I'm Still Standalone }}\n!"+    expected: "!"++  - name: Standalone Without Newline+    desc: Standalone tags should not require a newline to follow them.+    data: { }+    template: "!\n  {{! I'm Still Standalone }}"+    expected: "!\n"++  - name: Multiline Standalone+    desc: All standalone comment lines should be removed.+    data: { }+    template: |+      Begin.+      {{!+      Something's going on here...+      }}+      End.+    expected: |+      Begin.+      End.++  - name: Indented Multiline Standalone+    desc: All standalone comment lines should be removed.+    data: { }+    template: |+      Begin.+        {{!+          Something's going on here...+        }}+      End.+    expected: |+      Begin.+      End.++  - name: Indented Inline+    desc: Inline comments should not strip whitespace+    data: { }+    template: "  12 {{! 34 }}\n"+    expected: "  12 \n"++  - name: Surrounding Whitespace+    desc: Comment removal should preserve surrounding whitespace.+    data: { }+    template: '12345 {{! Comment Block! }} 67890'+    expected: '12345  67890'
+ specs/delimiters.yml view
@@ -0,0 +1,158 @@+overview: |+  Set Delimiter tags are used to change the tag delimiters for all content+  following the tag in the current compilation unit.++  The tag's content MUST be any two non-whitespace sequences (separated by+  whitespace) EXCEPT an equals sign ('=') followed by the current closing+  delimiter.++  Set Delimiter tags SHOULD be treated as standalone when appropriate.+tests:+  - name: Pair Behavior+    desc: The equals sign (used on both sides) should permit delimiter changes.+    data: { text: 'Hey!' }+    template: '{{=<% %>=}}(<%text%>)'+    expected: '(Hey!)'++  - name: Special Characters+    desc: Characters with special meaning regexen should be valid delimiters.+    data: { text: 'It worked!' }+    template: '({{=[ ]=}}[text])'+    expected: '(It worked!)'++  - name: Sections+    desc: Delimiters set outside sections should persist.+    data: { section: true, data: 'I got interpolated.' }+    template: |+      [+      {{#section}}+        {{data}}+        |data|+      {{/section}}++      {{= | | =}}+      |#section|+        {{data}}+        |data|+      |/section|+      ]+    expected: |+      [+        I got interpolated.+        |data|++        {{data}}+        I got interpolated.+      ]++  - name: Inverted Sections+    desc: Delimiters set outside inverted sections should persist.+    data: { section: false, data: 'I got interpolated.' }+    template: |+      [+      {{^section}}+        {{data}}+        |data|+      {{/section}}++      {{= | | =}}+      |^section|+        {{data}}+        |data|+      |/section|+      ]+    expected: |+      [+        I got interpolated.+        |data|++        {{data}}+        I got interpolated.+      ]++  - name: Partial Inheritence+    desc: Delimiters set in a parent template should not affect a partial.+    data: { value: 'yes' }+    partials:+      include: '.{{value}}.'+    template: |+      [ {{>include}} ]+      {{= | | =}}+      [ |>include| ]+    expected: |+      [ .yes. ]+      [ .yes. ]++  - name: Post-Partial Behavior+    desc: Delimiters set in a partial should not affect the parent template.+    data: { value: 'yes' }+    partials:+      include: '.{{value}}. {{= | | =}} .|value|.'+    template: |+      [ {{>include}} ]+      [ .{{value}}.  .|value|. ]+    expected: |+      [ .yes.  .yes. ]+      [ .yes.  .|value|. ]++  # Whitespace Sensitivity++  - name: Surrounding Whitespace+    desc: Surrounding whitespace should be left untouched.+    data: { }+    template: '| {{=@ @=}} |'+    expected: '|  |'++  - name: Outlying Whitespace (Inline)+    desc: Whitespace should be left untouched.+    data: { }+    template: " | {{=@ @=}}\n"+    expected: " | \n"++  - name: Standalone Tag+    desc: Standalone lines should be removed from the template.+    data: { }+    template: |+      Begin.+      {{=@ @=}}+      End.+    expected: |+      Begin.+      End.++  - name: Indented Standalone Tag+    desc: Indented standalone lines should be removed from the template.+    data: { }+    template: |+      Begin.+        {{=@ @=}}+      End.+    expected: |+      Begin.+      End.++  - name: Standalone Line Endings+    desc: '"\r\n" should be considered a newline for standalone tags.'+    data: { }+    template: "|\r\n{{= @ @ =}}\r\n|"+    expected: "|\r\n|"++  - name: Standalone Without Previous Line+    desc: Standalone tags should not require a newline to precede them.+    data: { }+    template: "  {{=@ @=}}\n="+    expected: "="++  - name: Standalone Without Newline+    desc: Standalone tags should not require a newline to follow them.+    data: { }+    template: "=\n  {{=@ @=}}"+    expected: "=\n"++  # Whitespace Insensitivity++  - name: Pair with Padding+    desc: Superfluous in-tag whitespace should be ignored.+    data: { }+    template: '|{{= @   @ =}}|'+    expected: '||'
+ specs/interpolation.yml view
@@ -0,0 +1,238 @@+overview: |+  Interpolation tags are used to integrate dynamic content into the template.++  The tag's content MUST be a non-whitespace character sequence NOT containing+  the current closing delimiter.++  This tag's content names the data to replace the tag.  A single period (`.`)+  indicates that the item currently sitting atop the context stack should be+  used; otherwise, name resolution is as follows:+    1) Split the name on periods; the first part is the name to resolve, any+    remaining parts should be retained.+    2) Walk the context stack from top to bottom, finding the first context+    that is a) a hash containing the name as a key OR b) an object responding+    to a method with the given name.+    3) If the context is a hash, the data is the value associated with the+    name.+    4) If the context is an object, the data is the value returned by the+    method with the given name.+    5) If any name parts were retained in step 1, each should be resolved+    against a context stack containing only the result from the former+    resolution.  If any part fails resolution, the result should be considered+    falsey, and should interpolate as the empty string.+  Data should be coerced into a string (and escaped, if appropriate) before+  interpolation.++  The Interpolation tags MUST NOT be treated as standalone.+tests:+  - name: No Interpolation+    desc: Mustache-free templates should render as-is.+    data: { }+    template: |+      Hello from {Mustache}!+    expected: |+      Hello from {Mustache}!++  - name: Basic Interpolation+    desc: Unadorned tags should interpolate content into the template.+    data: { subject: "world" }+    template: |+      Hello, {{subject}}!+    expected: |+      Hello, world!++  - name: HTML Escaping+    desc: Basic interpolation should be HTML escaped.+    data: { forbidden: '& " < >' }+    template: |+      These characters should be HTML escaped: {{forbidden}}+    expected: |+      These characters should be HTML escaped: &amp; &quot; &lt; &gt;++  - name: Triple Mustache+    desc: Triple mustaches should interpolate without HTML escaping.+    data: { forbidden: '& " < >' }+    template: |+      These characters should not be HTML escaped: {{{forbidden}}}+    expected: |+      These characters should not be HTML escaped: & " < >++  - name: Ampersand+    desc: Ampersand should interpolate without HTML escaping.+    data: { forbidden: '& " < >' }+    template: |+      These characters should not be HTML escaped: {{&forbidden}}+    expected: |+      These characters should not be HTML escaped: & " < >++  - name: Basic Integer Interpolation+    desc: Integers should interpolate seamlessly.+    data: { mph: 85 }+    template: '"{{mph}} miles an hour!"'+    expected: '"85 miles an hour!"'++  - name: Triple Mustache Integer Interpolation+    desc: Integers should interpolate seamlessly.+    data: { mph: 85 }+    template: '"{{{mph}}} miles an hour!"'+    expected: '"85 miles an hour!"'++  - name: Ampersand Integer Interpolation+    desc: Integers should interpolate seamlessly.+    data: { mph: 85 }+    template: '"{{&mph}} miles an hour!"'+    expected: '"85 miles an hour!"'++  - name: Basic Decimal Interpolation+    desc: Decimals should interpolate seamlessly with proper significance.+    data: { power: 1.210 }+    template: '"{{power}} jiggawatts!"'+    expected: '"1.21 jiggawatts!"'++  - name: Triple Mustache Decimal Interpolation+    desc: Decimals should interpolate seamlessly with proper significance.+    data: { power: 1.210 }+    template: '"{{{power}}} jiggawatts!"'+    expected: '"1.21 jiggawatts!"'++  - name: Ampersand Decimal Interpolation+    desc: Decimals should interpolate seamlessly with proper significance.+    data: { power: 1.210 }+    template: '"{{&power}} jiggawatts!"'+    expected: '"1.21 jiggawatts!"'++  # Context Misses++  - name: Basic Context Miss Interpolation+    desc: Failed context lookups should default to empty strings.+    data: { }+    template: "I ({{cannot}}) be seen!"+    expected: "I () be seen!"++  - name: Triple Mustache Context Miss Interpolation+    desc: Failed context lookups should default to empty strings.+    data: { }+    template: "I ({{{cannot}}}) be seen!"+    expected: "I () be seen!"++  - name: Ampersand Context Miss Interpolation+    desc: Failed context lookups should default to empty strings.+    data: { }+    template: "I ({{&cannot}}) be seen!"+    expected: "I () be seen!"++  # Dotted Names++  - name: Dotted Names - Basic Interpolation+    desc: Dotted names should be considered a form of shorthand for sections.+    data: { person: { name: 'Joe' } }+    template: '"{{person.name}}" == "{{#person}}{{name}}{{/person}}"'+    expected: '"Joe" == "Joe"'++  - name: Dotted Names - Triple Mustache Interpolation+    desc: Dotted names should be considered a form of shorthand for sections.+    data: { person: { name: 'Joe' } }+    template: '"{{{person.name}}}" == "{{#person}}{{{name}}}{{/person}}"'+    expected: '"Joe" == "Joe"'++  - name: Dotted Names - Ampersand Interpolation+    desc: Dotted names should be considered a form of shorthand for sections.+    data: { person: { name: 'Joe' } }+    template: '"{{&person.name}}" == "{{#person}}{{&name}}{{/person}}"'+    expected: '"Joe" == "Joe"'++  - name: Dotted Names - Arbitrary Depth+    desc: Dotted names should be functional to any level of nesting.+    data:+      a: { b: { c: { d: { e: { name: 'Phil' } } } } }+    template: '"{{a.b.c.d.e.name}}" == "Phil"'+    expected: '"Phil" == "Phil"'++  - name: Dotted Names - Broken Chains+    desc: Any falsey value prior to the last part of the name should yield ''.+    data:+      a: { }+    template: '"{{a.b.c}}" == ""'+    expected: '"" == ""'++  - name: Dotted Names - Broken Chain Resolution+    desc: Each part of a dotted name should resolve only against its parent.+    data:+      a: { b: { } }+      c: { name: 'Jim' }+    template: '"{{a.b.c.name}}" == ""'+    expected: '"" == ""'++  - name: Dotted Names - Initial Resolution+    desc: The first part of a dotted name should resolve as any other name.+    data:+      a: { b: { c: { d: { e: { name: 'Phil' } } } } }+      b: { c: { d: { e: { name: 'Wrong' } } } }+    template: '"{{#a}}{{b.c.d.e.name}}{{/a}}" == "Phil"'+    expected: '"Phil" == "Phil"'++  - name: Dotted Names - Context Precedence+    desc: Dotted names should be resolved against former resolutions.+    data:+      a: { b: { } }+      b: { c: 'ERROR' }+    template: '{{#a}}{{b.c}}{{/a}}'+    expected: ''++  # Whitespace Sensitivity++  - name: Interpolation - Surrounding Whitespace+    desc: Interpolation should not alter surrounding whitespace.+    data: { string: '---' }+    template: '| {{string}} |'+    expected: '| --- |'++  - name: Triple Mustache - Surrounding Whitespace+    desc: Interpolation should not alter surrounding whitespace.+    data: { string: '---' }+    template: '| {{{string}}} |'+    expected: '| --- |'++  - name: Ampersand - Surrounding Whitespace+    desc: Interpolation should not alter surrounding whitespace.+    data: { string: '---' }+    template: '| {{&string}} |'+    expected: '| --- |'++  - name: Interpolation - Standalone+    desc: Standalone interpolation should not alter surrounding whitespace.+    data: { string: '---' }+    template: "  {{string}}\n"+    expected: "  ---\n"++  - name: Triple Mustache - Standalone+    desc: Standalone interpolation should not alter surrounding whitespace.+    data: { string: '---' }+    template: "  {{{string}}}\n"+    expected: "  ---\n"++  - name: Ampersand - Standalone+    desc: Standalone interpolation should not alter surrounding whitespace.+    data: { string: '---' }+    template: "  {{&string}}\n"+    expected: "  ---\n"++  # Whitespace Insensitivity++  - name: Interpolation With Padding+    desc: Superfluous in-tag whitespace should be ignored.+    data: { string: "---" }+    template: '|{{ string }}|'+    expected: '|---|'++  - name: Triple Mustache With Padding+    desc: Superfluous in-tag whitespace should be ignored.+    data: { string: "---" }+    template: '|{{{ string }}}|'+    expected: '|---|'++  - name: Ampersand With Padding+    desc: Superfluous in-tag whitespace should be ignored.+    data: { string: "---" }+    template: '|{{& string }}|'+    expected: '|---|'
+ specs/inverted.yml view
@@ -0,0 +1,193 @@+overview: |+  Inverted Section tags and End Section tags are used in combination to wrap a+  section of the template.++  These tags' content MUST be a non-whitespace character sequence NOT+  containing the current closing delimiter; each Inverted Section tag MUST be+  followed by an End Section tag with the same content within the same+  section.++  This tag's content names the data to replace the tag.  Name resolution is as+  follows:+    1) Split the name on periods; the first part is the name to resolve, any+    remaining parts should be retained.+    2) Walk the context stack from top to bottom, finding the first context+    that is a) a hash containing the name as a key OR b) an object responding+    to a method with the given name.+    3) If the context is a hash, the data is the value associated with the+    name.+    4) If the context is an object and the method with the given name has an+    arity of 1, the method SHOULD be called with a String containing the+    unprocessed contents of the sections; the data is the value returned.+    5) Otherwise, the data is the value returned by calling the method with+    the given name.+    6) If any name parts were retained in step 1, each should be resolved+    against a context stack containing only the result from the former+    resolution.  If any part fails resolution, the result should be considered+    falsey, and should interpolate as the empty string.+  If the data is not of a list type, it is coerced into a list as follows: if+  the data is truthy (e.g. `!!data == true`), use a single-element list+  containing the data, otherwise use an empty list.++  This section MUST NOT be rendered unless the data list is empty.++  Inverted Section and End Section tags SHOULD be treated as standalone when+  appropriate.+tests:+  - name: Falsey+    desc: Falsey sections should have their contents rendered.+    data: { boolean: false }+    template: '"{{^boolean}}This should be rendered.{{/boolean}}"'+    expected: '"This should be rendered."'++  - name: Truthy+    desc: Truthy sections should have their contents omitted.+    data: { boolean: true }+    template: '"{{^boolean}}This should not be rendered.{{/boolean}}"'+    expected: '""'++  - name: Context+    desc: Objects and hashes should behave like truthy values.+    data: { context: { name: 'Joe' } }+    template: '"{{^context}}Hi {{name}}.{{/context}}"'+    expected: '""'++  - name: List+    desc: Lists should behave like truthy values.+    data: { list: [ { n: 1 }, { n: 2 }, { n: 3 } ] }+    template: '"{{^list}}{{n}}{{/list}}"'+    expected: '""'++  - name: Empty List+    desc: Empty lists should behave like falsey values.+    data: { list: [ ] }+    template: '"{{^list}}Yay lists!{{/list}}"'+    expected: '"Yay lists!"'++  - name: Doubled+    desc: Multiple inverted sections per template should be permitted.+    data: { bool: false, two: 'second' }+    template: |+      {{^bool}}+      * first+      {{/bool}}+      * {{two}}+      {{^bool}}+      * third+      {{/bool}}+    expected: |+      * first+      * second+      * third++  - name: Nested (Falsey)+    desc: Nested falsey sections should have their contents rendered.+    data: { bool: false }+    template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"+    expected: "| A B C D E |"++  - name: Nested (Truthy)+    desc: Nested truthy sections should be omitted.+    data: { bool: true }+    template: "| A {{^bool}}B {{^bool}}C{{/bool}} D{{/bool}} E |"+    expected: "| A  E |"++  - name: Context Misses+    desc: Failed context lookups should be considered falsey.+    data: { }+    template: "[{{^missing}}Cannot find key 'missing'!{{/missing}}]"+    expected: "[Cannot find key 'missing'!]"++  # Dotted Names++  - name: Dotted Names - Truthy+    desc: Dotted names should be valid for Inverted Section tags.+    data: { a: { b: { c: true } } }+    template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == ""'+    expected: '"" == ""'++  - name: Dotted Names - Falsey+    desc: Dotted names should be valid for Inverted Section tags.+    data: { a: { b: { c: false } } }+    template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"'+    expected: '"Not Here" == "Not Here"'++  - name: Dotted Names - Broken Chains+    desc: Dotted names that cannot be resolved should be considered falsey.+    data: { a: { } }+    template: '"{{^a.b.c}}Not Here{{/a.b.c}}" == "Not Here"'+    expected: '"Not Here" == "Not Here"'++  # Whitespace Sensitivity++  - name: Surrounding Whitespace+    desc: Inverted sections should not alter surrounding whitespace.+    data: { boolean: false }+    template: " | {{^boolean}}\t|\t{{/boolean}} | \n"+    expected: " | \t|\t | \n"++  - name: Internal Whitespace+    desc: Inverted should not alter internal whitespace.+    data: { boolean: false }+    template: " | {{^boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"+    expected: " |  \n  | \n"++  - name: Indented Inline Sections+    desc: Single-line sections should not alter surrounding whitespace.+    data: { boolean: false }+    template: " {{^boolean}}NO{{/boolean}}\n {{^boolean}}WAY{{/boolean}}\n"+    expected: " NO\n WAY\n"++  - name: Standalone Lines+    desc: Standalone lines should be removed from the template.+    data: { boolean: false }+    template: |+      | This Is+      {{^boolean}}+      |+      {{/boolean}}+      | A Line+    expected: |+      | This Is+      |+      | A Line++  - name: Standalone Indented Lines+    desc: Standalone indented lines should be removed from the template.+    data: { boolean: false }+    template: |+      | This Is+        {{^boolean}}+      |+        {{/boolean}}+      | A Line+    expected: |+      | This Is+      |+      | A Line++  - name: Standalone Line Endings+    desc: '"\r\n" should be considered a newline for standalone tags.'+    data: { boolean: false }+    template: "|\r\n{{^boolean}}\r\n{{/boolean}}\r\n|"+    expected: "|\r\n|"++  - name: Standalone Without Previous Line+    desc: Standalone tags should not require a newline to precede them.+    data: { boolean: false }+    template: "  {{^boolean}}\n^{{/boolean}}\n/"+    expected: "^\n/"++  - name: Standalone Without Newline+    desc: Standalone tags should not require a newline to follow them.+    data: { boolean: false }+    template: "^{{^boolean}}\n/\n  {{/boolean}}"+    expected: "^\n/\n"++  # Whitespace Insensitivity++  - name: Padding+    desc: Superfluous in-tag whitespace should be ignored.+    data: { boolean: false }+    template: '|{{^ boolean }}={{/ boolean }}|'+    expected: '|=|'
+ specs/partials.yml view
@@ -0,0 +1,109 @@+overview: |+  Partial tags are used to expand an external template into the current+  template.++  The tag's content MUST be a non-whitespace character sequence NOT containing+  the current closing delimiter.++  This tag's content names the partial to inject.  Set Delimiter tags MUST NOT+  affect the parsing of a partial.  The partial MUST be rendered against the+  context stack local to the tag.  If the named partial cannot be found, the+  empty string SHOULD be used instead, as in interpolations.++  Partial tags SHOULD be treated as standalone when appropriate.  If this tag+  is used standalone, any whitespace preceding the tag should treated as+  indentation, and prepended to each line of the partial before rendering.+tests:+  - name: Basic Behavior+    desc: The greater-than operator should expand to the named partial.+    data: { }+    template: '"{{>text}}"'+    partials: { text: 'from partial' }+    expected: '"from partial"'++  - name: Failed Lookup+    desc: The empty string should be used when the named partial is not found.+    data: { }+    template: '"{{>text}}"'+    partials: { }+    expected: '""'++  - name: Context+    desc: The greater-than operator should operate within the current context.+    data: { text: 'content' }+    template: '"{{>partial}}"'+    partials: { partial: '*{{text}}*' }+    expected: '"*content*"'++  - name: Recursion+    desc: The greater-than operator should properly recurse.+    data: { content: "X", nodes: [ { content: "Y", nodes: [] } ] }+    template: '{{>node}}'+    partials: { node: '{{content}}<{{#nodes}}{{>node}}{{/nodes}}>' }+    expected: 'X<Y<>>'++  # Whitespace Sensitivity++  - name: Surrounding Whitespace+    desc: The greater-than operator should not alter surrounding whitespace.+    data: { }+    template: '| {{>partial}} |'+    partials: { partial: "\t|\t" }+    expected: "| \t|\t |"++  - name: Inline Indentation+    desc: Whitespace should be left untouched.+    data: { data: '|' }+    template: "  {{data}}  {{> partial}}\n"+    partials: { partial: ">\n>" }+    expected: "  |  >\n>\n"++  - name: Standalone Line Endings+    desc: '"\r\n" should be considered a newline for standalone tags.'+    data: { }+    template: "|\r\n{{>partial}}\r\n|"+    partials: { partial: ">" }+    expected: "|\r\n>|"++  - name: Standalone Without Previous Line+    desc: Standalone tags should not require a newline to precede them.+    data: { }+    template: "  {{>partial}}\n>"+    partials: { partial: ">\n>"}+    expected: "  >\n  >>"++  - name: Standalone Without Newline+    desc: Standalone tags should not require a newline to follow them.+    data: { }+    template: ">\n  {{>partial}}"+    partials: { partial: ">\n>" }+    expected: ">\n  >\n  >"++  - name: Standalone Indentation+    desc: Each line of the partial should be indented before rendering.+    data: { content: "<\n->" }+    template: |+      \+       {{>partial}}+      /+    partials:+      partial: |+        |+        {{{content}}}+        |+    expected: |+      \+       |+       <+      ->+       |+      /++  # Whitespace Insensitivity++  - name: Padding Whitespace+    desc: Superfluous in-tag whitespace should be ignored.+    data: { boolean: true }+    template: "|{{> partial }}|"+    partials: { partial: "[]" }+    expected: '|[]|'
+ specs/sections.yml view
@@ -0,0 +1,263 @@+overview: |+  Section tags and End Section tags are used in combination to wrap a section+  of the template for iteration++  These tags' content MUST be a non-whitespace character sequence NOT+  containing the current closing delimiter; each Section tag MUST be followed+  by an End Section tag with the same content within the same section.++  This tag's content names the data to replace the tag.  Name resolution is as+  follows:+    1) Split the name on periods; the first part is the name to resolve, any+    remaining parts should be retained.+    2) Walk the context stack from top to bottom, finding the first context+    that is a) a hash containing the name as a key OR b) an object responding+    to a method with the given name.+    3) If the context is a hash, the data is the value associated with the+    name.+    4) If the context is an object and the method with the given name has an+    arity of 1, the method SHOULD be called with a String containing the+    unprocessed contents of the sections; the data is the value returned.+    5) Otherwise, the data is the value returned by calling the method with+    the given name.+    6) If any name parts were retained in step 1, each should be resolved+    against a context stack containing only the result from the former+    resolution.  If any part fails resolution, the result should be considered+    falsey, and should interpolate as the empty string.+  If the data is not of a list type, it is coerced into a list as follows: if+  the data is truthy (e.g. `!!data == true`), use a single-element list+  containing the data, otherwise use an empty list.++  For each element in the data list, the element MUST be pushed onto the+  context stack, the section MUST be rendered, and the element MUST be popped+  off the context stack.++  Section and End Section tags SHOULD be treated as standalone when+  appropriate.+tests:+  - name: Truthy+    desc: Truthy sections should have their contents rendered.+    data: { boolean: true }+    template: '"{{#boolean}}This should be rendered.{{/boolean}}"'+    expected: '"This should be rendered."'++  - name: Falsey+    desc: Falsey sections should have their contents omitted.+    data: { boolean: false }+    template: '"{{#boolean}}This should not be rendered.{{/boolean}}"'+    expected: '""'++  - name: Context+    desc: Objects and hashes should be pushed onto the context stack.+    data: { context: { name: 'Joe' } }+    template: '"{{#context}}Hi {{name}}.{{/context}}"'+    expected: '"Hi Joe."'++  - name: Deeply Nested Contexts+    desc: All elements on the context stack should be accessible.+    data:+      a: { one: 1 }+      b: { two: 2 }+      c: { three: 3 }+      d: { four: 4 }+      e: { five: 5 }+    template: |+      {{#a}}+      {{one}}+      {{#b}}+      {{one}}{{two}}{{one}}+      {{#c}}+      {{one}}{{two}}{{three}}{{two}}{{one}}+      {{#d}}+      {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}+      {{#e}}+      {{one}}{{two}}{{three}}{{four}}{{five}}{{four}}{{three}}{{two}}{{one}}+      {{/e}}+      {{one}}{{two}}{{three}}{{four}}{{three}}{{two}}{{one}}+      {{/d}}+      {{one}}{{two}}{{three}}{{two}}{{one}}+      {{/c}}+      {{one}}{{two}}{{one}}+      {{/b}}+      {{one}}+      {{/a}}+    expected: |+      1+      121+      12321+      1234321+      123454321+      1234321+      12321+      121+      1++  - name: List+    desc: Lists should be iterated; list items should visit the context stack.+    data: { list: [ { item: 1 }, { item: 2 }, { item: 3 } ] }+    template: '"{{#list}}{{item}}{{/list}}"'+    expected: '"123"'++  - name: Empty List+    desc: Empty lists should behave like falsey values.+    data: { list: [ ] }+    template: '"{{#list}}Yay lists!{{/list}}"'+    expected: '""'++  - name: Doubled+    desc: Multiple sections per template should be permitted.+    data: { bool: true, two: 'second' }+    template: |+      {{#bool}}+      * first+      {{/bool}}+      * {{two}}+      {{#bool}}+      * third+      {{/bool}}+    expected: |+      * first+      * second+      * third++  - name: Nested (Truthy)+    desc: Nested truthy sections should have their contents rendered.+    data: { bool: true }+    template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"+    expected: "| A B C D E |"++  - name: Nested (Falsey)+    desc: Nested falsey sections should be omitted.+    data: { bool: false }+    template: "| A {{#bool}}B {{#bool}}C{{/bool}} D{{/bool}} E |"+    expected: "| A  E |"++  - name: Context Misses+    desc: Failed context lookups should be considered falsey.+    data: { }+    template: "[{{#missing}}Found key 'missing'!{{/missing}}]"+    expected: "[]"++  # Implicit Iterators++  - name: Implicit Iterator - String+    desc: Implicit iterators should directly interpolate strings.+    data:+      list: [ 'a', 'b', 'c', 'd', 'e' ]+    template: '"{{#list}}({{.}}){{/list}}"'+    expected: '"(a)(b)(c)(d)(e)"'++  - name: Implicit Iterator - Integer+    desc: Implicit iterators should cast integers to strings and interpolate.+    data:+      list: [ 1, 2, 3, 4, 5 ]+    template: '"{{#list}}({{.}}){{/list}}"'+    expected: '"(1)(2)(3)(4)(5)"'++  - name: Implicit Iterator - Decimal+    desc: Implicit iterators should cast decimals to strings and interpolate.+    data:+      list: [ 1.10, 2.20, 3.30, 4.40, 5.50 ]+    template: '"{{#list}}({{.}}){{/list}}"'+    expected: '"(1.1)(2.2)(3.3)(4.4)(5.5)"'++  - name: Implicit Iterator - Array+    desc: Implicit iterators should allow iterating over nested arrays.+    data:+      list: [ [1, 2, 3], ['a', 'b', 'c'] ]+    template: '"{{#list}}({{#.}}{{.}}{{/.}}){{/list}}"'+    expected: '"(123)(abc)"'++  # Dotted Names++  - name: Dotted Names - Truthy+    desc: Dotted names should be valid for Section tags.+    data: { a: { b: { c: true } } }+    template: '"{{#a.b.c}}Here{{/a.b.c}}" == "Here"'+    expected: '"Here" == "Here"'++  - name: Dotted Names - Falsey+    desc: Dotted names should be valid for Section tags.+    data: { a: { b: { c: false } } }+    template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""'+    expected: '"" == ""'++  - name: Dotted Names - Broken Chains+    desc: Dotted names that cannot be resolved should be considered falsey.+    data: { a: { } }+    template: '"{{#a.b.c}}Here{{/a.b.c}}" == ""'+    expected: '"" == ""'++  # Whitespace Sensitivity++  - name: Surrounding Whitespace+    desc: Sections should not alter surrounding whitespace.+    data: { boolean: true }+    template: " | {{#boolean}}\t|\t{{/boolean}} | \n"+    expected: " | \t|\t | \n"++  - name: Internal Whitespace+    desc: Sections should not alter internal whitespace.+    data: { boolean: true }+    template: " | {{#boolean}} {{! Important Whitespace }}\n {{/boolean}} | \n"+    expected: " |  \n  | \n"++  - name: Indented Inline Sections+    desc: Single-line sections should not alter surrounding whitespace.+    data: { boolean: true }+    template: " {{#boolean}}YES{{/boolean}}\n {{#boolean}}GOOD{{/boolean}}\n"+    expected: " YES\n GOOD\n"++  - name: Standalone Lines+    desc: Standalone lines should be removed from the template.+    data: { boolean: true }+    template: |+      | This Is+      {{#boolean}}+      |+      {{/boolean}}+      | A Line+    expected: |+      | This Is+      |+      | A Line++  - name: Indented Standalone Lines+    desc: Indented standalone lines should be removed from the template.+    data: { boolean: true }+    template: |+      | This Is+        {{#boolean}}+      |+        {{/boolean}}+      | A Line+    expected: |+      | This Is+      |+      | A Line++  - name: Standalone Line Endings+    desc: '"\r\n" should be considered a newline for standalone tags.'+    data: { boolean: true }+    template: "|\r\n{{#boolean}}\r\n{{/boolean}}\r\n|"+    expected: "|\r\n|"++  - name: Standalone Without Previous Line+    desc: Standalone tags should not require a newline to precede them.+    data: { boolean: true }+    template: "  {{#boolean}}\n#{{/boolean}}\n/"+    expected: "#\n/"++  - name: Standalone Without Newline+    desc: Standalone tags should not require a newline to follow them.+    data: { boolean: true }+    template: "#{{#boolean}}\n/\n  {{/boolean}}"+    expected: "#\n/\n"++  # Whitespace Insensitivity++  - name: Padding+    desc: Superfluous in-tag whitespace should be ignored.+    data: { boolean: true }+    template: '|{{# boolean }}={{/ boolean }}|'+    expected: '|=|'
+ stache.cabal view
@@ -0,0 +1,136 @@+--+-- Cabal configuration for ‘stache’ package.+--+-- Copyright © 2016 Stack Builders+--+-- Redistribution and use in source and binary forms, with or without+-- modification, are permitted provided that the following conditions are+-- met:+--+-- * Redistributions of source code must retain the above copyright notice,+--   this list of conditions and the following disclaimer.+--+-- * Redistributions in binary form must reproduce the above copyright+--   notice, this list of conditions and the following disclaimer in the+--   documentation and/or other materials provided with the distribution.+--+-- * Neither the name Mark Karpov nor the names of contributors may be used+--   to endorse or promote products derived from this software without+--   specific prior written permission.+--+-- THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS “AS IS” AND ANY+-- EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED+-- WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE+-- DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY+-- DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL+-- DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS+-- OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)+-- HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,+-- STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN+-- ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+-- POSSIBILITY OF SUCH DAMAGE.++name:                 stache+version:              0.1.0+cabal-version:        >= 1.10+license:              BSD3+license-file:         LICENSE.md+author:               Mark Karpov <markkarpov@openmailbox.org>+maintainer:           Mark Karpov <markkarpov@openmailbox.org>+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-source-files:   CHANGELOG.md+                    , README.md+                    , specs/comments.yml+                    , specs/delimiters.yml+                    , specs/interpolation.yml+                    , specs/inverted.yml+                    , specs/partials.yml+                    , specs/sections.yml+                    , templates/foo.mustache+                    , templates/bar.mustache++source-repository head+  type:               git+  location:           https://github.com/stackbuilders/stache.git++flag dev+  description:        Turn on development settings.+  manual:             True+  default:            False++library+  build-depends:      aeson            >= 0.11 && < 0.12+                    , base             >= 4.7  && < 5.0+                    , bytestring       >= 0.10 && < 0.11+                    , containers       >= 0.5  && < 0.6+                    , directory        >= 1.2  && < 1.3+                    , exceptions       >= 0.8  && < 0.9+                    , filepath         >= 1.2  && < 1.5+                    , megaparsec       >= 5.0  && < 6.0+                    , mtl              >= 2.1  && < 3.0+                    , template-haskell >= 2.10 && < 2.12+                    , text             >= 1.2  && < 1.3+                    , unordered-containers >= 0.2.5 && < 0.3+                    , vector           >= 0.11 && < 0.12+  if !impl(ghc >= 8.0)+    build-depends:    semigroups       == 0.18.*+  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:      -Wall -Werror -fsimpl-tick-factor=150+  else+    ghc-options:      -O2 -Wall -fsimpl-tick-factor=150+  default-language:   Haskell2010++test-suite tests+  main-is:            Spec.hs+  hs-source-dirs:     tests+  type:               exitcode-stdio-1.0+  build-depends:      aeson            >= 0.11 && < 0.12+                    , base             >= 4.7  && < 5.0+                    , containers       >= 0.5  && < 0.6+                    , hspec            >= 2.0  && < 3.0+                    , hspec-megaparsec >= 0.2  && < 0.3+                    , megaparsec       >= 5.0  && < 6.0+                    , stache           >= 0.1.0+                    , text             >= 1.2  && < 1.3+  other-modules:      Text.Mustache.Compile.THSpec+                    , Text.Mustache.ParserSpec+                    , Text.Mustache.RenderSpec+                    , Text.Mustache.TypeSpec+  if !impl(ghc >= 8.0)+    build-depends:    semigroups     == 0.18.*+  if flag(dev)+    ghc-options:      -Wall -Werror+  else+    ghc-options:      -O2 -Wall+  default-language:   Haskell2010++test-suite mustache-spec+  main-is:            Spec.hs+  hs-source-dirs:     mustache-spec+  type:               exitcode-stdio-1.0+  build-depends:      aeson            >= 0.11 && < 0.12+                    , base             >= 4.7  && < 5.0+                    , bytestring       >= 0.10 && < 0.11+                    , containers       >= 0.5  && < 0.6+                    , file-embed+                    , hspec            >= 2.0  && < 3.0+                    , megaparsec       >= 5.0  && < 6.0+                    , stache           >= 0.1.0+                    , text             >= 1.2  && < 1.3+                    , yaml             >= 0.8  && < 0.9+  if flag(dev)+    ghc-options:      -Wall -Werror+  else+    ghc-options:      -O2 -Wall+  default-language:   Haskell2010
+ templates/bar.mustache view
@@ -0,0 +1,1 @@+And this is the ‘bar’.
+ templates/foo.mustache view
@@ -0,0 +1,1 @@+This is the ‘foo’.
+ tests/Spec.hs view
@@ -0,0 +1,1 @@+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
+ tests/Text/Mustache/Compile/THSpec.hs view
@@ -0,0 +1,47 @@+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TemplateHaskell   #-}++module Text.Mustache.Compile.THSpec+  ( main+  , spec )+where++import Test.Hspec++#if MIN_VERSION_template_haskell(2,11,0)+import Data.Semigroup ((<>))+import Text.Mustache.Type+import qualified Data.Map                 as M+import qualified Text.Mustache.Compile.TH as TH+#endif++main :: IO ()+main = hspec spec++spec :: Spec+#if MIN_VERSION_template_haskell(2,11,0)+spec = do+  describe "compileMustacheText" $+    it "compiles template from text at compile time" $+      $(TH.compileMustacheText "foo" "This is the ‘foo’.\n")+        `shouldBe` fooTemplate+  describe "compileMustacheFile" $+    it "compiles template from file at compile time" $+      $(TH.compileMustacheFile "templates/foo.mustache")+        `shouldBe` fooTemplate+  describe "compileMustacheDir" $+    it "compiles templates from directory at compile time" $+      $(TH.compileMustacheDir "foo" "templates/")+        `shouldBe` (fooTemplate <> barTemplate)++fooTemplate :: Template+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"]+#else+spec = return ()+#endif
+ tests/Text/Mustache/ParserSpec.hs view
@@ -0,0 +1,104 @@+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}++module Text.Mustache.ParserSpec+  ( main+  , spec )+where++import Data.List.NonEmpty (NonEmpty (..))+import Test.Hspec+import Test.Hspec.Megaparsec+import Text.Megaparsec+import Text.Mustache.Parser+import Text.Mustache.Type+import qualified Data.List.NonEmpty as NE+import qualified Data.Set           as S++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative (pure)+#endif++main :: IO ()+main = hspec spec++spec :: Spec+spec = describe "parseMustache" $ do+  let p = parseMustache ""+      key = Key . pure+  it "parses text" $+    p "test12356p0--=-34{}jnv,\n"+      `shouldParse` [TextBlock "test12356p0--=-34{}jnv,\n"]+  context "when parsing a variable" $ do+    context "with white space" $ do+      it "parses escaped {{ variable }}" $+        p "{{ name }}" `shouldParse` [EscapedVar (key "name")]+      it "parses unescaped {{{ variable }}}" $+        p "{{{ name }}}" `shouldParse` [UnescapedVar (key "name")]+      it "parses unescaped {{& variable }}" $+        p "{{& name }}" `shouldParse` [UnescapedVar (key "name")]+    context "without white space" $ do+      it "parses escaped {{variable}}" $+        p "{{name}}" `shouldParse` [EscapedVar (key "name")]+      it "parses unescaped {{{variable}}}" $+        p "{{{name}}}" `shouldParse` [UnescapedVar (key "name")]+      it "parses unescaped {{& variable }}" $+        p "{{&name}}" `shouldParse` [UnescapedVar (key "name")]+    it "allows '-' in variable names" $+      p "{{ var-name }}" `shouldParse` [EscapedVar (key "var-name")]+    it "allows '_' in variable names" $+      p "{{ var_name }}" `shouldParse` [EscapedVar (key "var_name")]+  context "when parsing a section" $ do+    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"]]+  context "when parsing an inverted section" $ do+    it "parses empty inverted 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"]]+  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 $ unsafePos 1)]+    it "parses a partial without white space" $+      p "{{>that-s_my-partial}}" `shouldParse`+        [Partial "that-s_my-partial" (Just $ unsafePos 1)]+    it "handles indented partial correctly" $+      p "   {{> next_one }}" `shouldParse`+        [Partial "next_one" (Just $ unsafePos 4)]+  context "when running into delimiter change" $ do+    it "has effect" $+      p "{{=<< >>=}}<<var>>{{var}}" `shouldParse`+        [EscapedVar (key "var"), TextBlock "{{var}}"]+    it "handles whitespace just as well" $+      p "{{=<<   >>=}}<<  var >>{{ var  }}" `shouldParse`+        [EscapedVar (key "var"), TextBlock "{{ var  }}"]+    it "parses two subsequent delimiter changes" $+      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")]+  context "when given malformed input" $ do+    let pos l c = SourcePos "" (unsafePos l) (unsafePos c) :| []+        ne      = NE.fromList+    it "rejects unclosed tags" $+      p "{{ name " `shouldFailWith` ParseError+        { errorPos        = pos 1 9+        , errorUnexpected = S.singleton EndOfInput+        , errorExpected   = S.singleton (Tokens $ ne "}}")+        , errorCustom     = S.empty }+    it "rejects unknown tags" $+      p "{{? boo }}" `shouldFailWith` ParseError+        { errorPos        = pos 1 3+        , errorUnexpected = S.singleton (Tokens $ ne "?")+        , errorExpected   = S.singleton (Label  $ ne "key")+        , errorCustom     = S.empty }
+ tests/Text/Mustache/RenderSpec.hs view
@@ -0,0 +1,109 @@+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}++module Text.Mustache.RenderSpec+  ( main+  , spec )+where++import Data.Aeson (object, KeyValue (..), Value (..))+import Data.Text (Text)+import Test.Hspec+import Text.Megaparsec+import Text.Mustache.Render+import Text.Mustache.Type+import qualified Data.Map as M++#if !MIN_VERSION_base(4,8,0)+import Control.Applicative (pure)+#endif++main :: IO ()+main = hspec spec++spec :: Spec+spec = describe "renderMustache" $ do+  let r ns value =+        let template = Template "test" (M.singleton "test" ns)+        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")]+      (object ["foo" .= ("<html>&\"something\"</html>" :: Text)])+      `shouldBe` "&lt;html&gt;&amp;&quot;something&quot;&lt;/html&gt;"+  it "renders unescaped variables “as is”" $+    r [UnescapedVar (key "foo")]+      (object ["foo" .= ("<html>&\"something\"</html>" :: Text)])+      `shouldBe` "<html>&\"something\"</html>"+  context "when rendering a section" $ do+    let nodes = [Section (key "foo") [UnescapedVar (key "bar"), TextBlock "*"]]+    context "when the key is not present" $+      it "renders nothing" $+        r nodes (object []) `shouldBe` ""+    context "when the key is present" $ do+      context "when the key is a “false” value" $ do+        it "skips the Null value" $+          r nodes (object ["foo" .= Null]) `shouldBe` ""+        it "skips false Boolean" $+          r nodes (object ["foo" .= False]) `shouldBe` ""+        it "skips empty list" $+          r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` ""+        it "skips empty object" $+          r nodes (object ["foo" .= object []]) `shouldBe` ""+      context "when the key is a Boolean true" $+        it "renders the section without interpolation" $+          r [Section (key "foo") [TextBlock "brr"]]+            (object ["foo" .= object ["bar" .= True]])+            `shouldBe` "brr"+      context "when the key is an object" $+        it "uses it to render section once" $+          r nodes (object ["foo" .= object ["bar" .= ("huh?" :: Text)]])+            `shouldBe` "huh?*"+      context "when the key is a singleton list" $+        it "uses it to render section once" $+          r nodes (object ["foo" .= object ["bar" .= ("huh!" :: Text)]])+            `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"]]+            (object ["foo" .= [True, True]])+            `shouldBe` "brrbrr"+      context "wehn 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]]])+            `shouldBe` "1*2*3*4*"+  context "when rendering an inverted section" $ do+    let nodes = [InvertedSection (key "foo") [TextBlock "Here!"]]+    context "when the key is not present" $+      it "renders the inverse section" $+        r nodes (object []) `shouldBe` "Here!"+    context "when the key is present" $ do+      context "when the key is a “false” value" $ do+        it "renders with Null value" $+          r nodes (object ["foo" .= Null]) `shouldBe` "Here!"+        it "renders with false Boolean" $+          r nodes (object ["foo" .= False]) `shouldBe` "Here!"+        it "renders with empty list" $+          r nodes (object ["foo" .= ([] :: [Text])]) `shouldBe` "Here!"+        it "renders with empty object" $+          r nodes (object ["foo" .= object []]) `shouldBe` "Here!"+      context "when the key is a “true” value" $ do+        it "skips true Boolean" $+          r nodes (object ["foo" .= True]) `shouldBe` ""+        it "skips non-empty object" $+          r nodes (object ["foo" .= object ["bar" .= True]]) `shouldBe` ""+        it "skips non-empty list" $+          r nodes (object ["foo" .= [True]]) `shouldBe` ""+  context "when rendering a partial" $ do+    let nodes = [ Partial "partial" (Just $ unsafePos 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*"
+ tests/Text/Mustache/TypeSpec.hs view
@@ -0,0 +1,38 @@+{-# LANGUAGE OverloadedStrings #-}++module Text.Mustache.TypeSpec+  ( main+  , spec )+where++import Data.Semigroup ((<>))+import Test.Hspec+import Text.Mustache.Type+import qualified Data.Map as M++main :: IO ()+main = hspec spec++spec :: Spec+spec =+  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"]) ]++templateA :: Template+templateA = Template "a" $ M.fromList+  [ ("c", [TextBlock "foo"])+  , ("d", [TextBlock "bar"]) ]++templateB :: Template+templateB = Template "b" $ M.fromList+  [ ("c", [TextBlock "bar"])+  , ("e", [TextBlock "baz"]) ]