module Data.EasyTpl.Render where
import Data.Aeson (Value(..))
import qualified Data.Aeson.Encode as JE
import Data.ByteString (ByteString)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Char8 as BSC
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Builder as LTB
import qualified Data.Text.Encoding as TE
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as H
import qualified Data.Vector as V
import Data.Attoparsec.ByteString.Char8
import qualified Data.Attoparsec.Text as AT
#ifdef WITH_REGEX
import Text.Regex (Regex)
import qualified Text.Regex as RE
#endif
import Data.Fixed (mod')
import Prelude hiding (takeWhile)
import Data.EasyTpl.Types
import Data.EasyTpl.Parser
renderTemplate' :: Template -> HashMap Text Value -> ByteString
renderTemplate' tpl env = renderTemplate tpl $ Object env
renderTemplate :: Template -> Value -> ByteString
renderTemplate (Template tokens) env = BS.concat $ map (flip renderToken env) tokens
renderToken :: TemplateToken -> Value -> ByteString
renderToken (ContentToken text) _ = text
renderToken (LiteralToken expr) env = getString $ toString $ evalExpression expr env
where
getString (String text) = TE.encodeUtf8 text
getString _ = ""
renderToken (ControlToken ctl tpl) env = renderControl ctl tpl env
renderControl :: Control -> Template -> Value -> ByteString
renderControl (Condition expr) tpl env@(Object _) = applyCondition $ toBoolean $ evalExpression expr env
where
applyCondition :: Value -> ByteString
applyCondition (Bool True) = renderTemplate tpl env
applyCondition _ = ""
renderControl (Iteration (keyVar, valVar) expr) tpl env'@(Object henv) = BS.concat $ applyIteration $ evalExpression expr env'
where
applyIteration :: Value -> [ByteString]
applyIteration (Array list) = V.toList $ V.imap applyIndex list
applyIteration (Object hash) = map applyField $ H.toList hash
applyIteration _ = []
addLocal :: Text -> Value -> HashMap Text Value -> HashMap Text Value
addLocal var val env = if T.null var then env else H.insert var val env
applyElement :: Value -> Value -> ByteString
applyElement key val = renderTemplate' tpl $ addLocal keyVar key $ addLocal valVar val henv
applyIndex :: Int -> Value -> ByteString
applyIndex key val = applyElement (Number (fromIntegral key)) val
applyField :: (Text, Value) -> ByteString
applyField (key, val) = applyElement (String key) val
renderControl _ _ _ = ""
evalExpression :: Expression -> Value -> Value
evalExpression (Constant val) _ = val
evalExpression (Variable var) (Object henv) = maybe Null id $ H.lookup var henv
evalExpression (Variable _) _ = Null
evalExpression (Range from' to' step') env = genRange (toNumber $ evalExpression from' env)
(toNumber $ evalExpression to' env)
(toNumber $ evalExpression step' env)
where
genRange :: Value -> Value -> Value -> Value
genRange (Number from) (Number to) (Number step) =
Array $ V.fromList $ range from to step
genRange _ _ _ = Null
range from to step | from > to = []
| otherwise = Number from : range (from + step) to step
evalExpression (UnaryOperation op' a') env = unary op' (evalExpression a' env)
where
unary :: UnaryOperator -> Value -> Value
unary GetLength = getLength
unary Stringify = stringify
unary Evaluate = either (String . T.pack . ("Error: " ++)) (flip evalExpression env) . parseOnly parseExpression . toByteString
unary LogicNot = booleanOp not
unary Negate = numericOp (0 )
unary ToNumber = numericOp id
unary NotNull = Bool . notNull
notNull :: Value -> Bool
notNull Null = False
notNull _ = True
booleanOp :: (Bool -> Bool) -> Value -> Value
booleanOp op a = case toBoolean a of
Bool b -> Bool $ op b
_ -> Null
numericOp op a = case toNumber a of
Number n -> Number $ op n
_ -> Null
toByteString a = case toString a of
String s -> TE.encodeUtf8 s
_ -> ""
#ifdef WITH_REGEX
evalExpression (BinaryOperation op' a' (Regexp re ml cs)) env = apply op' $ toString $ evalExpression a' env
where
apply RegexTest (String s) = Bool $ maybe False (\_ -> True) $ match s
apply RegexMatch (String s) = maybe Null (Array . V.fromList . map (String . T.pack)) $ match s
apply RegexSplit (String s) = Array $ V.fromList $ map String $ split s
apply _ _ = Null
match :: Text -> Maybe [String]
match = RE.matchRegex regex . T.unpack
split :: Text -> [Text]
split = map T.pack . RE.splitRegex regex . T.unpack
regex :: Regex
regex = RE.mkRegexWithOpts (BSC.unpack re) ml cs
evalExpression (Regexp _ _ _) _ = Null
#endif
evalExpression (BinaryOperation op' a' b') env = binary op' (evalExpression a' env) (evalExpression b' env)
where
binary :: BinaryOperator -> Value -> Value -> Value
binary Equal a = Bool . (a ==)
binary NotEqual a = Bool . (a /=)
binary Append (String a) = applyToString $ String . T.append a
binary Multiply (String a) = applyToInteger $ String . flip T.replicate a
binary IntDivide (String a) = applyToString $ Array . V.fromList . map String . flip T.splitOn a
binary LessThan (String a) = applyToString $ Bool . (a <)
binary GreatThan (String a) = applyToString $ Bool . (a >)
binary LessEqual (String a) = applyToString $ Bool . (a <=)
binary GreatEqual (String a) = applyToString $ Bool . (a >=)
binary GetField (String s) = applyToInteger (\i -> if i >= 0 && i < T.length s then String $ T.singleton $ T.index s i else Null)
binary LessThan (Number a) = applyToNumber $ Bool . (a <)
binary GreatThan (Number a) = applyToNumber $ Bool . (a >)
binary LessEqual (Number a) = applyToNumber $ Bool . (a <=)
binary GreatEqual (Number a) = applyToNumber $ Bool . (a >=)
binary GetField (Array a) = applyToInteger (maybe Null id . (V.!?) a)
binary GetField (Object a) = applyToString (maybe Null id . flip H.lookup a)
binary op a = binary' op a
binary' LogicOr = booleanOp (||)
binary' LogicAnd = booleanOp (&&)
binary' Substract = numericOp ()
binary' Append = numericOp (+)
binary' Multiply = numericOp (*)
binary' Divide = numericOp (/)
binary' IntDivide = numericOp' (/)
binary' Module = numericOp mod'
binary' _ = \_ _ -> Null
numericOp op a b = case (toNumber a, toNumber b) of
(Number na, Number nb) -> Number $ op na nb
_ -> Null
numericOp' op a b = case (toNumber a, toNumber b) of
(Number na, Number nb) -> Number $ fromIntegral ((round $ op na nb) :: Integer)
_ -> Null
booleanOp op a b = case (toBoolean a, toBoolean b) of
(Bool ba, Bool bb) -> Bool $ op ba bb
_ -> Null
applyToString fn v = case toString v of
String s -> fn s
_ -> Null
applyToNumber fn v = case toNumber v of
Number n -> fn $ n
_ -> Null
applyToInteger fn = applyToNumber (fn . round)
getLength :: Value -> Value
getLength (String text) = Number $ fromIntegral $ T.length text
getLength (Array list) = Number $ fromIntegral $ V.length list
getLength (Object hash) = Number $ fromIntegral $ H.size hash
getLength _ = Null
toBoolean :: Value -> Value
toBoolean bool@(Bool _) = bool
#if MIN_VERSION_attoparsec(0,12,0)
toBoolean (Number val) = Bool $ val /= 0
#else
toBoolean (Number val) = Bool $ val /= I 0
#endif
toBoolean (String text) = Bool $ not $ T.null text
toBoolean Null = Bool False
toBoolean (Object hash) = Bool $ not $ H.null hash
toBoolean (Array list) = Bool $ not $ V.null list
toNumber :: Value -> Value
toNumber val@(Number _) = val
toNumber (Bool bool) = Number $ if bool then 1 else 0
#if MIN_VERSION_attoparsec(0,12,0)
toNumber (String text) = getNumber' $ AT.parse AT.scientific text
where
getNumber' (AT.Done _ val) = Number val
getNumber' _ = Null
#else
toNumber (String text) = getNumber' $ AT.parse AT.number text
where
getNumber' (AT.Done _ val) = Number val
getNumber' _ = Null
#endif
toNumber _ = Null
toString :: Value -> Value
toString text@(String _) = text
toString (Number val) = String $ T.pack $ show val
toString Null = String "[null]"
toString (Bool _) = String "[bool]"
toString (Object _) = String "[object]"
toString (Array _) = String "[array]"
stringify :: Value -> Value
stringify = String . LT.toStrict . LTB.toLazyText . JE.fromValue