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 +3/−0
- LICENSE.md +28/−0
- README.md +83/−0
- Setup.hs +6/−0
- Text/Mustache.hs +93/−0
- Text/Mustache/Compile.hs +103/−0
- Text/Mustache/Compile/TH.hs +96/−0
- Text/Mustache/Parser.hs +197/−0
- Text/Mustache/Render.hs +270/−0
- Text/Mustache/Type.hs +93/−0
- mustache-spec/Spec.hs +88/−0
- specs/comments.yml +103/−0
- specs/delimiters.yml +158/−0
- specs/interpolation.yml +238/−0
- specs/inverted.yml +193/−0
- specs/partials.yml +109/−0
- specs/sections.yml +263/−0
- stache.cabal +136/−0
- templates/bar.mustache +1/−0
- templates/foo.mustache +1/−0
- tests/Spec.hs +1/−0
- tests/Text/Mustache/Compile/THSpec.hs +47/−0
- tests/Text/Mustache/ParserSpec.hs +104/−0
- tests/Text/Mustache/RenderSpec.hs +109/−0
- tests/Text/Mustache/TypeSpec.hs +38/−0
+ 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++[](http://opensource.org/licenses/BSD-3-Clause)+[](https://hackage.haskell.org/package/stache)+[](http://stackage.org/nightly/package/stache)+[](http://stackage.org/lts/package/stache)+[](https://travis-ci.org/stackbuilders/stache)+[](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+ [ ("\"", """)+ , ("<", "<")+ , (">", ">")+ , ("&", "&") ]+{-# 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: & " < >++ - 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` "<html>&"something"</html>"+ 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"]) ]