{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE EmptyDataDecls #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PolyKinds #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
module Reflex.Dom.Builder.Static where
import Data.IORef (IORef)
import Blaze.ByteString.Builder.Html.Utf8
import Control.Lens hiding (element)
import Control.Monad.Exception
import Control.Monad.Identity
import Control.Monad.Primitive
import Control.Monad.Ref
import Control.Monad.State.Strict
import Control.Monad.Trans.Reader
import Data.ByteString (ByteString)
import Data.ByteString.Builder (Builder, byteString, toLazyByteString)
import qualified Data.ByteString.Lazy as LBS
import Data.Default
import Data.Dependent.Map (DMap)
import qualified Data.Dependent.Map as DMap
import Data.Dependent.Sum (DSum (..))
import Data.Functor.Compose
import Data.Functor.Constant
import Data.IntMap (IntMap)
import qualified Data.IntMap as IntMap
import qualified Data.Map as Map
import Data.Map.Misc (applyMap)
import Data.Monoid ((<>))
import qualified Data.Set as Set
import Data.Text (Text)
import qualified Data.Text as T
import Data.Text.Encoding
import Data.Tuple
import GHC.Generics
import Reflex.Adjustable.Class
import Reflex.Class
import Reflex.Dom.Main (DomHost, DomTimeline, runDomHost)
import Reflex.Dom.Builder.Class
import Reflex.Dynamic
import Reflex.Host.Class
import Reflex.PerformEvent.Base
import Reflex.PerformEvent.Class
import Reflex.PostBuild.Base
import Reflex.PostBuild.Class
import Reflex.TriggerEvent.Class
import System.Random (randomRIO)
data StaticDomBuilderEnv t = StaticDomBuilderEnv
{ _staticDomBuilderEnv_shouldEscape :: Bool
, _staticDomBuilderEnv_selectValue :: Maybe (Behavior t Text)
}
newtype StaticDomBuilderT t m a = StaticDomBuilderT
{ unStaticDomBuilderT :: ReaderT (StaticDomBuilderEnv t) (StateT [Behavior t Builder] m) a
}
deriving (Functor, Applicative, Monad, MonadFix, MonadIO, MonadException, MonadAsyncException)
instance PrimMonad m => PrimMonad (StaticDomBuilderT x m) where
type PrimState (StaticDomBuilderT x m) = PrimState m
primitive = lift . primitive
instance MonadTrans (StaticDomBuilderT t) where
lift = StaticDomBuilderT . lift . lift
runStaticDomBuilderT :: (Monad m, Reflex t) => StaticDomBuilderT t m a -> StaticDomBuilderEnv t -> m (a, Behavior t Builder)
runStaticDomBuilderT (StaticDomBuilderT a) e = do
(result, a') <- runStateT (runReaderT a e) []
return (result, mconcat $ reverse a')
instance PostBuild t m => PostBuild t (StaticDomBuilderT t m) where
{-# INLINABLE getPostBuild #-}
getPostBuild = lift getPostBuild
instance MonadReflexCreateTrigger t m => MonadReflexCreateTrigger t (StaticDomBuilderT t m) where
{-# INLINABLE newEventWithTrigger #-}
newEventWithTrigger = lift . newEventWithTrigger
{-# INLINABLE newFanEventWithTrigger #-}
newFanEventWithTrigger f = lift $ newFanEventWithTrigger f
instance PerformEvent t m => PerformEvent t (StaticDomBuilderT t m) where
type Performable (StaticDomBuilderT t m) = Performable m
{-# INLINABLE performEvent_ #-}
performEvent_ e = lift $ performEvent_ e
{-# INLINABLE performEvent #-}
performEvent e = lift $ performEvent e
instance MonadSample t m => MonadSample t (StaticDomBuilderT t m) where
{-# INLINABLE sample #-}
sample = lift . sample
instance MonadHold t m => MonadHold t (StaticDomBuilderT t m) where
{-# INLINABLE hold #-}
hold v0 v' = lift $ hold v0 v'
{-# INLINABLE holdDyn #-}
holdDyn v0 v' = lift $ holdDyn v0 v'
{-# INLINABLE holdIncremental #-}
holdIncremental v0 v' = lift $ holdIncremental v0 v'
{-# INLINABLE buildDynamic #-}
buildDynamic a0 = lift . buildDynamic a0
{-# INLINABLE headE #-}
headE = lift . headE
instance (Monad m, Ref m ~ Ref IO, Reflex t) => TriggerEvent t (StaticDomBuilderT t m) where
{-# INLINABLE newTriggerEvent #-}
newTriggerEvent = return (never, const $ return ())
{-# INLINABLE newTriggerEventWithOnComplete #-}
newTriggerEventWithOnComplete = return (never, \_ _ -> return ())
{-# INLINABLE newEventWithLazyTriggerWithOnComplete #-}
newEventWithLazyTriggerWithOnComplete _ = return never
instance MonadRef m => MonadRef (StaticDomBuilderT t m) where
type Ref (StaticDomBuilderT t m) = Ref m
newRef = lift . newRef
readRef = lift . readRef
writeRef r = lift . writeRef r
instance MonadAtomicRef m => MonadAtomicRef (StaticDomBuilderT t m) where
atomicModifyRef r = lift . atomicModifyRef r
type SupportsStaticDomBuilder t m = (Reflex t, MonadIO m, MonadHold t m, MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadRef m, Ref m ~ Ref IO, Adjustable t m)
data StaticDomSpace
data StaticDomEvent (a :: k)
data StaticDomHandler (a :: k) (b :: k) = StaticDomHandler
data StaticEventSpec (er :: EventTag -> *) = StaticEventSpec deriving (Generic)
instance Default (StaticEventSpec er)
instance DomSpace StaticDomSpace where
type EventSpec StaticDomSpace = StaticEventSpec
type RawDocument StaticDomSpace = ()
type RawTextNode StaticDomSpace = ()
type RawCommentNode StaticDomSpace = ()
type RawElement StaticDomSpace = ()
type RawInputElement StaticDomSpace = ()
type RawTextAreaElement StaticDomSpace = ()
type RawSelectElement StaticDomSpace = ()
addEventSpecFlags _ _ _ _ = StaticEventSpec
instance (SupportsStaticDomBuilder t m, Monad m) => HasDocument (StaticDomBuilderT t m) where
askDocument = pure ()
instance (Reflex t, Adjustable t m, MonadHold t m, SupportsStaticDomBuilder t m) => Adjustable t (StaticDomBuilderT t m) where
runWithReplace a0 a' = do
e <- StaticDomBuilderT ask
key <- replaceStart
(result0, result') <- lift $ runWithReplace (runStaticDomBuilderT a0 e) (flip runStaticDomBuilderT e <$> a')
o <- hold (snd result0) $ fmapCheap snd result'
StaticDomBuilderT $ modify $ (:) $ join o
replaceEnd key
return (fst result0, fmapCheap fst result')
traverseIntMapWithKeyWithAdjust = hoistIntMapWithKeyWithAdjust traverseIntMapWithKeyWithAdjust
traverseDMapWithKeyWithAdjust = hoistDMapWithKeyWithAdjust traverseDMapWithKeyWithAdjust mapPatchDMap
traverseDMapWithKeyWithAdjustWithMove = hoistDMapWithKeyWithAdjust traverseDMapWithKeyWithAdjustWithMove mapPatchDMapWithMove
replaceStart :: (DomBuilder t m, MonadIO m) => m Text
replaceStart = do
str <- liftIO $ replicateM 8 $ randomRIO ('a', 'z')
let key = "-" <> T.pack str
_ <- commentNode $ def { _commentNodeConfig_initialContents = "replace-start" <> key }
pure key
replaceEnd :: DomBuilder t m => Text -> m ()
replaceEnd key = void $ commentNode $ def { _commentNodeConfig_initialContents = "replace-end" <> key }
hoistIntMapWithKeyWithAdjust :: forall t m p a b.
( Adjustable t m
, MonadHold t m
, Patch (p a)
, Functor p
, Patch (p (Behavior t Builder))
, PatchTarget (p (Behavior t Builder)) ~ IntMap (Behavior t Builder)
, Ref m ~ IORef, MonadIO m, MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadRef m
)
=> (forall x. (IntMap.Key -> a -> m x)
-> IntMap a
-> Event t (p a)
-> m (IntMap x, Event t (p x))
)
-> (IntMap.Key -> a -> StaticDomBuilderT t m b)
-> IntMap a
-> Event t (p a)
-> StaticDomBuilderT t m (IntMap b, Event t (p b))
hoistIntMapWithKeyWithAdjust base f im0 im' = do
e <- StaticDomBuilderT ask
(children0, children') <- lift $ base (\k v -> runStaticDomBuilderT (f k v) e) im0 im'
let result0 = IntMap.map fst children0
result' = (fmap . fmap) fst children'
outputs0 :: IntMap (Behavior t Builder)
outputs0 = IntMap.map snd children0
outputs' :: Event t (p (Behavior t Builder))
outputs' = (fmap . fmap) snd children'
outputs <- holdIncremental outputs0 outputs'
StaticDomBuilderT $ modify $ (:) $ pull $ do
os <- sample $ currentIncremental outputs
fmap mconcat $ forM (IntMap.toList os) $ \(_, o) -> do
sample o
return (result0, result')
hoistDMapWithKeyWithAdjust :: forall (k :: * -> *) v v' t m p.
( Adjustable t m
, MonadHold t m
, PatchTarget (p k (Constant (Behavior t Builder))) ~ DMap k (Constant (Behavior t Builder))
, Patch (p k (Constant (Behavior t Builder)))
, Ref m ~ IORef, MonadIO m, MonadFix m, PerformEvent t m, MonadReflexCreateTrigger t m, MonadRef m
)
=> (forall vv vv'.
(forall a. k a -> vv a -> m (vv' a))
-> DMap k vv
-> Event t (p k vv)
-> m (DMap k vv', Event t (p k vv'))
)
-> (forall vv vv'. (forall a. vv a -> vv' a) -> p k vv -> p k vv')
-> (forall a. k a -> v a -> StaticDomBuilderT t m (v' a))
-> DMap k v
-> Event t (p k v)
-> StaticDomBuilderT t m (DMap k v', Event t (p k v'))
hoistDMapWithKeyWithAdjust base mapPatch f dm0 dm' = do
e <- StaticDomBuilderT ask
(children0, children') <- lift $ base (\k v -> fmap (Compose . swap) (runStaticDomBuilderT (f k v) e)) dm0 dm'
let result0 = DMap.map (snd . getCompose) children0
result' = ffor children' $ mapPatch $ snd . getCompose
outputs0 :: DMap k (Constant (Behavior t Builder))
outputs0 = DMap.map (Constant . fst . getCompose) children0
outputs' :: Event t (p k (Constant (Behavior t Builder)))
outputs' = ffor children' $ mapPatch $ Constant . fst . getCompose
outputs <- holdIncremental outputs0 outputs'
StaticDomBuilderT $ modify $ (:) $ pull $ do
os <- sample $ currentIncremental outputs
fmap mconcat $ forM (DMap.toList os) $ \(_ :=> Constant o) -> do
sample o
return (result0, result')
instance SupportsStaticDomBuilder t m => NotReady t (StaticDomBuilderT t m) where
notReadyUntil _ = pure ()
notReady = pure ()
instance SupportsStaticDomBuilder t m => DomBuilder t (StaticDomBuilderT t m) where
type DomBuilderSpace (StaticDomBuilderT t m) = StaticDomSpace
{-# INLINABLE textNode #-}
textNode (TextNodeConfig initialContents mSetContents) = StaticDomBuilderT $ do
shouldEscape <- asks _staticDomBuilderEnv_shouldEscape
let escape = if shouldEscape then fromHtmlEscapedText else byteString . encodeUtf8
modify . (:) =<< case mSetContents of
Nothing -> return (pure (escape initialContents))
Just setContents -> hold (escape initialContents) $ fmapCheap escape setContents
return $ TextNode ()
{-# INLINABLE commentNode #-}
commentNode (CommentNodeConfig initialContents mSetContents) = StaticDomBuilderT $ do
shouldEscape <- asks _staticDomBuilderEnv_shouldEscape
let escape = if shouldEscape then fromHtmlEscapedText else byteString . encodeUtf8
modify . (:) =<< (\c -> "<!--" <> c <> "-->") <$> case mSetContents of
Nothing -> return (pure (escape initialContents))
Just setContents -> hold (escape initialContents) $ fmapCheap escape setContents
return $ CommentNode ()
{-# INLINABLE element #-}
element elementTag cfg child = do
let voidElements = Set.fromList ["area", "base", "br", "col", "command", "embed", "hr", "img", "input", "keygen", "link", "meta", "param", "source", "track", "wbr"]
let noEscapeElements = Set.fromList ["style", "script"]
let toAttr (AttributeName _mns k) v = byteString (encodeUtf8 k) <> byteString "=\"" <> fromHtmlEscapedText v <> byteString "\""
es <- newFanEventWithTrigger $ \_ _ -> return (return ())
StaticDomBuilderT $ do
let shouldEscape = elementTag `Set.notMember` noEscapeElements
(result, innerHtml) <- lift $ lift $ runStaticDomBuilderT child $ StaticDomBuilderEnv shouldEscape Nothing
attrs0 <- foldDyn applyMap (cfg ^. initialAttributes) (cfg ^. modifyAttributes)
selectValue <- asks _staticDomBuilderEnv_selectValue
let addSelectedAttr attrs sel = case Map.lookup "value" attrs of
Just v | v == sel -> attrs <> Map.singleton "selected" ""
_ -> Map.delete "selected" attrs
let attrs1 = case (elementTag, selectValue) of
("option", Just sv) -> pull $ addSelectedAttr <$> sample (current attrs0) <*> sample sv
_ -> current attrs0
let attrs2 = ffor attrs1 $ mconcat . fmap (\(k, v) -> " " <> toAttr k v) . Map.toList
let tagBS = encodeUtf8 elementTag
if Set.member elementTag voidElements
then modify $ (:) $ mconcat [constant ("<" <> byteString tagBS), attrs2, constant (byteString " />")]
else do
let open = mconcat [constant ("<" <> byteString tagBS), attrs2, constant (byteString ">")]
let close = constant $ byteString $ "</" <> tagBS <> ">"
modify $ (:) $ mconcat [open, innerHtml, close]
let e = Element
{ _element_events = es
, _element_raw = ()
}
return (e, result)
{-# INLINABLE inputElement #-}
inputElement cfg = do
(e, _result) <- element "input" (cfg ^. inputElementConfig_elementConfig) $ return ()
let v0 = constDyn $ cfg ^. inputElementConfig_initialValue
let c0 = constDyn $ cfg ^. inputElementConfig_initialChecked
let hasFocus = constDyn False
return $ InputElement
{ _inputElement_value = v0
, _inputElement_checked = c0
, _inputElement_checkedChange = never
, _inputElement_input = never
, _inputElement_hasFocus = hasFocus
, _inputElement_element = e
, _inputElement_raw = ()
, _inputElement_files = constDyn mempty
}
{-# INLINABLE textAreaElement #-}
textAreaElement cfg = do
(e, _domElement) <- element "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return ()
let v0 = constDyn $ cfg ^. textAreaElementConfig_initialValue
let hasFocus = constDyn False
return $ TextAreaElement
{ _textAreaElement_value = v0
, _textAreaElement_input = never
, _textAreaElement_hasFocus = hasFocus
, _textAreaElement_element = e
, _textAreaElement_raw = ()
}
selectElement cfg child = do
v <- holdDyn (cfg ^. selectElementConfig_initialValue) (cfg ^. selectElementConfig_setValue)
(e, result) <- element "select" (_selectElementConfig_elementConfig cfg) $ do
(a, innerHtml) <- StaticDomBuilderT $ lift $ lift $ runStaticDomBuilderT child $ StaticDomBuilderEnv False $ Just (current v)
StaticDomBuilderT $ lift $ modify $ (:) innerHtml
return a
let wrapped = SelectElement
{ _selectElement_value = v
, _selectElement_change = never
, _selectElement_hasFocus = constDyn False
, _selectElement_element = e
, _selectElement_raw = ()
}
return (wrapped, result)
placeRawElement () = return ()
wrapRawElement () _ = return $ Element (EventSelector $ const never) ()
type StaticWidget x = PostBuildT DomTimeline (StaticDomBuilderT DomTimeline (PerformEventT DomTimeline DomHost))
{-# INLINE renderStatic #-}
renderStatic :: StaticWidget x a -> IO (a, ByteString)
renderStatic w = do
runDomHost $ do
(postBuild, postBuildTriggerRef) <- newEventWithTriggerRef
let env0 = StaticDomBuilderEnv True Nothing
((res, bs), FireCommand fire) <- hostPerformEventT $ runStaticDomBuilderT (runPostBuildT w postBuild) env0
mPostBuildTrigger <- readRef postBuildTriggerRef
forM_ mPostBuildTrigger $ \postBuildTrigger -> fire [postBuildTrigger :=> Identity ()] $ return ()
bs' <- sample bs
return (res, LBS.toStrict $ toLazyByteString bs')