threepenny-gui 0.4.0.2 → 0.4.1.0
raw patch · 20 files changed
+173/−115 lines, 20 filesdep +aesondep −jsondep ~bytestringdep ~text
Dependencies added: aeson
Dependencies removed: json
Dependency ranges changed: bytestring, text
Files
- CHANGELOG.md +12/−1
- samples/BarTab.hs +1/−1
- samples/Buttons.hs +1/−4
- samples/CRUD.hs +1/−1
- samples/Chat.hs +1/−2
- samples/CurrencyConverter.hs +1/−1
- samples/DragNDropExample.hs +1/−4
- samples/DrumMachine.hs +1/−1
- samples/MissingDollars.hs +1/−4
- samples/UseWords.hs +1/−4
- src/Graphics/UI/Threepenny/Attributes.hs +6/−5
- src/Graphics/UI/Threepenny/Core.hs +2/−2
- src/Graphics/UI/Threepenny/Internal/Driver.hs +29/−23
- src/Graphics/UI/Threepenny/Internal/FFI.hs +45/−20
- src/Graphics/UI/Threepenny/Internal/Include.hs +5/−1
- src/Graphics/UI/Threepenny/Internal/Types.hs +52/−33
- src/Graphics/UI/Threepenny/JQuery.hs +8/−3
- src/Reactive/Threepenny/Monads.hs +1/−1
- src/Reactive/Threepenny/Types.hs +1/−1
- threepenny-gui.cabal +3/−3
CHANGELOG.md view
@@ -1,8 +1,19 @@ ## Changelog for the `threepenny-gui` package -**0.4.0.1** -- Bugfix release.+**0.4.1.0** -- Maintenance release. +* Dependency on `text` package now from version 0.11 to 1.1.*.+* Dependency on `aeson` package replaces the former dependency on the `json` package.+* Unicode characters are now transmitted correctly to the browser. #75, #62.+* Change default port number to 8023. #64++**0.4.0.2** -- Bugfix release.+ * Fix CSS bug for `grid` function.++**0.4.0.1** -- Maintenance release.++* Adjust package dependencies. **0.4.0.0** -- Snapshot release.
samples/BarTab.hs view
@@ -11,7 +11,7 @@ -- | Main entry point. main :: IO ()-main = startGUI defaultConfig { tpPort = 10000 } setup+main = startGUI defaultConfig setup setup :: Window -> UI () setup w = do
samples/Buttons.hs view
@@ -13,10 +13,7 @@ main :: IO () main = do static <- getStaticDir- startGUI defaultConfig- { tpPort = 10000- , tpStatic = Just static- } setup+ startGUI defaultConfig { tpStatic = Just static } setup setup :: Window -> UI () setup w = void $ do
samples/CRUD.hs view
@@ -24,7 +24,7 @@ Main ------------------------------------------------------------------------------} main :: IO ()-main = startGUI defaultConfig { tpPort = 10000 } setup+main = startGUI defaultConfig setup setup :: Window -> UI () setup window = void $ mdo
samples/Chat.hs view
@@ -22,8 +22,7 @@ static <- getStaticDir messages <- Chan.newChan startGUI defaultConfig- { tpPort = 10000- , tpCustomHTML = Just "chat.html"+ { tpCustomHTML = Just "chat.html" , tpStatic = Just static } $ setup messages
samples/CurrencyConverter.hs view
@@ -10,7 +10,7 @@ Main ------------------------------------------------------------------------------} main :: IO ()-main = startGUI defaultConfig { tpPort = 10000 } setup+main = startGUI defaultConfig setup setup :: Window -> UI () setup window = void $ do
samples/DragNDropExample.hs view
@@ -14,10 +14,7 @@ main :: IO () main = do static <- getStaticDir- startGUI defaultConfig- { tpPort = 10000- , tpStatic = Just static- } setup+ startGUI defaultConfig { tpStatic = Just static } setup setup :: Window -> UI () setup w = void $ do
samples/DrumMachine.hs view
@@ -30,7 +30,7 @@ Main ------------------------------------------------------------------------------} main :: IO ()-main = startGUI defaultConfig { tpPort = 10000 } setup+main = startGUI defaultConfig setup setup :: Window -> UI () setup w = void $ do
samples/MissingDollars.hs view
@@ -12,10 +12,7 @@ main :: IO () main = do static <- getStaticDir- startGUI defaultConfig- { tpPort = 10000- , tpStatic = Just static- } setup+ startGUI defaultConfig { tpStatic = Just static } setup setup :: Window -> UI ()
samples/UseWords.hs view
@@ -17,10 +17,7 @@ main :: IO () main = do static <- getStaticDir- startGUI defaultConfig- { tpPort = 10000- , tpStatic = Just static- } setup+ startGUI defaultConfig { tpStatic = Just static } setup setup :: Window -> UI ()
src/Graphics/UI/Threepenny/Attributes.hs view
@@ -18,7 +18,7 @@ target, text_, type_, usemap, valign, version, vlink, vspace, width, ) where -import Text.JSON+import qualified Data.Aeson as JSON import Graphics.UI.Threepenny.Core {-----------------------------------------------------------------------------@@ -26,20 +26,21 @@ ------------------------------------------------------------------------------} -- | The @checked@ status of an input element of type checkbox. checked :: Attr Element Bool-checked = fromProp "checked" (== JSBool True) JSBool+checked = fromProp "checked" (== JSON.Bool True) JSON.Bool -- | The @enabled@ status of an input element enabled :: Attr Element Bool-enabled = fromProp "disabled" (== JSBool False) (JSBool . not)+enabled = fromProp "disabled" (== JSON.Bool False) (JSON.Bool . not) -- | Index of the currently selected option of a @<select>@ element. -- -- The index starts at @0@. -- If no option is selected, then the selection is 'Nothing'. selection :: Attr Element (Maybe Int)-selection = fromProp "selectedIndex" from (showJSON . maybe (-1) id)+selection = fromProp "selectedIndex" from (JSON.toJSON . maybe (-1) id) where- from s = let Ok x = readJSON s in if x == -1 then Nothing else Just x+ from s = let JSON.Success x = JSON.fromJSON s in + if x == -1 then Nothing else Just x {-----------------------------------------------------------------------------
src/Graphics/UI/Threepenny/Core.hs view
@@ -71,7 +71,7 @@ import qualified Control.Monad.Trans.RWS.Lazy as Monad import Network.URI-import Text.JSON+import qualified Data.Aeson as JSON import Reactive.Threepenny hiding (onChange) import qualified Reactive.Threepenny as Reactive@@ -355,7 +355,7 @@ audioStop el = runFunction $ ffi "prim_audio_stop(%1)" el -- Turn a jQuery property @.prop()@ into an attribute.-fromProp :: String -> (JSValue -> a) -> (a -> JSValue) -> Attr Element a+fromProp :: String -> (JSON.Value -> a) -> (a -> JSON.Value) -> Attr Element a fromProp name from to = mkReadWriteAttr get set where set v el = runFunction $ ffi "$(%1).prop(%2,%3)" el name (to v)
src/Graphics/UI/Threepenny/Internal/Driver.hs view
@@ -72,7 +72,7 @@ import Data.Maybe import Data.Text (Text,pack,unpack) import qualified Data.Text as Text-import Data.Text.Encoding+import Data.Text.Encoding as Text import Data.Time import Network.URI import qualified Network.WebSockets as WS@@ -83,10 +83,14 @@ import Snap.Core import qualified Snap.Http.Server as Snap import Snap.Util.FileServe+import System.Environment (getEnvironment) import System.FilePath-import qualified Text.JSON as JSON-import Text.JSON.Generic +import qualified Data.Aeson as JSON+import Data.Aeson (Result(..))+import Data.Aeson.Generic+import qualified Data.ByteString.Lazy.Char8 as LBS+import Data.Data import Graphics.UI.Threepenny.Internal.Types as Threepenny import Graphics.UI.Threepenny.Internal.Resources@@ -109,10 +113,13 @@ -- worker action. serve :: Config -> (Session -> IO ()) -> IO () serve Config{..} worker = do+ env <- getEnvironment+ let portEnv = Safe.readMay =<< Prelude.lookup "PORT" env+ server <- newServerState _ <- forkIO $ custodian 30 (sSessions server)- let config = Snap.setPort tpPort- $ Snap.setErrorLog (Snap.ConfigIoLog tpLog)+ let config = Snap.setPort (maybe defaultPort id (tpPort `mplus` portEnv))+ $ Snap.setErrorLog (Snap.ConfigIoLog tpLog) $ Snap.setAccessLog (Snap.ConfigIoLog tpLog) $ Snap.defaultConfig Snap.httpServe config . route $@@ -239,14 +246,11 @@ -- | Handle signals sent from the client. signal :: Session -> Snap () signal Session{..} = do- input <- getInput "signal"- case input of- Just signalJson -> do- let signal = JSON.decode signalJson- case signal of- Ok signal -> liftIO $ writeChan sSignals signal- Error err -> error err- Nothing -> error $ "Unable to parse " ++ show input+ input <- getParam "signal"+ let err = error $ "Unable to parse " ++ show input+ case JSON.decode . LBS.fromStrict =<< input of+ Just signal -> liftIO $ writeChan sSignals signal+ Nothing -> err {----------------------------------------------------------------------------- Implementation of two-way communication@@ -273,17 +277,19 @@ sendData <- forkIO . forever $ do x <- readChan sInstructions -- see note [Instruction strictness]- WS.sendTextData connection . Text.pack . JSON.encode $ x+ WS.sendTextData connection . JSON.encode $ x -- read data let readData = do input <- WS.receiveData connection case input of- "ping" -> WS.sendTextData connection . Text.pack $ "pong"+ "ping" -> WS.sendTextData connection . LBS.pack $ "pong" "quit" -> E.throw WS.ConnectionClosed- input -> case JSON.decode . Text.unpack $ input of- Ok signal -> writeChan sSignals signal- Error err -> E.throw $ Atto.ParseError [] err+ input -> case JSON.decode input of+ Just signal -> writeChan sSignals signal+ Nothing -> E.throw $ Atto.ParseError [] $+ "Threepenny.Internal.Core: Couldn't parse 'Signal' "+ ++ show input forever readData `E.finally` (do@@ -370,8 +376,8 @@ call window (CallJSFunction . toCode $ jsfunction) $ \signal -> case signal of FunctionResult v -> case marshalResult jsfunction window v of- Ok a -> return $ Just a- Error _ -> return Nothing+ Success a -> return $ Just a+ Error _ -> return Nothing _ -> return Nothing @@ -395,14 +401,14 @@ Snap utilities ------------------------------------------------------------------------------} -- Write JSON to output.-writeJson :: (MonadSnap m, JSON a) => a -> m ()+writeJson :: (MonadSnap m, JSON.ToJSON a) => a -> m () writeJson json = do modifyResponse $ setContentType "application/json"- (writeText . pack . (\x -> showJSValue x "") . showJSON) json+ writeLBS . JSON.encode $ json -- Get a text input from snap. getInput :: (MonadSnap f) => ByteString -> f (Maybe String)-getInput = fmap (fmap (unpack . decodeUtf8)) . getParam+getInput = fmap (fmap (unpack . Text.decodeUtf8)) . getParam -- Read an input from snap. readInput :: (MonadSnap f,Read a) => ByteString -> f (Maybe a)
src/Graphics/UI/Threepenny/Internal/FFI.hs view
@@ -9,17 +9,38 @@ FFI(..), ToJS(..), JSFunction, + showJSON,+ toCode, marshalResult, ) where -import Data.Functor+import Data.Aeson as JSON+import qualified Data.Aeson.Types as JSON+import qualified Data.Aeson.Encode import Data.ByteString (ByteString)-import qualified Data.ByteString.Char8 as BS-import Text.JSON.Generic+import Data.Data+import Data.Functor+import Data.Maybe+import Data.String (fromString)+import qualified Data.Text.Lazy+import qualified Data.Text.Lazy.Builder +import Safe (atMay)+ import Graphics.UI.Threepenny.Internal.Types {-----------------------------------------------------------------------------+ Easy, if stupid conversion between String and JSON++ TODO: Use more efficient string types like ByteString, Text, etc.+------------------------------------------------------------------------------}+showJSON :: ToJSON a => a -> String+showJSON+ = Data.Text.Lazy.unpack+ . Data.Text.Lazy.Builder.toLazyText+ . Data.Aeson.Encode.fromValue . JSON.toJSON++{----------------------------------------------------------------------------- JavaScript Code and Foreign Function Interface ------------------------------------------------------------------------------} -- | JavaScript code snippet.@@ -30,10 +51,11 @@ class ToJS a where render :: a -> JSCode -instance ToJS String where render = JSCode . show+instance ToJS String where render = render . JSON.String . fromString instance ToJS Int where render = JSCode . show instance ToJS Bool where render b = JSCode $ if b then "false" else "true"-instance ToJS JSValue where render x = JSCode $ showJSValue x ""+instance ToJS JSON.Value where render = JSCode . showJSON+-- TODO: ByteString instance may be wrong. Only needed for ElementId right now. instance ToJS ByteString where render = JSCode . show instance ToJS ElementId where render (ElementId x) = apply "elidToElement(%1)" [render x]@@ -42,8 +64,9 @@ -- | A JavaScript function with a given output type @a@. data JSFunction a = JSFunction- { code :: JSCode -- code snippet- , marshal :: Window -> JSValue -> Result a -- convert to Haskell value+ { code :: JSCode -- ^ code snippet+ , marshal :: Window -> JSON.Value -> JSON.Parser a+ -- ^ conversion to Haskell value } -- | Render function to a textual representation using JavaScript syntax.@@ -52,11 +75,11 @@ -- | Convert function result to a Haskell value. marshalResult- :: JSFunction a -- ^ Function that has been executed- -> Window -- ^ Browser context- -> JSValue -- ^ JSON representation of the return value - -> Result a -- ^ -marshalResult = marshal+ :: JSFunction a -- ^ Function that has been executed+ -> Window -- ^ Browser context+ -> JSON.Value -- ^ JSON representation of the return value + -> JSON.Result a -- ^ Function result as parsed Haskell value+marshalResult fun w = JSON.parse (marshal fun w) instance Functor JSFunction where fmap f = fmapWindow (const f)@@ -65,7 +88,7 @@ fmapWindow f (JSFunction c m) = JSFunction c (\w v -> f w <$> m w v) fromJSCode :: JSCode -> JSFunction ()-fromJSCode c = JSFunction { code = c, marshal = \_ _ -> Ok () }+fromJSCode c = JSFunction { code = c, marshal = \_ _ -> return () } -- | Helper class for making 'ffi' a variable argument function. class FFI a where@@ -74,10 +97,10 @@ instance (ToJS a, FFI b) => FFI (a -> b) where fancy f a = fancy $ f . (render a:) -instance FFI (JSFunction ()) where fancy f = fromJSCode $ f []-instance FFI (JSFunction String) where fancy = mkResult "%1.toString()"-instance FFI (JSFunction JSValue) where fancy = mkResult "%1"-instance FFI (JSFunction [ElementId]) where fancy = mkResult "elementsToElids(%1)"+instance FFI (JSFunction ()) where fancy f = fromJSCode $ f []+instance FFI (JSFunction String) where fancy = mkResult "%1.toString()"+instance FFI (JSFunction JSON.Value) where fancy = mkResult "%1"+instance FFI (JSFunction [ElementId]) where fancy = mkResult "elementsToElids(%1)" -- FIXME: We need access to IO in order to turn a Coupon into an Element. {- @@ -85,10 +108,10 @@ fancy = fmapWindow (\w elid -> Element elid w) . fancy -} -mkResult :: JSON a => String -> ([JSCode] -> JSCode) -> JSFunction a+mkResult :: FromJSON a => String -> ([JSCode] -> JSCode) -> JSFunction a mkResult client f = JSFunction { code = apply client [f []]- , marshal = \w -> readJSON+ , marshal = \w -> parseJSON } -- | Simple JavaScript FFI with string substitution.@@ -121,7 +144,9 @@ apply :: String -> [JSCode] -> JSCode apply code args = JSCode $ go code where- argument i = unJSCode (args !! i)+ at xs i = maybe (error err) id $ atMay xs i+ err = "Graphics.UI.Threepenny.FFI: Too few arguments in FFI call!"+ argument i = unJSCode (args `at` i) go [] = [] go ('%':c:cs) = argument index ++ go cs
src/Graphics/UI/Threepenny/Internal/Include.hs view
@@ -6,12 +6,15 @@ import qualified Language.Haskell.TH as TH import Language.Haskell.TH.Quote -#ifdef CABAL+#if defined(CABAL) || defined(FPCOMPLETE)+root :: FilePath root = "src/" #else+root :: FilePath root = "../src/" -- running examples from ghci #endif +include :: QuasiQuoter include = QuasiQuoter { quoteExp = f -- only used as an expression, , quotePat = undefined -- hence all other use cases undefined@@ -21,6 +24,7 @@ where f s = TH.LitE . TH.StringL <$> TH.runIO (readFileUTF8 $ root ++ s) +readFileUTF8 :: FilePath -> IO String readFileUTF8 path = do h <- openFile path ReadMode hSetEncoding h utf8
src/Graphics/UI/Threepenny/Internal/Types.hs view
@@ -1,3 +1,4 @@+{-# LANGUAGE CPP #-} {-# LANGUAGE RecordWildCards #-} {-# LANGUAGE DeriveDataTypeable #-} {-# LANGUAGE OverloadedStrings #-}@@ -9,6 +10,7 @@ import Control.Applicative import Control.Concurrent import Control.DeepSeq+import Control.Monad import qualified Reactive.Threepenny as E import Data.ByteString.Char8 (ByteString) import qualified Data.ByteString.Char8 as BS@@ -17,7 +19,11 @@ import Data.Time import Network.URI-import Text.JSON.Generic+import Data.Data+import Data.Aeson as JSON+import qualified Data.Aeson.Types as JSON+import qualified Data.Aeson.Generic+ import System.IO (stderr) import System.IO.Unsafe @@ -39,11 +45,15 @@ instance NFData ElementId where rnf (ElementId x) =+#if defined(CABAL) || defined(FPCOMPLETE) #if MIN_VERSION_bytestring(0, 10, 0) rnf x #else BS.length x `seq` () #endif+#else+ rnf x+#endif type EventId = String type Handlers = Map EventId (E.Handler EventData)@@ -51,11 +61,11 @@ -- Marshalling ElementId-instance JSON ElementId where- showJSON (ElementId o) = showJSON o- readJSON obj = do- obj <- readJSON obj- ElementId <$> valFromObj "Element" obj+instance ToJSON ElementId where+ toJSON (ElementId o) = toJSON o+instance FromJSON ElementId where+ parseJSON (Object v) = ElementId <$> v .: "Element"+ parseJSON _ = mzero -- | Perform an action on the element.@@ -149,18 +159,29 @@ -- | Record for configuring the Threepenny GUI server. data Config = Config- { tpPort :: Int -- ^ Port number.- , tpCustomHTML :: Maybe FilePath -- ^ Custom HTML file to replace the default one.- , tpStatic :: Maybe FilePath -- ^ Directory that is served under @/static@.- , tpLog :: ByteString -> IO () -- ^ Print a single log message.- }+ { tpPort :: Maybe Int + -- ^ Port number.+ -- @Nothing@ means that the port number is+ -- read from the environment variable @PORT@.+ -- Alternatively, port @8023@ is used if this variable is not set.+ , tpCustomHTML :: Maybe FilePath+ -- ^ Custom HTML file to replace the default one.+ , tpStatic :: Maybe FilePath+ -- ^ Directory that is served under @/static@.+ , tpLog :: ByteString -> IO ()+ -- ^ Print a single log message.+ } +defaultPort :: Int+defaultPort = 8023+ -- | Default configuration. ----- Port 10000, no custom HTML, no static directory, logging to stderr.+-- Port from environment variable or @8023@,+-- no custom HTML, no static directory, logging to stderr. defaultConfig :: Config defaultConfig = Config- { tpPort = 10000+ { tpPort = Nothing , tpCustomHTML = Nothing , tpStatic = Nothing , tpLog = \s -> BS.hPut stderr s >> BS.hPut stderr "\n"@@ -182,9 +203,8 @@ | Delete ElementId deriving (Typeable,Data,Show) -instance JSON Instruction where- readJSON _ = error "JSON.Instruction.readJSON: No method implemented."- showJSON x = toJSON x +instance ToJSON Instruction where+ toJSON x = Data.Aeson.Generic.toJSON x instance NFData Instruction where rnf (Debug x ) = rnf x@@ -202,29 +222,28 @@ | Event ElementId EventId [Maybe String] | Values [String] | FunctionCallValues [Maybe String]- | FunctionResult JSValue+ | FunctionResult JSON.Value deriving (Typeable,Show) -instance JSON Signal where- showJSON _ = error "JSON.Signal.showJSON: No method implemented."- readJSON obj = do- obj <- readJSON obj- let quit = Quit <$> valFromObj "Quit" obj+instance FromJSON Signal where+ parseJSON (Object v) = do+ let quit = Quit <$> v .: "Quit" event = do- e <- valFromObj "Event" obj- elid <- valFromObj "Element" e- eventId <- valFromObj "EventId" e- arguments <- valFromObj "Params" e+ e <- v .: "Event"+ elid <- e .: "Element"+ eventId <- e .: "EventId"+ arguments <- e .: "Params" args <- mapM nullable arguments return $ Event elid eventId args- values = Values <$> valFromObj "Values" obj+ values = Values <$> v .: "Values" fcallvalues = do- FunctionCallValues <$> (valFromObj "FunctionCallValues" obj >>= mapM nullable)- fresult = FunctionResult <$> valFromObj "FunctionResult" obj+ FunctionCallValues <$> (v .: "FunctionCallValues" >>= mapM nullable)+ fresult = FunctionResult <$> v .: "FunctionResult" quit <|> event <|> values <|> fcallvalues <|> fresult+ parseJSON _ = mzero --- | Read a JSValue that may be null.-nullable :: JSON a => JSValue -> Result (Maybe a)-nullable JSNull = return Nothing-nullable v = Just <$> readJSON v+-- | Read a JSON Value that may be null.+nullable :: FromJSON a => JSON.Value -> JSON.Parser (Maybe a)+nullable Null = return Nothing+nullable v = Just <$> parseJSON v
src/Graphics/UI/Threepenny/JQuery.hs view
@@ -2,15 +2,18 @@ module Graphics.UI.Threepenny.JQuery where import Control.Arrow+import Data.Aeson as JSON+import Data.String import Data.Char import Data.Default import Data.Maybe import Graphics.UI.Threepenny.Core+import Graphics.UI.Threepenny.Internal.FFI (showJSON) import qualified Graphics.UI.Threepenny.Internal.Driver as Core-import qualified Graphics.UI.Threepenny.Internal.Types as Core-import Text.JSON+import qualified Graphics.UI.Threepenny.Internal.Types as Core import Reactive.Threepenny + data Easing = Swing | Linear deriving (Eq,Enum,Show) @@ -23,8 +26,10 @@ Core.withElement (toElement el) $ \elid window -> callDeferredFunction window "jquery_animate"- [encode elid,encode (makeObj (map (second showJSON) props)),show duration,map toLower (show easing)]+ [showJSON elid,showJSON propsJSON,show duration,map toLower (show easing)] (const complete)+ where+ propsJSON = JSON.object [fromString name .= val | (name,val) <- props] -- | Fade in an element. fadeIn :: Element -> Int -> Easing -> IO () -> IO ()
src/Reactive/Threepenny/Monads.hs view
@@ -8,5 +8,5 @@ ------------------------------------------------------------------------------} runEvalP :: Values -> EvalP a -> IO (a, Values) runEvalP pulses m = do- (a, s, w) <- runRWST m () pulses+ (a, s, _) <- runRWST m () pulses return (a, s)
src/Reactive/Threepenny/Types.hs view
@@ -1,7 +1,7 @@ module Reactive.Threepenny.Types where import Control.Monad.Trans.RWS.Lazy-import Data.Functor.Identity+import Data.Functor.Identity () import Data.Hashable import qualified Data.Vault.Strict as Vault.Strict
threepenny-gui.cabal view
@@ -1,5 +1,5 @@ Name: threepenny-gui-Version: 0.4.0.2+Version: 0.4.1.0 Synopsis: GUI framework that uses the web browser as a display. Description: Threepenny-GUI is a GUI framework that uses the web browser as a display.@@ -92,6 +92,7 @@ cpp-options: -DREBUG ghc-options: -O2 build-depends: base >= 4 && < 5+ ,aeson == 0.6.* ,attoparsec-enumerator == 0.3.* ,bytestring >= 0.9.2 && < 0.11 ,containers >= 0.4.2 && < 0.6@@ -99,7 +100,6 @@ ,deepseq == 1.3.* ,filepath == 1.3.* ,hashable >= 1.1.0 && < 1.3- ,json >= 0.4.4 && < 0.8 ,MonadCatchIO-transformers == 0.3.* ,network >= 2.3.0 && < 2.5 ,safe == 0.3.*@@ -107,7 +107,7 @@ ,snap-core == 0.9.* ,stm >= 2.3 && < 2.5 ,template-haskell >= 2.7.0 && < 2.9- ,text == 0.11.*+ ,text >= 0.11 && < 1.2 ,time == 1.4.* ,transformers == 0.3.* ,unordered-containers == 0.2.*