{-# LANGUAGE CPP #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE UndecidableInstances #-}
module Lucid.Base
(
renderText
,renderBS
,renderTextT
,renderBST
,renderToFile
,execHtmlT
,evalHtmlT
,runHtmlT
,relaxHtmlT
,commuteHtmlT
,makeElement
,makeElementNoEnd
,makeXmlElementNoEnd
,makeAttribute
,Html
,HtmlT(HtmlT)
,Attribute(..)
,Term(..)
,TermRaw(..)
,ToHtml(..)
,With(..))
where
import Blaze.ByteString.Builder (Builder)
import qualified Blaze.ByteString.Builder as Blaze
import qualified Blaze.ByteString.Builder.Html.Utf8 as Blaze
import Control.Applicative
import Control.Monad
import Control.Monad.Morph
import Control.Monad.Reader
import Control.Monad.Error.Class (MonadError(..))
import Control.Monad.State.Class (MonadState(..))
import Control.Monad.Writer.Class (MonadWriter(..))
import Data.ByteString.Lazy (ByteString)
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString as S
import Data.Functor.Identity
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as M
import Data.Hashable (Hashable(..))
import Data.Semigroup (Semigroup (..))
import Data.Monoid (Monoid (..))
import Data.String
import Data.Text (Text)
import qualified Data.Text.Lazy as LT
import qualified Data.Text.Lazy.Encoding as LT
import qualified Data.Text.Encoding as T
import Data.Typeable (Typeable)
import Prelude
data Attribute = Attribute !Text !Text
deriving (Show,Eq,Typeable)
instance Hashable Attribute where
hashWithSalt salt (Attribute a b) = salt `hashWithSalt` a `hashWithSalt` b
type Html = HtmlT Identity
newtype HtmlT m a =
HtmlT {runHtmlT :: m (HashMap Text Text -> Builder,a)
}
#if __GLASGOW_HASKELL__ >= 707
deriving (Typeable)
#endif
instance MFunctor HtmlT where
hoist f (HtmlT xs) = HtmlT (f xs)
instance (a ~ (),Applicative m) => Semigroup (HtmlT m a) where
(<>) = liftA2 (<>)
instance (a ~ (),Applicative m) => Monoid (HtmlT m a) where
mempty = pure mempty
mappend = liftA2 mappend
instance Applicative m => Applicative (HtmlT m) where
pure a = HtmlT (pure (mempty,a))
{-# INLINE pure #-}
f <*> x = HtmlT $ mk <$> runHtmlT f <*> runHtmlT x
where mk ~(g, f') ~(h, x') = (g <> h, f' x')
{-# INLINE (<*>) #-}
m *> n = HtmlT $ mk <$> runHtmlT m <*> runHtmlT n
where mk ~(g, _) ~(h, b) = (g <> h, b)
{-# INLINE (*>) #-}
m <* n = HtmlT $ mk <$> runHtmlT m <*> runHtmlT n
where mk ~(g, a) ~(h, _) = (g <> h, a)
{-# INLINE (<*) #-}
instance Functor m => Functor (HtmlT m) where
fmap f = HtmlT . fmap (fmap f) . runHtmlT
(<$) = fmap . const
{-# INLINE (<$) #-}
instance Monad m => Monad (HtmlT m) where
return a = HtmlT (return (mempty,a))
{-# INLINE return #-}
m >>= f = HtmlT $ do
~(g,a) <- runHtmlT m
~(h,b) <- runHtmlT (f a)
return (g <> h,b)
{-# INLINE (>>=) #-}
m >> n = HtmlT $ do
~(g, _) <- runHtmlT m
~(h, b) <- runHtmlT n
return (g <> h, b)
{-# INLINE (>>) #-}
instance MonadTrans HtmlT where
lift m =
HtmlT (do a <- m
return (\_ -> mempty,a))
instance MonadFix m => MonadFix (HtmlT m) where
mfix m = HtmlT $ mfix $ \ ~(_, a) -> runHtmlT $ m a
instance MonadReader r m => MonadReader r (HtmlT m) where
ask = lift ask
local f (HtmlT a) = HtmlT (local f a)
instance MonadState s m => MonadState s (HtmlT m) where
get = lift get
put = lift . put
state = lift . state
instance MonadError e m => MonadError e (HtmlT m) where
throwError = lift . throwError
catchError (HtmlT m) h = HtmlT $ catchError m (runHtmlT . h)
instance MonadWriter w m => MonadWriter w (HtmlT m) where
tell = lift . tell
listen (HtmlT x) = HtmlT $ fmap reassoc $ listen x
where reassoc ((a, b), c) = (a, (b, c))
pass (HtmlT p) = HtmlT $ pass $ fmap assoc p
where assoc (a, (b, c)) = ((a, b), c)
instance MonadIO m => MonadIO (HtmlT m) where
liftIO = lift . liftIO
instance (Monad m,a ~ ()) => IsString (HtmlT m a) where
fromString = toHtml
instance (m ~ Identity) => Show (HtmlT m a) where
show = LT.unpack . renderText
class ToHtml a where
toHtml :: Monad m => a -> HtmlT m ()
toHtmlRaw :: Monad m => a -> HtmlT m ()
instance (a ~ (), m ~ Identity) => ToHtml (HtmlT m a) where
toHtml = relaxHtmlT
toHtmlRaw = relaxHtmlT
instance ToHtml String where
toHtml = build . Blaze.fromHtmlEscapedString
toHtmlRaw = build . Blaze.fromString
instance ToHtml Text where
toHtml = build . Blaze.fromHtmlEscapedText
toHtmlRaw = build . Blaze.fromText
instance ToHtml LT.Text where
toHtml = build . Blaze.fromHtmlEscapedLazyText
toHtmlRaw = build . Blaze.fromLazyText
instance ToHtml S.ByteString where
toHtml = build . Blaze.fromHtmlEscapedText . T.decodeUtf8
toHtmlRaw = build . Blaze.fromByteString
instance ToHtml L.ByteString where
toHtml = build . Blaze.fromHtmlEscapedLazyText . LT.decodeUtf8
toHtmlRaw = build . Blaze.fromLazyByteString
build :: Monad m => Builder -> HtmlT m ()
build b = HtmlT (return (const b,()))
{-# INLINE build #-}
class Term arg result | result -> arg where
term :: Text
-> arg
-> result
term = flip termWith []
{-# INLINE term #-}
termWith :: Text
-> [Attribute]
-> arg
-> result
instance (Applicative m,f ~ HtmlT m a) => Term [Attribute] (f -> HtmlT m a) where
termWith name f = with (makeElement name) . (<> f)
instance (Applicative m) => Term (HtmlT m a) (HtmlT m a) where
termWith name f = with (makeElement name) f
{-# INLINE termWith #-}
instance Term Text Attribute where
termWith key _ value = makeAttribute key value
class TermRaw arg result | result -> arg where
termRaw :: Text
-> arg
-> result
termRaw = flip termRawWith []
termRawWith :: Text
-> [Attribute]
-> arg
-> result
instance (Monad m,ToHtml f, a ~ ()) => TermRaw [Attribute] (f -> HtmlT m a) where
termRawWith name f attrs = with (makeElement name) (attrs <> f) . toHtmlRaw
instance (Monad m,a ~ ()) => TermRaw Text (HtmlT m a) where
termRawWith name f = with (makeElement name) f . toHtmlRaw
instance TermRaw Text Attribute where
termRawWith key _ value = makeAttribute key value
class With a where
with :: a
-> [Attribute]
-> a
instance (Functor m) => With (HtmlT m a) where
with f = \attr -> HtmlT (mk attr <$> runHtmlT f)
where
mk attr ~(f',a) = (\attr' -> f' (unionArgs (M.fromListWith (<>) (map toPair attr)) attr')
,a)
toPair (Attribute x y) = (x,y)
instance (Functor m) => With (HtmlT m a -> HtmlT m a) where
with f = \attr inner -> HtmlT (mk attr <$> runHtmlT (f inner))
where
mk attr ~(f',a) = (\attr' -> f' (unionArgs (M.fromListWith (<>) (map toPair attr)) attr')
,a)
toPair (Attribute x y) = (x,y)
unionArgs :: HashMap Text Text -> HashMap Text Text -> HashMap Text Text
unionArgs = M.unionWith (<>)
renderToFile :: FilePath -> Html a -> IO ()
renderToFile fp = L.writeFile fp . Blaze.toLazyByteString . runIdentity . execHtmlT
renderBS :: Html a -> ByteString
renderBS = Blaze.toLazyByteString . runIdentity . execHtmlT
renderText :: Html a -> LT.Text
renderText = LT.decodeUtf8 . Blaze.toLazyByteString . runIdentity . execHtmlT
renderBST :: Monad m => HtmlT m a -> m ByteString
renderBST = fmap Blaze.toLazyByteString . execHtmlT
renderTextT :: Monad m => HtmlT m a -> m LT.Text
renderTextT = fmap (LT.decodeUtf8 . Blaze.toLazyByteString) . execHtmlT
execHtmlT :: Monad m
=> HtmlT m a
-> m Builder
execHtmlT m =
do (f,_) <- runHtmlT m
return (f mempty)
relaxHtmlT :: Monad m
=> HtmlT Identity a
-> HtmlT m a
relaxHtmlT = hoist go
where
go :: Monad m => Identity a -> m a
go = return . runIdentity
commuteHtmlT :: (Functor m, Monad n)
=> HtmlT m a
-> m (HtmlT n a)
commuteHtmlT (HtmlT xs) = fmap (HtmlT . return) xs
evalHtmlT :: Monad m
=> HtmlT m a
-> m a
evalHtmlT m =
do (_,a) <- runHtmlT m
return a
makeAttribute :: Text
-> Text
-> Attribute
makeAttribute x y = Attribute x y
makeElement :: Functor m
=> Text
-> HtmlT m a
-> HtmlT m a
{-# INLINE[1] makeElement #-}
makeElement name = \m' -> HtmlT (mk <$> runHtmlT m')
where
mk ~(f,a) =
(\attr ->
s "<" <> Blaze.fromText name
<> foldlMapWithKey buildAttr attr <> s ">"
<> f mempty
<> s "</" <> Blaze.fromText name <> s ">"
,a)
makeElementNoEnd :: Applicative m
=> Text
-> HtmlT m ()
makeElementNoEnd name =
HtmlT (pure (\attr -> s "<" <> Blaze.fromText name
<> foldlMapWithKey buildAttr attr <> s ">",
()))
makeXmlElementNoEnd :: Applicative m
=> Text
-> HtmlT m ()
makeXmlElementNoEnd name =
HtmlT (pure (\attr -> s "<" <> Blaze.fromText name
<> foldlMapWithKey buildAttr attr <> s "/>",
()))
buildAttr :: Text -> Text -> Builder
buildAttr key val =
s " " <>
Blaze.fromText key <>
if val == mempty
then mempty
else s "=\"" <> Blaze.fromHtmlEscapedText val <> s "\""
foldlMapWithKey :: Monoid m => (k -> v -> m) -> HashMap k v -> m
foldlMapWithKey f = M.foldlWithKey' (\m k v -> m `mappend` f k v) mempty
s :: String -> Builder
s = Blaze.fromString
{-# INLINE s #-}