diff options
| author | Mirek Kratochvil <exa.exa@gmail.com> | 2023-05-27 20:20:43 +0200 |
|---|---|---|
| committer | Mirek Kratochvil <exa.exa@gmail.com> | 2023-05-27 20:20:43 +0200 |
| commit | 73498534cfdccab95e580f8d6b121655d00e7578 (patch) | |
| tree | f1160d0c93833cbf668700a096e2bf4a2b7ff4ea /mustache/src | |
| parent | 35837f5607986b18746590c1611927d59cbe8c87 (diff) | |
| download | reploy-73498534cfdccab95e580f8d6b121655d00e7578.tar.gz reploy-73498534cfdccab95e580f8d6b121655d00e7578.tar.bz2 | |
fork mustache
Diffstat (limited to 'mustache/src')
| -rw-r--r-- | mustache/src/Text/Mustache.hs | 197 | ||||
| -rw-r--r-- | mustache/src/Text/Mustache/Compile.hs | 213 | ||||
| -rw-r--r-- | mustache/src/Text/Mustache/Internal.hs | 41 | ||||
| -rw-r--r-- | mustache/src/Text/Mustache/Internal/Types.hs | 412 | ||||
| -rw-r--r-- | mustache/src/Text/Mustache/Parser.hs | 311 | ||||
| -rw-r--r-- | mustache/src/Text/Mustache/Render.hs | 248 | ||||
| -rw-r--r-- | mustache/src/Text/Mustache/Types.hs | 110 |
7 files changed, 1532 insertions, 0 deletions
diff --git a/mustache/src/Text/Mustache.hs b/mustache/src/Text/Mustache.hs new file mode 100644 index 0000000..cf3027f --- /dev/null +++ b/mustache/src/Text/Mustache.hs @@ -0,0 +1,197 @@ +{-| +Module : $Header$ +Description : Basic functions for dealing with mustache templates. +Copyright : (c) Justus Adam, 2015 +License : BSD3 +Maintainer : dev@justus.science +Stability : experimental +Portability : POSIX + += How to use this library + +This module exposes some of the most convenient functions for dealing with mustache +templates. + +== Compiling with automatic partial discovery + +The easiest way of compiling a file and its potential includes (called partials) +is by using the 'automaticCompile' function. + +@ +main :: IO () +main = do + let searchSpace = [".", "./templates"] + templateName = "main.mustache" + + compiled <- automaticCompile searchSpace templateName + case compiled of + Left err -> print err + Right template -> return () -- this is where you can start using it +@ + +The @searchSpace@ encompasses all directories in which the compiler should +search for the template source files. +The search itself is conducted in order, from left to right. + +Should your search space be only the current working directory, you can use +'localAutomaticCompile'. + +The @templateName@ is the relative path of the template to any directory +of the search space. + +'automaticCompile' not only finds and compiles the template for you, it also +recursively finds any partials included in the template as well, +compiles them and stores them in the 'partials' hash attached to the resulting +template. + +The compiler will throw errors if either the template is malformed +or the source file for a partial or the template itself could not be found +in any of the directories in @searchSpace@. + +== Substituting + +In order to substitute data into the template it must be an instance of the 'ToMustache' +typeclass or be of type 'Value'. + +This libray tries to imitate the API of <https://hackage.haskell.org/package/aeson aeson> +by allowing you to define conversions of your own custom data types into 'Value', +the type used internally by the substitutor via typeclass and a selection of +operators and convenience functions. + +=== Example + +@ + data Person = { age :: Int, name :: String } + + instance ToMustache Person where + toMustache person = object + [ "age" ~> age person + , "name" ~> name person + ] +@ + +The values to the left of the '~>' operator has to be of type 'Text', hence the +@OverloadedStrings@ can becomes very handy here. + +Values to the right of the '~>' operator must be an instance of the 'ToMustache' +typeclass. Alternatively, if your value to the right of the '~>' operator is +not an instance of 'ToMustache' but an instance of 'ToJSON' you can use the +'~=' operator, which accepts 'ToJSON' values. + +@ + data Person = { age :: Int, name :: String, address :: Address } + + data Address = ... + + instance ToJSON Address where + ... + + instance ToMustache Person where + toMustache person = object + [ "age" ~> age person + , "name" ~> name person + , "address" ~= address person + ] +@ + +All operators are also provided in a unicode form, for those that, like me, enjoy +unicode operators. + +== Manual compiling + +You can compile templates manually without requiring the IO monad at all, using +the 'compileTemplate' function. This is the same function internally used by +'automaticCompile' and does not check if required partial are present. + +More functions for manual compilation can be found in the 'Text.Mustache.Compile' +module. Including helpers for finding lists of partials in templates. + +Additionally the 'compileTemplateWithCache' function is exposed here which you +may use to automatically compile a template but avoid some of the compilation +overhead by providing already compiled partials as well. + +== Fundamentals + +This library builds on three important data structures/types. + +['Value'] A data structure almost identical to Data.Aeson.Value extended with +lambda functions which represents the data the template is being filled with. + +['ToMustache'] A typeclass for converting arbitrary types to 'Value', similar +to Data.Aeson.ToJSON but with support for lambdas. + +['Template'] Contains the 'STree', the syntax tree, which is basically a +list of text blocks and mustache tags. The 'name' of the template and its +'partials' cache. + +=== Compiling + +During the compilation step the template file is located, read, then parsed in a single +pass ('compileTemplate'), resulting in a 'Template' with an empty 'partials' section. + +Subsequenty the 'STree' of the template is scanned for included partials, any +present 'TemplateCache' is queried for the partial ('compileTemplateWithCache'), +if not found it will be searched for in the @searchSpace@, compiled and +inserted into the template's own cache as well as the global cache for the +compilation process. + +Internally no partial is compiled twice, as long as the names stay consistent. + +Once compiled templates may be used multiple times for substitution or as +partial for other templates. + +Partials are not being embedded into the templates during compilation, but during +substitution, hence the 'partials' cache is vital to the template even after +compilation has been completed. Any non existent partial in the cache will +rsubstitute to an empty string. + +=== Substituting + + + +-} +{-# LANGUAGE LambdaCase #-} +module Text.Mustache + ( + -- * Compiling + + -- ** Automatic + automaticCompile, localAutomaticCompile + + -- ** Manually + , compileTemplateWithCache, compileTemplate, Template(..) + + -- * Rendering + + -- ** Generic + + , substitute, checkedSubstitute + + -- ** Specialized + + , substituteValue, checkedSubstituteValue + + -- ** In Lambdas + + , substituteNode, substituteAST, catchSubstitute + + -- * Data Conversion + , ToMustache, toMustache, integralToMustache, object, (~>), (~=) + + -- ** Utilities for lambdas + + , overText + + ) where + + + +import Text.Mustache.Compile +import Text.Mustache.Render +import Text.Mustache.Types +import qualified Data.Text as T + + +-- | Creates a 'Lambda' which first renders the contained section and then applies the supplied function +overText :: (T.Text -> T.Text) -> Value +overText f = toMustache $ fmap (f . snd) . catchSubstitute . substituteAST diff --git a/mustache/src/Text/Mustache/Compile.hs b/mustache/src/Text/Mustache/Compile.hs new file mode 100644 index 0000000..8079d6c --- /dev/null +++ b/mustache/src/Text/Mustache/Compile.hs @@ -0,0 +1,213 @@ +{-| +Module : $Header$ +Description : Basic functions for dealing with mustache templates. +Copyright : (c) Justus Adam, 2015 +License : BSD3 +Maintainer : dev@justus.science +Stability : experimental +Portability : POSIX +-} +{-# OPTIONS_GHC -fno-warn-missing-fields #-} +{-# LANGUAGE QuasiQuotes #-} +{-# LANGUAGE TemplateHaskell #-} +module Text.Mustache.Compile + ( automaticCompile, localAutomaticCompile, TemplateCache, compileTemplateWithCache + , compileTemplate, cacheFromList, getPartials, mustache, embedTemplate, embedSingleTemplate + ) where + + +import Control.Arrow ((&&&)) +import Control.Monad +import Control.Monad.Except +import Control.Monad.State +import Data.Bool +import Data.HashMap.Strict as HM +import Data.Text hiding (concat, find, map, uncons) +import qualified Data.Text.IO as TIO +import Language.Haskell.TH (Exp, Loc, Q, loc_filename, + loc_start, location) +import Language.Haskell.TH.Quote (QuasiQuoter (QuasiQuoter), + quoteExp) +import qualified Language.Haskell.TH.Syntax as THS +import System.Directory +import System.FilePath +import Text.Mustache.Parser +import Text.Mustache.Types +import Text.Parsec.Error +import Text.Parsec.Pos +import Text.Printf + +{-| + Compiles a mustache template provided by name including the mentioned partials. + + The same can be done manually using 'getFile', 'mustacheParser' and 'getPartials'. + + This function also ensures each partial is only compiled once even though it may + be included by other partials including itself. + + A reference to the included template will be found in each including templates + 'partials' section. +-} +automaticCompile :: [FilePath] -> FilePath -> IO (Either ParseError Template) +automaticCompile searchSpace = compileTemplateWithCache searchSpace mempty + + +-- | Compile the template with the search space set to only the current directory +localAutomaticCompile :: FilePath -> IO (Either ParseError Template) +localAutomaticCompile = automaticCompile ["."] + + +{-| + Compile a mustache template providing a list of precompiled templates that do + not have to be recompiled. +-} +compileTemplateWithCache :: [FilePath] + -> TemplateCache + -> FilePath + -> IO (Either ParseError Template) +compileTemplateWithCache searchSpace templates initName = + runExceptT $ evalStateT (compile' initName) $ flattenPartials templates + where + compile' :: FilePath + -> StateT + (HM.HashMap String Template) + (ExceptT ParseError IO) + Template + compile' name' = do + templates' <- get + case HM.lookup name' templates' of + Just template -> return template + Nothing -> do + rawSource <- lift $ getFile searchSpace name' + compiled@(Template { ast = mSTree }) <- + lift $ ExceptT . pure $ compileTemplate name' rawSource + + foldM + (\st@(Template { partials = p }) partialName -> do + nt <- compile' partialName + modify (HM.insert partialName nt) + return (st { partials = HM.insert partialName nt p }) + ) + compiled + (getPartials mSTree) + + +-- | Flatten a list of Templates into a single 'TemplateCache' +cacheFromList :: [Template] -> TemplateCache +cacheFromList = flattenPartials . fromList . fmap (name &&& id) + + +-- | Compiles a 'Template' directly from 'Text' without checking for missing partials. +-- the result will be a 'Template' with an empty 'partials' cache. +compileTemplate :: String -> Text -> Either ParseError Template +compileTemplate name' = fmap (flip (Template name') mempty) . parse name' + + +{-| + Find the names of all included partials in a mustache STree. + + Same as @join . fmap getPartials'@ +-} +getPartials :: STree -> [FilePath] +getPartials = join . fmap getPartials' + + +{-| + Find partials in a single Node +-} +getPartials' :: Node Text -> [FilePath] +getPartials' (Partial _ p) = return p +getPartials' (Section _ n) = getPartials n +getPartials' (InvertedSection _ n) = getPartials n +getPartials' _ = mempty + + +flattenPartials :: TemplateCache -> TemplateCache +flattenPartials m = foldrWithKey (insertWith (\_ b -> b)) m m + + +{-| + @getFile searchSpace file@ iteratively searches all directories in + @searchSpace@ for a @file@ returning it if found or raising an error if none + of the directories contain the file. + + This trows 'ParseError's to be compatible with the internal Either Monad of + 'compileTemplateWithCache'. +-} +getFile :: [FilePath] -> FilePath -> ExceptT ParseError IO Text +getFile [] fp = throwError $ fileNotFound fp +getFile (templateDir : xs) fp = + lift (doesFileExist filePath) >>= + bool + (getFile xs fp) + (lift $ TIO.readFile filePath) + where + filePath = templateDir </> fp + + +-- | +-- Compile a mustache 'Template' at compile time. Usage: +-- +-- > {-# LANGUAGE QuasiQuotes #-} +-- > import Text.Mustache.Compile (mustache) +-- > +-- > foo :: Template +-- > foo = [mustache|This is my inline {{ template }} created at compile time|] +-- +-- Partials are not supported in the QuasiQuoter + +mustache :: QuasiQuoter +mustache = QuasiQuoter {quoteExp = \unprocessedTemplate -> do + l <- location + compileTemplateTH (fileAndLine l) unprocessedTemplate } + +-- | +-- Compile a mustache 'Template' at compile time providing a search space for any partials. Usage: +-- +-- > {-# LANGUAGE TemplateHaskell #-} +-- > import Text.Mustache.Compile (embedTemplate) +-- > +-- > foo :: Template +-- > foo = $(embedTemplate ["dir", "dir/partials"] "file.mustache") +-- + +embedTemplate :: [FilePath] -> FilePath -> Q Exp +embedTemplate searchSpace filename = do + template <- either (fail . ("Parse error in mustache template: " ++) . show) pure =<< THS.runIO (automaticCompile searchSpace filename) + let possiblePaths = do + fname <- (filename:) . HM.keys . partials $ template + path <- searchSpace + pure $ path </> fname + mapM_ addDependentRelativeFile =<< THS.runIO (filterM doesFileExist possiblePaths) + THS.lift template + +-- | +-- Compile a mustache 'Template' at compile time. Usage: +-- +-- > {-# LANGUAGE TemplateHaskell #-} +-- > import Text.Mustache.Compile (embedSingleTemplate) +-- > +-- > foo :: Template +-- > foo = $(embedSingleTemplate "dir/file.mustache") +-- +-- Partials are not supported in embedSingleTemplate + +embedSingleTemplate :: FilePath -> Q Exp +embedSingleTemplate filePath = do + addDependentRelativeFile filePath + compileTemplateTH filePath =<< THS.runIO (readFile filePath) + +fileAndLine :: Loc -> String +fileAndLine loc = loc_filename loc ++ ":" ++ (show . fst . loc_start $ loc) + +compileTemplateTH :: String -> String -> Q Exp +compileTemplateTH filename unprocessed = + either (fail . ("Parse error in mustache template: " ++) . show) THS.lift $ compileTemplate filename (pack unprocessed) + +addDependentRelativeFile :: FilePath -> Q () +addDependentRelativeFile = THS.qAddDependentFile <=< THS.runIO . makeAbsolute + +-- ERRORS + +fileNotFound :: FilePath -> ParseError +fileNotFound fp = newErrorMessage (Message $ printf "Template file '%s' not found" fp) (initialPos fp) diff --git a/mustache/src/Text/Mustache/Internal.hs b/mustache/src/Text/Mustache/Internal.hs new file mode 100644 index 0000000..c1bf8b7 --- /dev/null +++ b/mustache/src/Text/Mustache/Internal.hs @@ -0,0 +1,41 @@ +{-| +Module : $Header$ +Description : Types and conversions +Copyright : (c) Justus Adam, 2015 +License : BSD3 +Maintainer : dev@justus.science +Stability : experimental +Portability : POSIX + +escapeXML and xmlEntities curtesy to the tagsoup library. +-} +module Text.Mustache.Internal (uncons, escapeXMLText) where + + +import Data.Char (ord) +import qualified Data.IntMap as IntMap +import qualified Data.Text as T + + +uncons :: [α] -> Maybe (α, [α]) +uncons [] = Nothing +uncons (x:xs) = return (x, xs) + + +escapeXMLText :: T.Text -> T.Text +escapeXMLText = T.pack . escapeXML . T.unpack + + +escapeXML :: String -> String +escapeXML = concatMap $ \x -> IntMap.findWithDefault [x] (ord x) mp + where mp = IntMap.fromList [(ord b, "&"++a++";") | (a,[b]) <- xmlEntities] + + +xmlEntities :: [(String, String)] +xmlEntities = + [ ("quot", "\"") + , ("#39", "'") + , ("amp" , "&") + , ("lt" , "<") + , ("gt" , ">") + ] diff --git a/mustache/src/Text/Mustache/Internal/Types.hs b/mustache/src/Text/Mustache/Internal/Types.hs new file mode 100644 index 0000000..d499ba3 --- /dev/null +++ b/mustache/src/Text/Mustache/Internal/Types.hs @@ -0,0 +1,412 @@ +{-# OPTIONS_GHC -fno-warn-orphans #-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE CPP #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} +module Text.Mustache.Internal.Types where + + +import Control.Arrow +import Control.Monad.RWS hiding (lift) +import qualified Data.Aeson as Aeson +#if MIN_VERSION_aeson(2,0,0) +import qualified Data.Aeson.KeyMap as KM +#endif +import Data.Int (Int8, Int16, Int32, Int64) +import Data.Foldable (toList) +import qualified Data.HashMap.Strict as HM +import qualified Data.HashSet as HS +import qualified Data.Map as Map +import Data.Scientific +import qualified Data.Sequence as Seq +import qualified Data.Set as Set +import Data.Text +import qualified Data.Text.Lazy as LT +import qualified Data.Vector as V +import Data.Word (Word8, Word16, Word32, Word64) +import Language.Haskell.TH.Lift (deriveLift) +import Language.Haskell.TH.Syntax +import Numeric.Natural (Natural) + + +-- | Type of errors we may encounter during substitution. +data SubstitutionError + = VariableNotFound [Key] -- ^ The template contained a variable for which there was no data counterpart in the current context + | InvalidImplicitSectionContextType String -- ^ When substituting an implicit section the current context had an unsubstitutable type + | InvertedImplicitSection -- ^ Inverted implicit sections should never occur + | SectionTargetNotFound [Key] -- ^ The template contained a section for which there was no data counterpart in the current context + | PartialNotFound FilePath -- ^ The template contained a partial for which there was no data counterpart in the current context + | DirectlyRenderedValue Value -- ^ A complex value such as an Object or Array was directly rendered into the template (warning) + deriving (Show) + + +tellError :: SubstitutionError -> SubM () +tellError e = SubM $ tell ([e], []) + + +tellSuccess :: Text -> SubM () +tellSuccess s = SubM $ tell ([], [s]) + + +newtype SubM a = SubM { runSubM' :: RWS (Context Value, TemplateCache) ([SubstitutionError], [Text]) () a } deriving (Monad, Functor, Applicative, MonadReader (Context Value, TemplateCache)) + +runSubM :: SubM a -> Context Value -> TemplateCache -> ([SubstitutionError], [Text]) +runSubM comp ctx cache = snd $ evalRWS (runSubM' comp) (ctx, cache) () + +shiftContext :: Context Value -> SubM a -> SubM a +shiftContext = local . first . const + +-- | Search for a key in the current context. +-- +-- The search is conducted inside out mening the current focus +-- is searched first. If the key is not found the outer scopes are recursively +-- searched until the key is found, then 'innerSearch' is called on the result. +search :: [Key] -> SubM (Maybe Value) +search [] = return Nothing +search (key:nextKeys) = (>>= innerSearch nextKeys) <$> go + where + go = asks fst >>= \case + Context parents focus -> do + let searchParents = case parents of + (newFocus: newParents) -> shiftContext (Context newParents newFocus) $ go + _ -> return Nothing + case focus of + Object o -> + case HM.lookup key o of + Just res -> return $ Just res + _ -> searchParents + _ -> searchParents + + +-- | Searches nested scopes navigating inward. Fails if it encunters something +-- other than an object before the key is expended. +innerSearch :: [Key] -> Value -> Maybe Value +innerSearch [] v = Just v +innerSearch (y:ys) (Object o) = HM.lookup y o >>= innerSearch ys +innerSearch _ _ = Nothing + + + +-- | Syntax tree for a mustache template +type STree = ASTree Text + + +type ASTree α = [Node α] + + +-- | Basic values composing the STree +data Node α + = TextBlock α + | Section DataIdentifier (ASTree α) + | InvertedSection DataIdentifier (ASTree α) + | Variable Bool DataIdentifier + | Partial (Maybe α) FilePath + deriving (Show, Eq) + + +-- | Kinds of identifiers for Variables and sections +data DataIdentifier + = NamedData [Key] + | Implicit + deriving (Show, Eq) + + +-- | A list-like structure used in 'Value' +type Array = V.Vector Value +-- | A map-like structure used in 'Value' +type Object = HM.HashMap Text Value +-- | Source type for constructing 'Object's +type Pair = (Text, Value) + + +-- | Representation of stateful context for the substitution process +data Context α = Context { ctxtParents :: [α], ctxtFocus :: α } + deriving (Eq, Show, Ord) + +-- | Internal value representation +data Value + = Object !Object + | Array !Array + | Number !Scientific + | String !Text + | Lambda (STree -> SubM STree) + | Bool !Bool + | Null + + +instance Show Value where + show (Lambda _) = "Lambda function" + show (Object o) = show o + show (Array a) = show a + show (String s) = show s + show (Number n) = show n + show (Bool b) = show b + show Null = "null" + + +listToMustache' :: ToMustache ω => [ω] -> Value +listToMustache' = Array . V.fromList . fmap toMustache + +integralToMustache :: Integral ω => ω -> Value +integralToMustache = toMustache . toInteger + +-- | Conversion class +class ToMustache ω where + toMustache :: ω -> Value + listToMustache :: [ω] -> Value + listToMustache = listToMustache' + +instance ToMustache Float where + toMustache = Number . fromFloatDigits + +instance ToMustache Double where + toMustache = Number . fromFloatDigits + +instance ToMustache Integer where + toMustache = Number . fromInteger + +instance ToMustache Natural where + toMustache = integralToMustache + +instance ToMustache Int where + toMustache = integralToMustache + +instance ToMustache Word where + toMustache = integralToMustache + +instance ToMustache Int8 where + toMustache = integralToMustache + +instance ToMustache Int16 where + toMustache = integralToMustache + +instance ToMustache Int32 where + toMustache = integralToMustache + +instance ToMustache Int64 where + toMustache = integralToMustache + +instance ToMustache Word8 where + toMustache = integralToMustache + +instance ToMustache Word16 where + toMustache = integralToMustache + +instance ToMustache Word32 where + toMustache = integralToMustache + +instance ToMustache Word64 where + toMustache = integralToMustache + +instance ToMustache Char where + toMustache = toMustache . (:[]) + listToMustache = String . pack + +instance ToMustache Value where + toMustache = id + +instance ToMustache Bool where + toMustache = Bool + +instance ToMustache () where + toMustache = const Null + +instance ToMustache ω => ToMustache (Maybe ω) where + toMustache (Just w) = toMustache w + toMustache Nothing = Null + +instance ToMustache Text where + toMustache = String + +instance ToMustache LT.Text where + toMustache = String . LT.toStrict + +instance ToMustache Scientific where + toMustache = Number + +instance ToMustache α => ToMustache [α] where + toMustache = listToMustache + +instance ToMustache ω => ToMustache (Seq.Seq ω) where + toMustache = listToMustache' . toList + +instance ToMustache ω => ToMustache (V.Vector ω) where + toMustache = Array . fmap toMustache + +instance (ToMustache ω) => ToMustache (Map.Map Text ω) where + toMustache = mapInstanceHelper id + +instance (ToMustache ω) => ToMustache (Map.Map LT.Text ω) where + toMustache = mapInstanceHelper LT.toStrict + +instance (ToMustache ω) => ToMustache (Map.Map String ω) where + toMustache = mapInstanceHelper pack + +mapInstanceHelper :: ToMustache v => (a -> Text) -> Map.Map a v -> Value +mapInstanceHelper conv = + toMustache + . Map.foldrWithKey + (\k -> HM.insert (conv k) . toMustache) + HM.empty + +instance ToMustache ω => ToMustache (HM.HashMap Text ω) where + toMustache = Object . fmap toMustache + +instance ToMustache ω => ToMustache (HM.HashMap LT.Text ω) where + toMustache = hashMapInstanceHelper LT.toStrict + +instance ToMustache ω => ToMustache (HM.HashMap String ω) where + toMustache = hashMapInstanceHelper pack + +hashMapInstanceHelper :: ToMustache v => (a -> Text) -> HM.HashMap a v -> Value +hashMapInstanceHelper conv = + toMustache + . HM.foldrWithKey + (\k -> HM.insert (conv k) . toMustache) + HM.empty + +instance ToMustache (STree -> SubM STree) where + toMustache = Lambda + +instance ToMustache Aeson.Value where + toMustache (Aeson.Object o) = Object $ fmap toMustache +#if MIN_VERSION_aeson(2,0,0) + (KM.toHashMapText o) +#else + o +#endif + toMustache (Aeson.Array a) = Array $ fmap toMustache a + toMustache (Aeson.Number n) = Number n + toMustache (Aeson.String s) = String s + toMustache (Aeson.Bool b) = Bool b + toMustache Aeson.Null = Null + +instance ToMustache ω => ToMustache (HS.HashSet ω) where + toMustache = listToMustache' . HS.toList + +instance ToMustache ω => ToMustache (Set.Set ω) where + toMustache = listToMustache' . Set.toList + +instance (ToMustache α, ToMustache β) => ToMustache (α, β) where + toMustache (a, b) = toMustache [toMustache a, toMustache b] + +instance (ToMustache α, ToMustache β, ToMustache γ) + => ToMustache (α, β, γ) where + toMustache (a, b, c) = toMustache [toMustache a, toMustache b, toMustache c] + +instance (ToMustache α, ToMustache β, ToMustache γ, ToMustache δ) + => ToMustache (α, β, γ, δ) where + toMustache (a, b, c, d) = toMustache + [ toMustache a + , toMustache b + , toMustache c + , toMustache d + ] + +instance ( ToMustache α + , ToMustache β + , ToMustache γ + , ToMustache δ + , ToMustache ε + ) => ToMustache (α, β, γ, δ, ε) where + toMustache (a, b, c, d, e) = toMustache + [ toMustache a + , toMustache b + , toMustache c + , toMustache d + , toMustache e + ] + +instance ( ToMustache α + , ToMustache β + , ToMustache γ + , ToMustache δ + , ToMustache ε + , ToMustache ζ + ) => ToMustache (α, β, γ, δ, ε, ζ) where + toMustache (a, b, c, d, e, f) = toMustache + [ toMustache a + , toMustache b + , toMustache c + , toMustache d + , toMustache e + , toMustache f + ] + +instance ( ToMustache α + , ToMustache β + , ToMustache γ + , ToMustache δ + , ToMustache ε + , ToMustache ζ + , ToMustache η + ) => ToMustache (α, β, γ, δ, ε, ζ, η) where + toMustache (a, b, c, d, e, f, g) = toMustache + [ toMustache a + , toMustache b + , toMustache c + , toMustache d + , toMustache e + , toMustache f + , toMustache g + ] + +instance ( ToMustache α + , ToMustache β + , ToMustache γ + , ToMustache δ + , ToMustache ε + , ToMustache ζ + , ToMustache η + , ToMustache θ + ) => ToMustache (α, β, γ, δ, ε, ζ, η, θ) where + toMustache (a, b, c, d, e, f, g, h) = toMustache + [ toMustache a + , toMustache b + , toMustache c + , toMustache d + , toMustache e + , toMustache f + , toMustache g + , toMustache h + ] + +-- | A collection of templates with quick access via their hashed names +type TemplateCache = HM.HashMap String Template + +-- | Type of key used for retrieving data from 'Value's +type Key = Text + +{-| + A compiled Template with metadata. +-} +data Template = Template + { name :: String + , ast :: STree + , partials :: TemplateCache + } deriving (Show) + + +deriveLift ''DataIdentifier +deriveLift ''Node +deriveLift ''Template + +-- Data.HashMap 0.2.17.0 introduces its own Lift instance +#if !MIN_VERSION_unordered_containers(0,2,17) +instance Lift TemplateCache where +#if MIN_VERSION_template_haskell(2,16,0) + liftTyped m = [|| HM.fromList $$(liftTyped $ HM.toList m) ||] +#else + lift m = [| HM.fromList $(lift $ HM.toList m) |] +#endif +#endif + +--Data.Text 1.2.4.0 introduces its own Lift Text instance +#if !MIN_VERSION_text(1,2,4) +instance Lift Text where + lift = lift . unpack +#endif + diff --git a/mustache/src/Text/Mustache/Parser.hs b/mustache/src/Text/Mustache/Parser.hs new file mode 100644 index 0000000..317e7aa --- /dev/null +++ b/mustache/src/Text/Mustache/Parser.hs @@ -0,0 +1,311 @@ +{-| +Module : $Header$ +Description : Basic functions for dealing with mustache templates. +Copyright : (c) Justus Adam, 2015 +License : BSD3 +Maintainer : dev@justus.science +Stability : experimental +Portability : POSIX +-} +{-# LANGUAGE FlexibleContexts #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE MultiParamTypeClasses #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TupleSections #-} +module Text.Mustache.Parser + ( + -- * Generic parsing functions + + parse, parseWithConf + + -- * Configurations + + , MustacheConf(..), defaultConf + + -- * Parser + + , Parser, MustacheState + + -- * Mustache Constants + + , sectionBegin, sectionEnd, invertedSectionBegin, unescape2, unescape1 + , delimiterChange, nestingSeparator + + ) where + + +import Control.Monad +import Data.Char (isAlphaNum, isSpace) +import Data.List (nub) +import Data.Monoid ((<>)) +import Data.Text as T (Text, null, pack) +import Prelude as Prel +import Text.Mustache.Types +import Text.Parsec as P hiding (endOfLine, parse) + + +-- | Initial configuration for the parser +data MustacheConf = MustacheConf + { delimiters :: (String, String) + } + + +-- | User state for the parser +data MustacheState = MustacheState + { sDelimiters :: (String, String) + , textStack :: Text + , isBeginngingOfLine :: Bool + , currentSectionName :: Maybe DataIdentifier + } + + +data ParseTagRes + = SectionBegin Bool DataIdentifier + | SectionEnd DataIdentifier + | Tag (Node Text) + | HandledTag + + +-- | @#@ +sectionBegin :: Char +sectionBegin = '#' +-- | @/@ +sectionEnd :: Char +sectionEnd = '/' +-- | @>@ +partialBegin :: Char +partialBegin = '>' +-- | @^@ +invertedSectionBegin :: Char +invertedSectionBegin = '^' +-- | @{@ and @}@ +unescape2 :: (Char, Char) +unescape2 = ('{', '}') +-- | @&@ +unescape1 :: Char +unescape1 = '&' +-- | @=@ +delimiterChange :: Char +delimiterChange = '=' +-- | @.@ +nestingSeparator :: Char +nestingSeparator = '.' +-- | @!@ +comment :: Char +comment = '!' +-- | @.@ +implicitIterator :: Char +implicitIterator = '.' +-- | Cannot be a letter, number or the nesting separation Character @.@ +isAllowedDelimiterCharacter :: Char -> Bool +isAllowedDelimiterCharacter = + not . Prel.or . sequence + [ isSpace, isAlphaNum, (== nestingSeparator) ] +allowedDelimiterCharacter :: Parser Char +allowedDelimiterCharacter = + satisfy isAllowedDelimiterCharacter + + +-- | Empty configuration +emptyState :: MustacheState +emptyState = MustacheState ("", "") mempty True Nothing + + +-- | Default configuration (delimiters = ("{{", "}}")) +defaultConf :: MustacheConf +defaultConf = MustacheConf ("{{", "}}") + + +initState :: MustacheConf -> MustacheState +initState (MustacheConf { delimiters }) = emptyState { sDelimiters = delimiters } + + +setIsBeginning :: Bool -> Parser () +setIsBeginning b = modifyState (\s -> s { isBeginngingOfLine = b }) + + +-- | The parser monad in use +type Parser = Parsec Text MustacheState + + +(<<) :: Monad m => m b -> m a -> m b +(<<) = flip (>>) + + +endOfLine :: Parser String +endOfLine = do + r <- optionMaybe $ char '\r' + n <- char '\n' + return $ maybe id (:) r [n] + + +{-| + Runs the parser for a mustache template, returning the syntax tree. +-} +parse :: FilePath -> Text -> Either ParseError STree +parse = parseWithConf defaultConf + + +-- | Parse using a custom initial configuration +parseWithConf :: MustacheConf -> FilePath -> Text -> Either ParseError STree +parseWithConf = P.runParser parseText . initState + + +parseText :: Parser STree +parseText = do + (MustacheState { isBeginngingOfLine }) <- getState + if isBeginngingOfLine + then parseLine + else continueLine + + +appendStringStack :: String -> Parser () +appendStringStack t = modifyState (\s -> s { textStack = textStack s <> pack t}) + + +continueLine :: Parser STree +continueLine = do + (MustacheState { sDelimiters = ( start@(x:_), _ )}) <- getState + let forbidden = x : "\n\r" + + many (noneOf forbidden) >>= appendStringStack + + (try endOfLine >>= appendStringStack >> setIsBeginning True >> parseLine) + <|> (try (string start) >> switchOnTag >>= continueFromTag) + <|> (try eof >> finishFile) + <|> (anyChar >>= appendStringStack . (:[]) >> continueLine) + + +flushText :: Parser STree +flushText = do + s@(MustacheState { textStack = text }) <- getState + putState $ s { textStack = mempty } + return $ if T.null text + then [] + else [TextBlock text] + + +finishFile :: Parser STree +finishFile = + getState >>= \case + (MustacheState {currentSectionName = Nothing}) -> flushText + (MustacheState {currentSectionName = Just name}) -> + parserFail $ "Unclosed section " <> show name + + +parseLine :: Parser STree +parseLine = do + (MustacheState { sDelimiters = ( start, _ ) }) <- getState + initialWhitespace <- many (oneOf " \t") + let handleStandalone = do + tag <- switchOnTag + let continueNoStandalone = do + appendStringStack initialWhitespace + setIsBeginning False + continueFromTag tag + standaloneEnding = do + try (skipMany (oneOf " \t") >> (eof <|> void endOfLine)) + setIsBeginning True + case tag of + Tag (Partial _ name) -> + ( standaloneEnding >> + continueFromTag (Tag (Partial (Just (pack initialWhitespace)) name)) + ) <|> continueNoStandalone + Tag _ -> continueNoStandalone + _ -> + ( standaloneEnding >> + continueFromTag tag + ) <|> continueNoStandalone + (try (string start) >> handleStandalone) + <|> (try eof >> appendStringStack initialWhitespace >> finishFile) + <|> (appendStringStack initialWhitespace >> setIsBeginning False >> continueLine) + + +continueFromTag :: ParseTagRes -> Parser STree +continueFromTag (SectionBegin inverted name) = do + textNodes <- flushText + state@(MustacheState + { currentSectionName = previousSection }) <- getState + putState $ state { currentSectionName = return name } + innerSectionContent <- parseText + let sectionTag = + if inverted + then InvertedSection + else Section + modifyState $ \s -> s { currentSectionName = previousSection } + outerSectionContent <- parseText + return (textNodes <> [sectionTag name innerSectionContent] <> outerSectionContent) +continueFromTag (SectionEnd name) = do + (MustacheState + { currentSectionName }) <- getState + case currentSectionName of + Just name' | name' == name -> flushText + Just name' -> parserFail $ "Expected closing sequence for \"" <> show name <> "\" got \"" <> show name' <> "\"." + Nothing -> parserFail $ "Encountered closing sequence for \"" <> show name <> "\" which has never been opened." +continueFromTag (Tag tag) = do + textNodes <- flushText + furtherNodes <- parseText + return $ textNodes <> return tag <> furtherNodes +continueFromTag HandledTag = parseText + + +switchOnTag :: Parser ParseTagRes +switchOnTag = do + (MustacheState { sDelimiters = ( _, end )}) <- getState + + choice + [ SectionBegin False <$> (try (char sectionBegin) >> genParseTagEnd mempty) + , SectionEnd + <$> (try (char sectionEnd) >> genParseTagEnd mempty) + , Tag . Variable False + <$> (try (char unescape1) >> genParseTagEnd mempty) + , Tag . Variable False + <$> (try (char (fst unescape2)) >> genParseTagEnd (return $ snd unescape2)) + , Tag . Partial Nothing + <$> (try (char partialBegin) >> spaces >> (noneOf (nub end) `manyTill` try (spaces >> string end))) + , return HandledTag + << (try (char delimiterChange) >> parseDelimChange) + , SectionBegin True + <$> (try (char invertedSectionBegin) >> genParseTagEnd mempty >>= \case + n@(NamedData _) -> return n + _ -> parserFail "Inverted Sections can not be implicit." + ) + , return HandledTag << (try (char comment) >> manyTill anyChar (try $ string end)) + , Tag . Variable True + <$> genParseTagEnd mempty + ] + where + parseDelimChange = do + (MustacheState { sDelimiters = ( _, end )}) <- getState + spaces + delim1 <- allowedDelimiterCharacter `manyTill` space + spaces + delim2 <- allowedDelimiterCharacter `manyTill` try (spaces >> string (delimiterChange : end)) + when (delim1 == mempty || delim2 == mempty) + $ parserFail "Tags must contain more than 0 characters" + oldState <- getState + putState $ oldState { sDelimiters = (delim1, delim2) } + + +genParseTagEnd :: String -> Parser DataIdentifier +genParseTagEnd emod = do + (MustacheState { sDelimiters = ( start, end ) }) <- getState + + let nEnd = emod <> end + disallowed = nub $ nestingSeparator : start <> end + + parseOne :: Parser [Text] + parseOne = do + + one <- noneOf disallowed + `manyTill` lookAhead + (try (spaces >> void (string nEnd)) + <|> try (void $ char nestingSeparator)) + + others <- (char nestingSeparator >> parseOne) + <|> (const mempty <$> (spaces >> string nEnd)) + return $ pack one : others + spaces + (try (char implicitIterator) >> spaces >> string nEnd >> return Implicit) + <|> (NamedData <$> parseOne) diff --git a/mustache/src/Text/Mustache/Render.hs b/mustache/src/Text/Mustache/Render.hs new file mode 100644 index 0000000..7a0ad89 --- /dev/null +++ b/mustache/src/Text/Mustache/Render.hs @@ -0,0 +1,248 @@ +{-| +Module : $Header$ +Description : Functions for rendering mustache templates. +Copyright : (c) Justus Adam, 2015 +License : BSD3 +Maintainer : dev@justus.science +Stability : experimental +Portability : POSIX +-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE TypeSynonymInstances #-} +{-# OPTIONS_GHC -fno-warn-orphans #-} +module Text.Mustache.Render + ( + -- * Substitution + substitute, substituteValue + -- * Checked substitution + , checkedSubstitute, checkedSubstituteValue, SubstitutionError(..) + -- * Working with Context + , Context(..), search, innerSearch, SubM, substituteNode, substituteAST, catchSubstitute + -- * Util + , toString + ) where + + +import Control.Arrow (first, second) +import Control.Monad + +import Data.Foldable (for_) +import Data.HashMap.Strict as HM hiding (keys, map) +import Data.Maybe (fromMaybe) + +import Data.Scientific (floatingOrInteger) +import Data.Text as T (Text, isSuffixOf, pack, + replace, stripSuffix) +import qualified Data.Vector as V +import Prelude hiding (length, lines, unlines) + +import Control.Monad.Reader +import Control.Monad.Writer +import qualified Data.Text as T +import qualified Data.Text.Lazy as LT +import Text.Mustache.Internal +import Text.Mustache.Internal.Types +import Text.Mustache.Types + + +{-| + Substitutes all mustache defined tokens (or tags) for values found in the + provided data structure. + + Equivalent to @substituteValue . toMustache@. +-} +substitute :: ToMustache k => Template -> k -> Text +substitute t = substituteValue t . toMustache + + +{-| + Substitutes all mustache defined tokens (or tags) for values found in the + provided data structure and report any errors and warnings encountered during + substitution. + + This function always produces results, as in a fully substituted/rendered template, + it never halts on errors. It simply reports them in the first part of the tuple. + Sites with errors are usually substituted with empty string. + + The second value in the tuple is a template rendered with errors ignored. + Therefore if you must enforce that there were no errors during substitution + you must check that the error list in the first tuple value is empty. + + Equivalent to @checkedSubstituteValue . toMustache@. +-} +checkedSubstitute :: ToMustache k => Template -> k -> ([SubstitutionError], Text) +checkedSubstitute t = checkedSubstituteValue t . toMustache + + +{-| + Substitutes all mustache defined tokens (or tags) for values found in the + provided data structure. +-} +substituteValue :: Template -> Value -> Text +substituteValue = (snd .) . checkedSubstituteValue + + +{-| + Substitutes all mustache defined tokens (or tags) for values found in the + provided data structure and report any errors and warnings encountered during + substitution. + + This function always produces results, as in a fully substituted/rendered template, + it never halts on errors. It simply reports them in the first part of the tuple. + Sites with errors are usually substituted with empty string. + + The second value in the tuple is a template rendered with errors ignored. + Therefore if you must enforce that there were no errors during substitution + you must check that the error list in the first tuple value is empty. +-} +checkedSubstituteValue :: Template -> Value -> ([SubstitutionError], Text) +checkedSubstituteValue template dataStruct = + second T.concat $ runSubM (substituteAST (ast template)) (Context mempty dataStruct) (partials template) + +-- | Catch the results of running the inner substitution. +catchSubstitute :: SubM a -> SubM (a, Text) +catchSubstitute = fmap (second (T.concat . snd)) . SubM . hideResults . listen . runSubM' + where + hideResults = censor (\(errs, _) -> (errs, [])) + +-- | Substitute an entire 'STree' rather than just a single 'Node' +substituteAST :: STree -> SubM () +substituteAST = mapM_ substituteNode + + +-- | Main substitution function +substituteNode :: Node Text -> SubM () + +-- subtituting text +substituteNode (TextBlock t) = tellSuccess t + +-- substituting a whole section (entails a focus shift) +substituteNode (Section Implicit secSTree) = + asks fst >>= \case + Context parents focus@(Array a) + | V.null a -> return () + | otherwise -> for_ a $ \focus' -> + let newContext = Context (focus:parents) focus' + in shiftContext newContext $ substituteAST secSTree + Context _ (Object _) -> substituteAST secSTree + Context _ v -> tellError $ InvalidImplicitSectionContextType $ showValueType v + +substituteNode (Section (NamedData secName) secSTree) = + search secName >>= \case + Just arr@(Array arrCont) -> + if V.null arrCont + then return () + else do + Context parents focus <- asks fst + for_ arrCont $ \focus' -> + let newContext = Context (arr:focus:parents) focus' + in shiftContext newContext $ substituteAST secSTree + Just (Bool False) -> return () + Just Null -> return () + Just (Lambda l) -> substituteAST =<< l secSTree + Just focus' -> do + Context parents focus <- asks fst + let newContext = Context (focus:parents) focus' + shiftContext newContext $ substituteAST secSTree + Nothing -> tellError $ SectionTargetNotFound secName + +-- substituting an inverted section +substituteNode (InvertedSection Implicit _) = tellError InvertedImplicitSection +substituteNode (InvertedSection (NamedData secName) invSecSTree) = + search secName >>= \case + Just (Bool False) -> contents + Just (Array a) | V.null a -> contents + Just Null -> contents + Nothing -> contents + _ -> return () + where + contents = mapM_ substituteNode invSecSTree + +-- substituting a variable +substituteNode (Variable _ Implicit) = asks (ctxtFocus . fst) >>= toString >>= tellSuccess +substituteNode (Variable escaped (NamedData varName)) = + maybe + (tellError $ VariableNotFound varName) + (toString >=> tellSuccess . (if escaped then escapeXMLText else id)) + =<< search varName + +-- substituting a partial +substituteNode (Partial indent pName) = do + cPartials <- asks snd + case HM.lookup pName cPartials of + Nothing -> tellError $ PartialNotFound pName + Just t -> + let ast' = handleIndent indent $ ast t + in local (second (partials t `HM.union`)) $ substituteAST ast' + + +showValueType :: Value -> String +showValueType Null = "Null" +showValueType (Object _) = "Object" +showValueType (Array _) = "Array" +showValueType (String _) = "String" +showValueType (Lambda _) = "Lambda" +showValueType (Number _) = "Number" +showValueType (Bool _) = "Bool" + + +handleIndent :: Maybe Text -> STree -> STree +handleIndent Nothing ast' = ast' +handleIndent (Just indentation) ast' = preface <> content + where + preface = if T.null indentation then [] else [TextBlock indentation] + content = if T.null indentation + then ast' + else reverse $ fromMaybe [] (uncurry (:) . first dropper <$> uncons (reverse fullIndented)) + where + fullIndented = fmap (indentBy indentation) ast' + dropper (TextBlock t) = TextBlock $ + if ("\n" <> indentation) `isSuffixOf` t + then fromMaybe t $ stripSuffix indentation t + else t + dropper a = a + +indentBy :: Text -> Node Text -> Node Text +indentBy indent p@(Partial (Just indent') name') + | T.null indent = p + | otherwise = Partial (Just (indent <> indent')) name' +indentBy indent (Partial Nothing name') = Partial (Just indent) name' +indentBy indent (TextBlock t) = TextBlock $ replace "\n" ("\n" <> indent) t +indentBy _ a = a + + + +-- | Converts values to Text as required by the mustache standard +toString :: Value -> SubM Text +toString (String t) = return t +toString (Number n) = return $ either (pack . show) (pack . show) (floatingOrInteger n :: Either Double Integer) +toString (Lambda l) = do + ((), res) <- catchSubstitute $ substituteAST =<< l [] + return res +toString e = do + tellError $ DirectlyRenderedValue e + return $ pack $ show e + + +instance ToMustache (Context Value -> STree -> STree) where + toMustache f = Lambda $ (<$> askContext) . flip f + +instance ToMustache (Context Value -> STree -> Text) where + toMustache = lambdaHelper id + +instance ToMustache (Context Value -> STree -> LT.Text) where + toMustache = lambdaHelper LT.toStrict + +instance ToMustache (Context Value -> STree -> String) where + toMustache = lambdaHelper pack + +lambdaHelper :: (r -> Text) -> (Context Value -> STree -> r) -> Value +lambdaHelper conv f = Lambda $ (<$> askContext) . wrapper + where + wrapper :: STree -> Context Value -> STree + wrapper lSTree c = [TextBlock $ conv $ f c lSTree] + +instance ToMustache (STree -> SubM Text) where + toMustache f = Lambda (fmap (return . TextBlock) . f) diff --git a/mustache/src/Text/Mustache/Types.hs b/mustache/src/Text/Mustache/Types.hs new file mode 100644 index 0000000..54d5991 --- /dev/null +++ b/mustache/src/Text/Mustache/Types.hs @@ -0,0 +1,110 @@ +{-| +Module : $Header$ +Description : Types and conversions +Copyright : (c) Justus Adam, 2015 +License : BSD3 +Maintainer : dev@justus.science +Stability : experimental +Portability : POSIX +-} +module Text.Mustache.Types + ( + -- * Types for the Parser / Template + ASTree + , STree + , Node(..) + , DataIdentifier(..) + , Template(..) + , TemplateCache + -- * Types for the Substitution / Data + , Value(..) + , Key + -- ** Converting + , object + , (~>), (↝), (~=), (⥱) + , ToMustache, toMustache, mFromJSON, integralToMustache + -- ** Representation + , Array, Object, Pair + , SubM, askContext, askPartials + , Context(..) + ) where + + +import Control.Monad.Reader +import qualified Data.Aeson as Aeson +import qualified Data.HashMap.Strict as HM +import Data.Text (Text) +import Text.Mustache.Internal.Types + + +-- | Convenience function for creating Object values. +-- +-- This function is supposed to be used in conjuction with the '~>' and '~=' operators. +-- +-- ==== __Examples__ +-- +-- @ +-- data Address = Address { ... } +-- +-- instance Address ToJSON where +-- ... +-- +-- data Person = Person { name :: String, address :: Address } +-- +-- instance ToMustache Person where +-- toMustache (Person { name, address }) = object +-- [ "name" ~> name +-- , "address" ~= address +-- ] +-- @ +-- +-- Here we can see that we can use the '~>' operator for values that have +-- themselves a 'ToMustache' instance, or alternatively if they lack such an +-- instance but provide an instance for the 'ToJSON' typeclass we can use the +-- '~=' operator. +object :: [Pair] -> Value +object = Object . HM.fromList + + +-- | Map keys to values that provide a 'ToMustache' instance +-- +-- Recommended in conjunction with the `OverloadedStrings` extension. +(~>) :: ToMustache ω => Text -> ω -> Pair +(~>) t = (t, ) . toMustache +{-# INLINEABLE (~>) #-} +infixr 8 ~> + +-- | Unicode version of '~>' +(↝) :: ToMustache ω => Text -> ω -> Pair +(↝) = (~>) +{-# INLINEABLE (↝) #-} +infixr 8 ↝ + + +-- | Map keys to values that provide a 'ToJSON' instance +-- +-- Recommended in conjunction with the `OverloadedStrings` extension. +(~=) :: Aeson.ToJSON ι => Text -> ι -> Pair +(~=) t = (t ~>) . Aeson.toJSON +{-# INLINEABLE (~=) #-} +infixr 8 ~= + + +-- | Unicode version of '~=' +(⥱) :: Aeson.ToJSON ι => Text -> ι -> Pair +(⥱) = (~=) +{-# INLINEABLE (⥱) #-} +infixr 8 ⥱ + + +-- | Converts a value that can be represented as JSON to a Value. +mFromJSON :: Aeson.ToJSON ι => ι -> Value +mFromJSON = toMustache . Aeson.toJSON + + +askContext :: SubM (Context Value) +askContext = asks fst + + +askPartials :: SubM TemplateCache +askPartials = asks snd |
