ginger 0.2.8.0 → 0.3.0.0
raw patch · 4 files changed
+407/−40 lines, 4 filesdep +timePVP ok
version bump matches the API change (PVP)
Dependencies added: time
API changes (from Hackage documentation)
+ Text.Ginger.GVal: (~!) :: (FromGVal m v) => GVal m -> GVal m -> Maybe v
+ Text.Ginger.GVal: (~:) :: ToGVal m a => a -> Cons m -> Cons m
+ Text.Ginger.GVal: class FromGVal m a where fromGValEither = maybe (Left "Conversion from GVal failed") Right . fromGVal fromGVal = either (const Nothing) Just . fromGValEither
+ Text.Ginger.GVal: fromGVal :: FromGVal m a => GVal m -> Maybe a
+ Text.Ginger.GVal: fromGValEither :: FromGVal m a => GVal m -> Either String a
+ Text.Ginger.GVal: fromGValM :: (Monad m, FromGVal m a) => GVal m -> m a
+ Text.Ginger.GVal: gcons :: ToGVal m a => a -> Cons m -> Cons m
+ Text.Ginger.GVal: infixr 5 ~:
+ Text.Ginger.GVal: instance (Text.Ginger.GVal.FromGVal m a, Text.Ginger.GVal.FromGVal m b) => Text.Ginger.GVal.FromGVal m (a, b)
+ Text.Ginger.GVal: instance (Text.Ginger.GVal.FromGVal m a, Text.Ginger.GVal.FromGVal m b, Text.Ginger.GVal.FromGVal m c) => Text.Ginger.GVal.FromGVal m (a, b, c)
+ Text.Ginger.GVal: instance (Text.Ginger.GVal.FromGVal m a, Text.Ginger.GVal.FromGVal m b, Text.Ginger.GVal.FromGVal m c, Text.Ginger.GVal.FromGVal m d) => Text.Ginger.GVal.FromGVal m (a, b, c, d)
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m ()
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m (Text.Ginger.GVal.GVal m)
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Aeson.Types.Internal.Value
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Scientific.Scientific
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Text.Internal.Text
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Time.Calendar.Days.Day
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Time.Format.Locale.TimeLocale
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Time.LocalTime.LocalTime.LocalTime
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Time.LocalTime.TimeOfDay.TimeOfDay
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m Data.Time.LocalTime.TimeZone.TimeZone
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m GHC.Types.Bool
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m GHC.Types.Int
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m a => Text.Ginger.GVal.FromGVal m (GHC.Base.Maybe a)
+ Text.Ginger.GVal: instance Text.Ginger.GVal.FromGVal m a => Text.Ginger.GVal.FromGVal m [a]
+ Text.Ginger.GVal: instance Text.Ginger.GVal.ToGVal m Data.Time.Calendar.Days.Day
+ Text.Ginger.GVal: instance Text.Ginger.GVal.ToGVal m Data.Time.Format.Locale.TimeLocale
+ Text.Ginger.GVal: instance Text.Ginger.GVal.ToGVal m Data.Time.LocalTime.LocalTime.LocalTime
+ Text.Ginger.GVal: instance Text.Ginger.GVal.ToGVal m Data.Time.LocalTime.TimeOfDay.TimeOfDay
+ Text.Ginger.GVal: list :: Cons m -> GVal m
+ Text.Ginger.GVal: lookupLooseDef :: GVal m -> GVal m -> GVal m -> GVal m
+ Text.Ginger.GVal: packPair :: ([Char], [Char]) -> (Text, Text)
+ Text.Ginger.GVal: pairwise :: (a -> b) -> (a, a) -> (b, b)
+ Text.Ginger.GVal: picoToScientific :: Pico -> Scientific
+ Text.Ginger.GVal: scientificToPico :: Scientific -> Pico
+ Text.Ginger.GVal: toInt0 :: GVal m -> Int
+ Text.Ginger.GVal: toIntDef :: Int -> GVal m -> Int
+ Text.Ginger.GVal: type Cons m = [GVal m]
+ Text.Ginger.GVal: unpackPair :: (Text, Text) -> ([Char], [Char])
Files
- ginger.cabal +3/−1
- src/Text/Ginger/GVal.hs +317/−39
- src/Text/Ginger/Run.hs +2/−0
- src/Text/Ginger/Run/Builtins.hs +85/−0
ginger.cabal view
@@ -2,7 +2,7 @@ -- documentation, see http://haskell.org/cabal/users-guide/ name: ginger-version: 0.2.8.0+version: 0.3.0.0 synopsis: An implementation of the Jinja2 template language in Haskell description: Ginger is Jinja, minus the most blatant pythonisms. Wants to be feature complete, but isn't quite there yet.@@ -41,6 +41,7 @@ , safe >= 0.3 , scientific >= 0.3 , text+ , time , transformers >= 0.3 , unordered-containers >= 0.2.5 , utf8-string@@ -76,6 +77,7 @@ , tasty-hunit , tasty-quickcheck , text+ , time , transformers , unordered-containers >= 0.2.5 , utf8-string
src/Text/Ginger/GVal.hs view
@@ -18,7 +18,7 @@ import Prelude ( (.), ($), (==), (/=) , (++), (+), (-), (*), (/), div- , (>>=), return+ , (=<<), (>>=), return , undefined, otherwise, id, const , Maybe (..) , Bool (..)@@ -46,8 +46,12 @@ import Data.Scientific ( Scientific , floatingOrInteger , toBoundedInteger- , toRealFloat + , toRealFloat+ , scientific+ , coefficient+ , base10Exponent )+import Data.Fixed (Fixed (..), Pico) import Control.Applicative import qualified Data.Aeson as JSON import qualified Data.HashMap.Strict as HashMap@@ -58,6 +62,17 @@ import Data.Default (Default, def) import Text.Printf import Debug.Trace (trace)+import Data.Time ( Day (..)+ , defaultTimeLocale+ , toModifiedJulianDay+ , formatTime+ , toGregorian+ , fromGregorian+ , LocalTime (..)+ , TimeOfDay (..)+ , TimeZone (..)+ , TimeLocale (..)+ ) import Text.Ginger.Html @@ -273,6 +288,98 @@ , isNull = False } +instance ToGVal m Day where+ toGVal x =+ let (year, month, day) = toGregorian x+ julian = toModifiedJulianDay x+ formatted = Text.pack $ formatTime defaultTimeLocale "%0Y-%m-%d" x+ in (orderedDict+ [ "year" ~> year+ , "month" ~> month+ , "day" ~> day+ ])+ { asHtml = html $ formatted+ , asText = formatted+ , asBoolean = True+ , asNumber = Just . fromIntegral $ julian+ , asList = Just+ [ toGVal year+ , toGVal month+ , toGVal day+ ]+ }++instance ToGVal m TimeOfDay where+ toGVal x =+ let TimeOfDay hours minutes seconds = x+ formatted = Text.pack $ formatTime defaultTimeLocale "%H:%M:%S" x+ in (orderedDict+ [ "hours" ~> hours+ , "minutes" ~> minutes+ , "seconds" ~> picoToScientific seconds+ ])+ { asHtml = html $ formatted+ , asText = formatted+ , asBoolean = True+ , asNumber = Nothing+ , asList = Just+ [ toGVal hours+ , toGVal minutes+ , toGVal (picoToScientific seconds)+ ]+ }++instance ToGVal m LocalTime where+ toGVal x =+ let (year, month, day) = toGregorian $ localDay x+ TimeOfDay hours minutes seconds = localTimeOfDay x+ formatted = Text.pack $ formatTime defaultTimeLocale "%0Y-%m-%d %H:%M:%S" x+ in (orderedDict+ [ "year" ~> year+ , "month" ~> month+ , "day" ~> day+ , "hours" ~> hours+ , "minutes" ~> minutes+ , "seconds" ~> picoToScientific seconds+ , "date" ~> localDay x+ , "time" ~> localTimeOfDay x+ ])+ { asHtml = html $ formatted+ , asText = formatted+ , asBoolean = True+ , asNumber = Nothing+ , asList = Just+ [ toGVal year+ , toGVal month+ , toGVal day+ , toGVal hours+ , toGVal minutes+ , toGVal (picoToScientific seconds)+ ]+ }++instance ToGVal m TimeLocale where+ toGVal t =+ let formattedExample =+ Text.pack . formatTime t "%c" $+ LocalTime (fromGregorian 2000 1 1) (TimeOfDay 13 15 00)+ in (dict+ [ "dayNames" ~> wDays t+ , "monthNames" ~> months t+ , "amPm" ~> amPm t+ , "dateTimeFmt" ~> dateTimeFmt t+ , "dateFmt" ~> dateFmt t+ , "timeFmt" ~> timeFmt t+ , "time12Fmt" ~> time12Fmt t+ -- TODO+ -- , "knownTimeZones" ~> knownTimeZones t+ ])+ { asHtml = html $ formattedExample+ , asText = formattedExample+ , asBoolean = True+ , asNumber = Nothing+ }+ instance (ToGVal m a, ToGVal m b) => ToGVal m (a, b) where toGVal (a, b) = toGVal ([ toGVal a, toGVal b ] :: [GVal m]) @@ -282,43 +389,6 @@ instance (ToGVal m a, ToGVal m b, ToGVal m c, ToGVal m d) => ToGVal m (a, b, c, d) where toGVal (a, b, c, d) = toGVal ([ toGVal a, toGVal b, toGVal c, toGVal d ] :: [GVal m]) --- * Convenience API for constructing heterogenous dictionaries.------ Example usage:------ > context :: GVal m--- > context = dict [ "number" ~> (15 :: Int), "name" ~> ("Joe" :: String) ]---- | A key/value pair, used for constructing dictionary GVals using a--- compact syntax.-type Pair m = (Text, GVal m)---- | Construct a dictionary GVal from a list of pairs. Internally, this uses--- a hashmap, so element order will not be preserved.-dict :: [Pair m] -> GVal m-dict = toGVal . HashMap.fromList---- | Construct an ordered dictionary GVal from a list of pairs. Internally,--- this conversion uses both a hashmap (for O(1) lookup) and the original list,--- so element order is preserved, but there is a bit of a memory overhead.-orderedDict :: [Pair m] -> GVal m-orderedDict xs =- def- { asHtml = mconcat . Prelude.map (asHtml . snd) $ xs- , asText = mconcat . Prelude.map (asText . snd) $ xs- , asBoolean = not . Prelude.null $ xs- , isNull = False- , asLookup = Just (`HashMap.lookup` hm)- , asDictItems = Just xs- }- where- hm = HashMap.fromList xs---- | Construct a pair from a key and a value.-(~>) :: ToGVal m a => Text -> a -> Pair m-k ~> v = (k, toGVal v)-infixr 8 ~>- -- | Silly helper function, needed to bypass the default 'Show' instance of -- 'Scientific' in order to make integral 'Scientific's look like integers. scientificToText :: Scientific -> Text@@ -429,6 +499,66 @@ } +-- * Convenience API for constructing heterogenous dictionaries.+--+-- Example usage:+--+-- > context :: GVal m+-- > context = dict [ "number" ~> (15 :: Int), "name" ~> ("Joe" :: String) ]++-- | A key/value pair, used for constructing dictionary GVals using a+-- compact syntax.+type Pair m = (Text, GVal m)++-- | Construct a dictionary GVal from a list of pairs. Internally, this uses+-- a hashmap, so element order will not be preserved.+dict :: [Pair m] -> GVal m+dict = toGVal . HashMap.fromList++-- | Construct an ordered dictionary GVal from a list of pairs. Internally,+-- this conversion uses both a hashmap (for O(1) lookup) and the original list,+-- so element order is preserved, but there is a bit of a memory overhead.+orderedDict :: [Pair m] -> GVal m+orderedDict xs =+ def+ { asHtml = mconcat . Prelude.map (asHtml . snd) $ xs+ , asText = mconcat . Prelude.map (asText . snd) $ xs+ , asBoolean = not . Prelude.null $ xs+ , isNull = False+ , asLookup = Just (`HashMap.lookup` hm)+ , asDictItems = Just xs+ }+ where+ hm = HashMap.fromList xs++-- | Construct a pair from a key and a value.+(~>) :: ToGVal m a => Text -> a -> Pair m+k ~> v = (k, toGVal v)+infixr 8 ~>++-- * Convenience API for constructing heterogenous lists++type Cons m = [GVal m]++-- | Alias for '(~:)'.+gcons :: ToGVal m a => a -> Cons m -> Cons m+gcons = (:) . toGVal++-- | This operator allows constructing heterogenous lists using cons-style+-- syntax, e.g.:+--+-- >>> asText $ list ("Found " ~: (6 :: Int) ~: " items" ~: [] :: [GVal IO])+-- "Found 6 items"+(~:) :: ToGVal m a => a -> Cons m -> Cons m+(~:) = gcons+infixr 5 ~:++-- | Construct a GVal from a list of GVals. This is equivalent to the 'toGVal'+-- implementation of @[GVal m]@, but typed more narrowly for clarity and+-- disambiguation.+list :: Cons m -> GVal m+list = toGVal+ -- * Inspecting 'GVal's / Marshalling 'GVal' to Haskell -- | Check if the given GVal is a list-like object@@ -468,6 +598,15 @@ lookupLoose k v = lookupKey (asText k) v <|> lookupIndexMay (floor <$> asNumber k) v +-- | Like 'lookupLoose', but fall back to the given default value if+-- the key is not in the dictionary, or if the indexee is not a+-- dictionary-like object.+lookupLooseDef :: GVal m -> GVal m -> GVal m -> GVal m+lookupLooseDef d k = fromMaybe d . lookupLoose k++(~!) :: (FromGVal m v) => GVal m -> GVal m -> Maybe v+g ~! k = lookupLoose k g >>= fromGVal+ -- | Treat a 'GVal' as a dictionary and list all the keys, with no particular -- ordering. keys :: GVal m -> Maybe [Text]@@ -483,6 +622,16 @@ toInt :: GVal m -> Maybe Int toInt x = toNumber x >>= toBoundedInteger +-- | Convert a 'GVal' to an 'Int', falling back to the given+-- default if the conversion fails.+toIntDef :: Int -> GVal m -> Int+toIntDef d = fromMaybe d . toInt++-- | Convert a 'GVal' to an 'Int', falling back to zero (0)+-- if the conversion fails.+toInt0 :: GVal m -> Int+toInt0 = toIntDef 0+ -- | Loose cast to boolean. -- -- Numeric zero, empty strings, empty lists, empty objects, 'Null', and boolean@@ -496,3 +645,132 @@ -- it's not. toFunction :: GVal m -> Maybe (Function m) toFunction = asFunction++picoToScientific :: Pico -> Scientific+picoToScientific (MkFixed x) = scientific x (-12)++scientificToPico :: Scientific -> Pico+scientificToPico s =+ MkFixed (Prelude.floor $ scientific (coefficient s) (base10Exponent s + 12))++class FromGVal m a where+ fromGValEither :: GVal m -> Either Prelude.String a+ fromGValEither = Prelude.maybe (Left "Conversion from GVal failed") Right . fromGVal+ fromGVal :: GVal m -> Maybe a+ fromGVal = Prelude.either (const Nothing) Just . fromGValEither++fromGValM :: (Monad m, FromGVal m a) => GVal m -> m a+fromGValM = Prelude.either Prelude.fail return . fromGValEither++instance FromGVal m Int where+ fromGVal = toInt++instance FromGVal m Scientific where+ fromGVal = asNumber++instance FromGVal m Text where+ fromGVal = Just . asText++instance FromGVal m (GVal m) where+ fromGVal = Just++instance FromGVal m a => FromGVal m (Maybe a) where+ fromGVal = \g ->+ if isNull g+ then+ Just Nothing+ else+ Just <$> fromGVal g++instance FromGVal m Bool where+ fromGVal = Just . asBoolean++instance FromGVal m JSON.Value where+ fromGVal = asJSON++instance FromGVal m () where+ fromGVal g = if isNull g then Just () else Nothing++instance FromGVal m a => FromGVal m [a] where+ fromGVal g = asList g >>= mapM fromGVal++instance ( FromGVal m a+ , FromGVal m b+ ) => FromGVal m (a, b) where+ fromGVal g = case asList g of+ Just [a, b] ->+ (,) <$> fromGVal a+ <*> fromGVal b+ _ -> Nothing++instance ( FromGVal m a+ , FromGVal m b+ , FromGVal m c+ ) => FromGVal m (a, b, c) where+ fromGVal g = case asList g of+ Just [a, b, c] ->+ (,,) <$> fromGVal a+ <*> fromGVal b+ <*> fromGVal c+ _ -> Nothing++instance ( FromGVal m a+ , FromGVal m b+ , FromGVal m c+ , FromGVal m d+ ) => FromGVal m (a, b, c, d) where+ fromGVal g = case asList g of+ Just [a, b, c, d] ->+ (,,,) <$> fromGVal a+ <*> fromGVal b+ <*> fromGVal c+ <*> fromGVal d+ _ -> Nothing++instance FromGVal m Day where+ fromGVal g = do+ year <- fromIntegral <$> (g ~! "year" :: Maybe Int)+ month <- g ~! "month"+ day <- g ~! "day"+ return $ fromGregorian year month day++instance FromGVal m TimeOfDay where+ fromGVal g = do+ hours <- g ~! "hours"+ minutes <- g ~! "minutes"+ seconds <- scientificToPico <$> g ~! "seconds"+ return $ TimeOfDay hours minutes seconds++instance FromGVal m LocalTime where+ fromGVal g = do+ date <- fromGVal g <|> g ~! "date"+ time <- fromGVal g <|> g ~! "time"+ return $ LocalTime date time++instance FromGVal m TimeZone where+ fromGVal g =+ TimeZone+ <$> g ~! "minutes"+ <*> g ~! "summerOnly"+ <*> (Text.unpack <$> g ~! "name")++instance FromGVal m TimeLocale where+ fromGVal g =+ TimeLocale+ <$> (List.map unpackPair <$> g ~! "dayNames")+ <*> (List.map unpackPair <$> g ~! "monthNames")+ <*> (unpackPair <$> g ~! "amPm")+ <*> (Text.unpack <$> g ~! "dateTimeFmt")+ <*> (Text.unpack <$> g ~! "dateFmt")+ <*> (Text.unpack <$> g ~! "timeFmt")+ <*> (Text.unpack <$> g ~! "time12Fmt")+ <*> (g ~! "knownTimeZones")++pairwise :: (a -> b) -> (a, a) -> (b, b)+pairwise f (a, b) = (f a, f b)++packPair :: ([Char], [Char]) -> (Text, Text)+packPair = pairwise Text.pack++unpackPair :: (Text, Text) -> ([Char], [Char])+unpackPair = pairwise Text.unpack
src/Text/Ginger/Run.hs view
@@ -100,6 +100,8 @@ , ("concat", fromFunction . variadicStringFunc $ mconcat) , ("contains", fromFunction gfnContains) , ("d", fromFunction gfnDefault)+ , ("date", fromFunction gfnDateFormat)+ , ("dateformat", fromFunction gfnDateFormat) , ("default", fromFunction gfnDefault) , ("difference", fromFunction . variadicNumericFunc 0 $ difference) , ("e", fromFunction gfnEscape)
src/Text/Ginger/Run/Builtins.hs view
@@ -59,6 +59,15 @@ import Network.HTTP.Types (urlEncode) import Debug.Trace (trace) import Data.List (lookup, zipWith, unzip)+import Data.Time ( defaultTimeLocale+ , formatTime+ , LocalTime (..)+ , TimeOfDay (..)+ , fromGregorian+ , Day (..)+ , parseTimeM+ )+import Data.Foldable (asum) gfnRawHtml :: Monad m => Function (Run m h) gfnRawHtml = unaryFunc (toGVal . unsafeRawHtml . asText)@@ -356,6 +365,82 @@ return . toGVal $ printfG fmtStr (fmap snd args) where fmtStr = Text.unpack $ asText fmtStrG++gvalToDate :: GVal m -> Maybe LocalTime+gvalToDate g = gvalDictToDate g+ <|> gvalListToDate g+ <|> gvalAutoParseDate g++gvalDictToDate :: GVal m -> Maybe LocalTime+gvalDictToDate g = do+ let datePartMay = do+ year <- fmap (fromIntegral :: Int -> Integer) $ g ~! "year"+ month <- g ~! "month"+ day <- g ~! "day"+ return $ fromGregorian year month day+ timePartMay = do+ hours <- g ~! "hours"+ minutes <- g ~! "minutes"+ seconds <- fmap scientificToPico $ g ~! "seconds"+ return $ TimeOfDay hours minutes seconds+ when (isNothing datePartMay && isNothing timePartMay) Nothing+ let datePart = fromMaybe (fromGregorian 1970 1 1) datePartMay+ timePart = fromMaybe (TimeOfDay 12 0 0) timePartMay+ return $ LocalTime datePart timePart++gvalListToDate :: GVal m -> Maybe LocalTime+gvalListToDate g = go =<< asList g+ where+ go :: [GVal m] -> Maybe LocalTime+ go parts = case parts of+ [yearG, monthG, dayG, hoursG, minutesG, secondsG] -> do+ datePart <-+ fromGregorian+ <$> (fromIntegral <$> toInt yearG)+ <*> toInt monthG+ <*> toInt dayG+ timePart <-+ TimeOfDay+ <$> toInt hoursG+ <*> toInt minutesG+ <*> (fromIntegral <$> toInt secondsG)+ return $ LocalTime datePart timePart+ [yearG, monthG, dayG, hoursG, minutesG] ->+ go [yearG, monthG, dayG, hoursG, minutesG, toGVal (0 :: Int)]+ [yearG, monthG, dayG] ->+ go [yearG, monthG, dayG, toGVal (12 :: Int), toGVal (0 :: Int)]+ _ -> Nothing++gvalAutoParseDate :: GVal m -> Maybe LocalTime+gvalAutoParseDate = go . Text.unpack . asText+ where+ go input =+ asum+ . fmap (\fmt -> parseTimeM True defaultTimeLocale fmt input)+ $ formats+ formats =+ [ "%Y-%m-%d %H:%M:%S"+ ]++gfnDateFormat :: Monad m => Function (Run m h)+gfnDateFormat args =+ let Right [gDate, gFormat, gTimeZone, gLocale] =+ extractArgsDefL+ [ ("date", def)+ , ("format", def)+ , ("tz", def)+ , ("locale", def)+ ]+ args+ dateMay = gvalToDate gDate+ fmtMay = Text.unpack <$> fromGVal gFormat+ in case fmtMay of+ Just fmt -> do+ let locale = fromMaybe defaultTimeLocale $+ fromGVal gLocale+ return . toGVal $ formatTime locale fmt <$> dateMay+ Nothing -> do+ return . toGVal $ dateMay gfnFilter :: Monad m => Function (Run m h) gfnFilter [] = return def