HSmarty 0.1.0.0 → 0.1.1.0
raw patch · 5 files changed
+697/−2 lines, 5 files
Files
- HSmarty.cabal +3/−2
- Text/HSmarty/Parser/Expr.hs +164/−0
- Text/HSmarty/Parser/Util.hs +65/−0
- Text/HSmarty/Render/Engine.hs +388/−0
- Text/HSmarty/Types.hs +77/−0
HSmarty.cabal view
@@ -1,5 +1,5 @@ name: HSmarty-version: 0.1.0.0+version: 0.1.1.0 synopsis: Haskell implementation of a subset of the PHP-Smarty template language -- description: license: BSD3@@ -14,7 +14,8 @@ Library exposed-modules: Text.HSmarty- other-modules: Text.HSmarty.Parser.Smarty+ other-modules: Text.HSmarty.Parser.Smarty, Text.HSmarty.Parser.Expr, Text.HSmarty.Parser.Util, + Text.HSmarty.Render.Engine, Text.HSmarty.Types build-depends: base ==4.6.*, vector ==0.10.*, text ==0.11.*,
+ Text/HSmarty/Parser/Expr.hs view
@@ -0,0 +1,164 @@+-----------------------------------------------------------------------------+-- |+-- Module : Text.Parsec.Expr+-- Copyright : (c) Daan Leijen 1999-2001, (c) Paolo Martini 2007+-- License : BSD-style (see the LICENSE file)+--+-- Author : derek.a.elkins@gmail.com+-- Ported by : Sebastiaan Visser <haskell@fvisser.nl>+-- Stability : provisional+-- Portability : non-portable+--+-- A helper module to parse \"expressions\".+-- Builds a parser given a table of operators and associativities.+--+-----------------------------------------------------------------------------++module Text.HSmarty.Parser.Expr+ ( Assoc(..), Operator(..), OperatorTable+ , buildExpressionParser+ ) where++import Control.Applicative+import Data.Monoid+import Data.Attoparsec.Combinator+import Data.Attoparsec.Types++-----------------------------------------------------------+-- Assoc and OperatorTable+-----------------------------------------------------------++-- | This data type specifies the associativity of operators: left, right+-- or none.++data Assoc = AssocNone+ | AssocLeft+ | AssocRight++-- | This data type specifies operators that work on values of type @a@.+-- An operator is either binary infix or unary prefix or postfix. A+-- binary operator has also an associated associativity.++data Operator t a = Infix (Parser t (a -> a -> a)) Assoc+ | Prefix (Parser t (a -> a))+ | Postfix (Parser t (a -> a))++-- | An @OperatorTable@ is a list of @Operator@+-- lists. The list is ordered in descending+-- precedence. All operators in one list have the same precedence (but+-- may have a different associativity).++type OperatorTable t a = [[Operator t a]]++-----------------------------------------------------------+-- Convert an OperatorTable and basic term parser into+-- a full fledged expression parser+-----------------------------------------------------------++-- | @buildExpressionParser table term@ builds an expression parser for+-- terms @term@ with operators from @table@, taking the associativity+-- and precedence specified in @table@ into account. Prefix and postfix+-- operators of the same precedence can only occur once (i.e. @--2@ is+-- not allowed if @-@ is prefix negate). Prefix and postfix operators+-- of the same precedence associate to the left (i.e. if @++@ is+-- postfix increment, than @-2++@ equals @-1@, not @-3@).+--+-- The @buildExpressionParser@ takes care of all the complexity+-- involved in building expression parser. Here is an example of an+-- expression parser that handles prefix signs, postfix increment and+-- basic arithmetic.+--+-- > expr = buildExpressionParser table term+-- > <?> "expression"+-- >+-- > term = parens expr+-- > <|> natural+-- > <?> "simple expression"+-- >+-- > table = [ [prefix "-" negate, prefix "+" id ]+-- > , [postfix "++" (+1)]+-- > , [binary "*" (*) AssocLeft, binary "/" (div) AssocLeft ]+-- > , [binary "+" (+) AssocLeft, binary "-" (-) AssocLeft ]+-- > ]+-- >+-- > binary name fun assoc = Infix (do{ reservedOp name; return fun }) assoc+-- > prefix name fun = Prefix (do{ reservedOp name; return fun })+-- > postfix name fun = Postfix (do{ reservedOp name; return fun })++buildExpressionParser :: Monoid t => [[Operator t b]] -> Parser t b -> Parser t b+buildExpressionParser operators simpleExpr+ = foldl makeParser simpleExpr operators+ where+ makeParser term ops+ = let (rassoc,lassoc,nassoc+ ,prefix,postfix) = foldr splitOp ([],[],[],[],[]) ops++ rassocOp = choice rassoc+ lassocOp = choice lassoc+ nassocOp = choice nassoc+ prefixOp = choice prefix+ postfixOp = choice postfix++ ambigious assoc op= do{ _ <- op; fail ("ambiguous use of a " ++ assoc+ ++ " associative operator")+ }++ ambigiousRight = ambigious "right" rassocOp+ ambigiousLeft = ambigious "left" lassocOp+ ambigiousNon = ambigious "non" nassocOp++ termP = do{ pre <- prefixP+ ; x <- term+ ; post <- postfixP+ ; return (post (pre x))+ }++ postfixP = postfixOp <|> return id++ prefixP = prefixOp <|> return id++ rassocP x = do{ f <- rassocOp+ ; y <- do{ z <- termP; rassocP1 z }+ ; return (f x y)+ }+ <|> ambigiousLeft+ <|> ambigiousNon+ -- <|> return x++ rassocP1 x = rassocP x <|> return x++ lassocP x = do{ f <- lassocOp+ ; y <- termP+ ; lassocP1 (f x y)+ }+ <|> ambigiousRight+ <|> ambigiousNon+ -- <|> return x++ lassocP1 x = lassocP x <|> return x++ nassocP x = do{ f <- nassocOp+ ; y <- termP+ ; ambigiousRight+ <|> ambigiousLeft+ <|> ambigiousNon+ <|> return (f x y)+ }+ -- <|> return x++ in do{ x <- termP+ ; rassocP x <|> lassocP x <|> nassocP x <|> return x+ }+++ splitOp (Infix op assoc) (rassoc,lassoc,nassoc,prefix,postfix)+ = case assoc of+ AssocNone -> (rassoc,lassoc,op:nassoc,prefix,postfix)+ AssocLeft -> (rassoc,op:lassoc,nassoc,prefix,postfix)+ AssocRight -> (op:rassoc,lassoc,nassoc,prefix,postfix)++ splitOp (Prefix op) (rassoc,lassoc,nassoc,prefix,postfix)+ = (rassoc,lassoc,nassoc,op:prefix,postfix)++ splitOp (Postfix op) (rassoc,lassoc,nassoc,prefix,postfix)+ = (rassoc,lassoc,nassoc,prefix,op:postfix)
+ Text/HSmarty/Parser/Util.hs view
@@ -0,0 +1,65 @@+{-# LANGUAGE OverloadedStrings #-}+module Text.HSmarty.Parser.Util where++import Control.Applicative+import Data.Attoparsec.Text+import Data.Char+import Numeric (readHex)+import Prelude hiding (takeWhile)+import qualified Data.Text as T++eolP :: Parser T.Text+eolP =+ "\n" <$ (string "\r\n" <|> string "\n" <|> string "\r") <|>+ "" <$ endOfInput++boolP :: Parser Bool+boolP =+ const True <$> string "true" <|>+ const False <$> string "false"++stringP :: Parser T.Text+stringP = (quotedString '"' <|> quotedString '\'') <?> "stringP"++identP :: (Char -> Bool) -> (Char -> Bool) -> Parser T.Text+identP first rest =+ (T.cons <$> satisfy first <*> takeWhile rest) <?> "identP"++stripSpace = between optSpace_ optSpace_++space_ = skipWhile1 isSpace+optSpace_ = skipWhile isSpace++between :: Parser a -> Parser b -> Parser c -> Parser c+between left right main = left *> main <* right++skipWhile1 pred = (() <$ takeWhile1 pred) <?> "skipWhile1"++quotedString :: Char -> Parser T.Text+quotedString c = T.pack <$> between (char c) (char c) (many innerChar)+ where innerChar = char '\\' *> (escapeSeq <|> unicodeSeq)+ <|> satisfy (`notElem` [c,'\\'])++escapeSeq :: Parser Char+escapeSeq = choice (zipWith decode "bnfrt\\\"'" "\b\n\f\r\t\\\"'")+ where decode c r = r <$ char c++unicodeSeq :: Parser Char+unicodeSeq = char 'u' *> (intToChar <$> decodeHexUnsafe <$> count 4 hexDigit)+ where intToChar = toEnum . fromIntegral++decodeHexUnsafe :: String -> Integer+decodeHexUnsafe hex = (head $ map fst $ readHex hex)++hexDigitUpper = satisfy (inClass "0-9A-F")+hexDigit = satisfy (inClass "0-9a-fA-F")++braced :: Parser l -> Parser r -> Parser a -> Parser a+braced l r =+ between (l *> optSpace_) (optSpace_ *> r)++listLike :: Parser l -> Parser r -> Parser s -> Parser a -> Parser [a]+listLike l r sep inner = braced l r (sepBy inner (stripSpace sep))++tupleP :: Parser p -> Parser [p]+tupleP = listLike (char '(') (char ')') (char ',')
+ Text/HSmarty/Render/Engine.hs view
@@ -0,0 +1,388 @@+{-# OPTIONS_GHC -fwarn-unused-imports -fwarn-incomplete-patterns #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE DoAndIfThenElse #-}+module Text.HSmarty.Render.Engine+ ( renderTemplate, mkParam, TemplateParam )+where++import Text.HSmarty.Types+import Text.HSmarty.Parser.Smarty++import Control.Applicative+import Control.Monad.Error+import Data.Attoparsec.Text (Number(..))+import Data.Char (ord)+import Data.Maybe+import Data.Vector ((!?))+import Network.HTTP.Base (urlEncode)+import qualified Data.Aeson as A+import qualified Data.HashMap.Strict as HM+import qualified Data.Text as T+import qualified Data.Text.IO as T+import qualified Data.Vector as V++newtype TemplateParam+ = TemplateParam { unTemplateParam :: A.Value }+ deriving (Show, Eq)++data TemplateVar+ = TemplateVar+ { tv_value :: A.Value+ , tv_props :: PropMap+ }+ deriving (Show, Eq)++type Env = HM.HashMap T.Text TemplateVar+type ParamMap = HM.HashMap T.Text TemplateParam+type PropMap = HM.HashMap T.Text A.Value++type EvalM a = ErrorT T.Text IO a++instance Error T.Text where+ strMsg = T.pack++mkParam :: A.ToJSON a => a -> TemplateParam+mkParam = TemplateParam . A.toJSON++mkEnv :: ParamMap -> Env+mkEnv =+ HM.map (\init -> TemplateVar (unTemplateParam init) HM.empty)++renderTemplate :: FilePath -> ParamMap -> IO (Either T.Text T.Text)+renderTemplate fp mp =+ do ct <- T.readFile fp+ tpl <- parseSmarty fp ct+ runErrorT $ evalTpl (mkEnv mp) tpl++applyPrintDirective :: T.Text -> PrintDirective -> EvalM T.Text+applyPrintDirective t "urlencode" =+ return $ T.pack $ urlEncode $ T.unpack t+applyPrintDirective t "nl2br" =+ return $ T.replace "\n" "<br />" t+applyPrintDirective t "escape" =+ return $ T.pack $ htmlEscape $ T.unpack t+ where+ forbidden = "<&\">'/"+ htmlEscape :: String -> String+ htmlEscape [] = []+ htmlEscape (x:xs) =+ if x `elem` forbidden+ then concat [ "&#" ++ show (ord x) ++ ";"+ , htmlEscape xs+ ]+ else x : htmlEscape xs+applyPrintDirective _ pd =+ throwError $ T.concat [ "Unknown print directive `"+ , pd+ , "`"+ ]++evalTpl :: Env -> Smarty -> EvalM T.Text+evalTpl env (Smarty filename tpl) =+ evalBody env tpl++evalStmt :: Env -> SmartyStmt -> EvalM T.Text+evalStmt _ (SmartyText t) = return t+evalStmt _ (SmartyComment _) = return T.empty+evalStmt env (SmartyPrint expr directives) =+ do t <- exprToText env expr+ foldM applyPrintDirective t directives+evalStmt env (SmartyIf (If cases elseBody)) =+ do evaledCases <- mapM (\(cond, body) ->+ do r <- evalExpr env cond+ b <- evalBody env body+ case r of+ (A.Bool True) ->+ return $ Just b+ _ ->+ return Nothing+ ) cases+ case catMaybes evaledCases of+ (x:_) ->+ return x+ _ ->+ case elseBody of+ Just elseB ->+ evalBody env elseB+ Nothing ->+ return T.empty++evalStmt env (SmartyForeach (Foreach source mKey val body elseBody)) =+ do evaledSource <- evalExpr env source+ (preparedSource, size) <- mkForeachInput evaledSource+ if size == 0+ then case elseBody of+ Just b -> evalBody env b+ Nothing -> return T.empty+ else do runs <- mapM (evalForeachBody env mKey val body) preparedSource+ return $ T.concat runs++evalBody :: Env -> [SmartyStmt] -> EvalM T.Text+evalBody env stmt =+ do b <- mapM (evalStmt env) stmt+ return $ T.concat b++exprToText :: Env -> Expr -> EvalM T.Text+exprToText env expr =+ do evaled <- evalExpr env expr+ case evaled of+ A.String t -> return t+ A.Number n -> return $ T.pack $ show n+ A.Null -> return "null"+ A.Bool b ->+ return (if b then "true" else "false")+ A.Object o ->+ return $ T.pack $ show o+ A.Array a ->+ return $ T.pack $ show a++evalForeachBody :: Env -> Maybe T.Text -> T.Text -> [ SmartyStmt ] -> ( A.Value, A.Value, PropMap ) -> EvalM T.Text+evalForeachBody env mKey item body (keyVal, itemVal, props) =+ let env' = HM.insert item (TemplateVar itemVal props) env+ env'' =+ case mKey of+ Just key -> HM.insert key (TemplateVar keyVal HM.empty) env'+ Nothing -> env'+ in evalBody env'' body+++mkForeachInput :: A.Value -> EvalM ( [ ( A.Value, A.Value, PropMap ) ], Int)+mkForeachInput (A.Array vec) =+ return $ ( V.toList $ V.imap (\idx elem ->+ ( A.Number (I $ fromIntegral idx)+ , elem+ , mkForeachMap idx fSize+ )+ ) vec+ , fSize+ )+ where+ fSize = V.length vec+mkForeachInput (A.Object hm) =+ let (_, input) =+ HM.foldlWithKey' (\(idx, out) key elem ->+ let newElem = ( A.String key+ , elem+ , mkForeachMap idx hSize+ )+ in (idx+1, newElem : out)+ ) (0, []) hm+ in return $ (reverse input, hSize)+ where+ hSize = HM.size hm+mkForeachInput _ =+ throwError "Tried to iterate over non traversable type."++mkForeachMap :: Int -> Int -> PropMap+mkForeachMap idx' size' =+ HM.fromList [ ("index", A.Number (I idx))+ , ("iteration", A.Number (I $ 1 + idx))+ , ("first", A.Bool $ idx == 0)+ , ("last", A.Bool $ (idx+1) == size)+ , ("total", A.Number (I size))+ ]+ where+ size = fromIntegral size'+ idx = fromIntegral idx'++str :: T.Text -> A.Value -> EvalM T.Text+str _ (A.String x) = return x+str desc _ = throwError $ T.concat [ "`", desc, "` is not a string!" ]++int :: T.Text -> A.Value -> EvalM Int+int _ (A.Number (I x)) = return (fromIntegral x)+int desc _ = throwError $ T.concat [ "`", desc, "` is not an integer!" ]++dbl :: T.Text -> A.Value -> EvalM Double+dbl _ (A.Number (D x)) = return x+dbl desc _ = throwError $ T.concat [ "`", desc, "` is not a double!" ]++ifExists :: (Eq a, Show a) => T.Text -> a -> [(a, A.Value)] -> (A.Value -> EvalM b) -> EvalM b+ifExists msg key env fun =+ case lookup key env of+ Just x -> fun x+ Nothing ->+ throwError $ T.concat [ "`", T.pack $ show key, "` is not given. ", msg]++lookupStr :: T.Text -> T.Text -> [(T.Text, A.Value)] -> EvalM T.Text+lookupStr funName key env =+ ifExists (T.concat ["Param for `", funName, "`"]) key env (str key)++evalFunCall :: Env -> T.Text -> [ (T.Text, Expr) ] -> EvalM A.Value+evalFunCall env "include" args =+ do evaledArgs <- mapM (\(k, expr) ->+ do val <- evalExpr env expr+ return (k, val)+ ) args+ filename <- lookupStr "include" "file" evaledArgs+ let otherArgs = filter (\arg@(k, _) ->+ not $ k `elem` [ "include" ]+ ) evaledArgs+ asTplParams = HM.fromList $ map (\(k, v) -> (k, TemplateParam v)) otherArgs+ content <- liftIO $ renderTemplate (T.unpack filename) asTplParams+ case content of+ Right c ->+ return $ A.String c+ Left e ->+ throwError $ T.concat ["Include failed. Error: ", e]+evalFunCall env fname _ =+ throwError $ T.concat [ "Call to undefined function "+ , fname+ ]+++evalExpr :: Env -> Expr -> EvalM A.Value+evalExpr _ (ExprLit v) = return v+evalExpr env (ExprBin op) =+ evalBinOp env op+evalExpr env (ExprFun funCall) =+ evalFunCall env (f_name funCall) (f_args funCall)+evalExpr env (ExprVar v) =+ case HM.lookup varName env of+ Just tplVar ->+ case v of+ (Variable { v_prop = Just propReq }) ->+ case HM.lookup propReq (tv_props tplVar) of+ Just val -> return val+ Nothing -> throwError $ T.concat [ "Property `"+ , propReq+ , "` is not defined for variable `"+ , varName+ , "`"+ ]+ (Variable { v_path = path, v_index = mIdx }) ->+ let pathName = T.concat [ varName+ , if (length path > 0) then "." else T.empty+ , T.intercalate "." path+ ]+ idxWalk val =+ case mIdx of+ Just eIdx ->+ do res <- evalExpr env eIdx+ walkIndex pathName res val+ Nothing ->+ return val++ in do pathRes <- walkPath varName path $ tv_value tplVar+ idxWalk pathRes+++ Nothing ->+ throwError $ T.concat [ "Variable `"+ , varName+ , "` is not defined"+ ]+ where+ varName = v_name v++walkIndex :: T.Text -> A.Value -> A.Value -> EvalM A.Value+walkIndex vname (A.Number (I idx)) (A.Array arr) =+ case arr !? (fromIntegral idx) of+ Just val -> return val+ Nothing ->+ throwError $ T.concat [ "Out of bounds. `"+ , vname+ , "["+ , T.pack $ show idx+ , "]` not defined."+ ]+walkIndex vname idx _ =+ throwError $ T.concat [ "Can't access `"+ , T.pack $ show idx+ , "` in `"+ , vname+ , "`. Index is not an integer or value not an array!"+ ]++walkPath :: T.Text -> [T.Text] -> A.Value -> EvalM A.Value+walkPath vname [] val = return val+walkPath vname (path:xs) (A.Object obj) =+ case HM.lookup path obj of+ Just val -> walkPath (T.concat [vname, ".", path]) xs val+ Nothing ->+ throwError $ T.concat [ "Variable `"+ , vname+ , "` doesn't have the key `"+ , path+ , "`"+ ]+walkPath vname (path:xs) _ =+ throwError $ T.concat [ "Variable `"+ , vname+ , "` is not a map! Can't lookup `"+ , path+ , "`"+ ]++evalBinOp :: Env -> BinOp -> EvalM A.Value+evalBinOp env (BinEq a b) =+ boolResOp (\x y -> return $ x == y) (a, b) env+evalBinOp env (BinNot e) =+ do e' <- evalExpr env e+ case e' of+ A.Bool a ->+ return (A.Bool $ not a)+ _ ->+ throwError "Tried to evaluate a NOT on a non boolean value"+evalBinOp env (BinOr x y) =+ boolOp "Or" (||) (x, y) env+evalBinOp env (BinAnd x y) =+ boolOp "And" (&&) (x, y) env+evalBinOp env (BinLarger x y) =+ numOp "Larger" (>) (x, y) env+evalBinOp env (BinLargerEq x y) =+ numOp "LargerEq" (>=) (x, y) env+evalBinOp env (BinSmaller x y) =+ numOp "Smaller" (<) (x, y) env+evalBinOp env (BinSmallerEq x y) =+ numOp "SmallerEq" (<=) (x, y) env+evalBinOp env (BinPlus x y) =+ calcOp "Plus" (+) (x, y) env+evalBinOp env (BinMinus x y) =+ calcOp "Minus" (-) (x, y) env+evalBinOp env (BinMul x y) =+ calcOp "Mul" (*) (x, y) env+evalBinOp env (BinDiv x y) =+ calcOp "Div" (/) (x, y) env+++boolOp :: T.Text -> (Bool -> Bool -> Bool) -> (Expr, Expr) -> Env -> EvalM A.Value+boolOp d op exprs env =+ boolResOp bOp exprs env+ where+ bOp (A.Bool a) (A.Bool b) =+ return $ a `op` b+ bOp _ _ = throwError $ T.concat [ "Tried ", d, "Op and on two non boolean values" ]++numOp :: T.Text -> (Number -> Number -> Bool) -> (Expr, Expr) -> Env -> EvalM A.Value+numOp =+ numGenOp boolResOp++calcOp :: T.Text -> (Number -> Number -> Number) -> (Expr, Expr) -> Env -> EvalM A.Value+calcOp =+ numGenOp numResOp++numGenOp :: ((A.Value -> A.Value -> EvalM a)+ -> (Expr, Expr) -> Env -> EvalM A.Value)+ -> T.Text -> (Number -> Number -> a) -> (Expr, Expr) -> Env -> EvalM A.Value+numGenOp fun d op exprs env =+ fun nOp exprs env+ where+ nOp (A.Number a) (A.Number b) =+ return $ a `op` b+ nOp _ _ = throwError $ T.concat [ "Tried ", d, "Op and on two non numeric values" ]++numResOp :: (A.Value -> A.Value -> EvalM Number)+ -> (Expr, Expr) -> Env -> EvalM A.Value+numResOp fun (a, b) env =+ do a' <- evalExpr env a+ b' <- evalExpr env b+ A.Number <$> fun a' b'++boolResOp :: (A.Value -> A.Value -> EvalM Bool)+ -> (Expr, Expr) -> Env -> EvalM A.Value+boolResOp fun (a, b) env =+ do a' <- evalExpr env a+ b' <- evalExpr env b+ A.Bool <$> fun a' b'
+ Text/HSmarty/Types.hs view
@@ -0,0 +1,77 @@+{-# OPTIONS_GHC -fwarn-unused-imports -fwarn-incomplete-patterns #-}+{-# LANGUAGE OverloadedStrings #-}+module Text.HSmarty.Types where++import qualified Data.Text as T+import qualified Data.Aeson as A++data Smarty+ = Smarty+ { s_name :: FilePath+ , s_template :: [ SmartyStmt ]+ } deriving (Eq, Show)++type PrintDirective = T.Text++data SmartyStmt+ = SmartyText T.Text+ | SmartyComment T.Text+ | SmartyIf If+ | SmartyForeach Foreach+ | SmartyPrint Expr [ PrintDirective ]+ deriving (Eq, Show)++data Expr+ = ExprVar Variable+ | ExprLit A.Value+ | ExprFun FunctionCall+ | ExprBin BinOp+ deriving (Eq, Show)++data Variable+ = Variable+ { v_name :: T.Text+ , v_path :: [T.Text]+ , v_index :: Maybe Expr+ , v_prop :: Maybe T.Text+ }+ deriving (Eq, Show)++data FunctionCall+ = FunctionCall+ { f_name :: T.Text+ , f_args :: [ (T.Text, Expr) ]+ }+ deriving (Eq, Show)++data BinOp+ = BinEq Expr Expr+ | BinNot Expr+ | BinAnd Expr Expr+ | BinOr Expr Expr+ | BinLarger Expr Expr+ | BinSmaller Expr Expr+ | BinLargerEq Expr Expr+ | BinSmallerEq Expr Expr+ | BinPlus Expr Expr+ | BinMinus Expr Expr+ | BinMul Expr Expr+ | BinDiv Expr Expr+ deriving (Eq, Show)++data If+ = If+ { if_cases :: [ (Expr, [SmartyStmt]) ]+ , if_else :: Maybe [SmartyStmt]+ }+ deriving (Eq, Show)++data Foreach+ = Foreach+ { f_source :: Expr+ , f_key :: Maybe T.Text+ , f_item :: T.Text+ , f_body :: [SmartyStmt]+ , f_else :: Maybe [SmartyStmt]+ }+ deriving (Eq, Show)