{-# 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) -- ^ When the parent element is a "select" whose value has been set, this value tells us the current value. -- We use this to add a "selected" attribute to the appropriate "option" child element. -- This is not yet a perfect simulation of what the browser does, but it is much closer than doing nothing. -- TODO: Handle edge cases, e.g. setting to a value for which there is no option, then adding that option dynamically afterwards. } newtype StaticDomBuilderT t m a = StaticDomBuilderT { unStaticDomBuilderT :: ReaderT (StaticDomBuilderEnv t) (StateT [Behavior t Builder] m) a -- Accumulated Html will be in reversed order } 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 -- | Static documents never produce any events, so this type has no inhabitants data StaticDomEvent (a :: k) -- | Static documents don't process events, so all handlers are equivalent 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 -- TODO remove this? 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 -- TODO remove ) => (forall x. (IntMap.Key -> a -> m x) -> IntMap a -> Event t (p a) -> m (IntMap x, Event t (p x)) ) -- ^ The base monad's traversal -> (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 -- TODO remove ) => (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')) ) -- ^ The base monad's traversal -> (forall vv vv'. (forall a. vv a -> vv' a) -> p k vv -> p k vv') -- ^ A way of mapping over the patch type -> (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 () -- TODO: the uses of illegal lenses in this instance causes it to be somewhat less efficient than it can be. replacing them with explicit cases to get the underlying Maybe Event and working with those is ideal. instance SupportsStaticDomBuilder t m => DomBuilder t (StaticDomBuilderT t m) where type DomBuilderSpace (StaticDomBuilderT t m) = StaticDomSpace {-# INLINABLE textNode #-} textNode (TextNodeConfig initialContents mSetContents) = StaticDomBuilderT $ do --TODO: Do not escape quotation marks; see https://stackoverflow.com/questions/25612166/what-characters-must-be-escaped-in-html-5 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 --Only because it doesn't get optimized when profiling is on return $ TextNode () {-# INLINABLE commentNode #-} commentNode (CommentNodeConfig initialContents mSetContents) = StaticDomBuilderT $ do --TODO: Do not escape quotation marks; see https://stackoverflow.com/questions/25612166/what-characters-must-be-escaped-in-html-5 shouldEscape <- asks _staticDomBuilderEnv_shouldEscape let escape = if shouldEscape then fromHtmlEscapedText else byteString . encodeUtf8 modify . (:) =<< (\c -> "") <$> case mSetContents of Nothing -> return (pure (escape initialContents)) Just setContents -> hold (escape initialContents) $ fmapCheap escape setContents --Only because it doesn't get optimized when profiling is on return $ CommentNode () {-# INLINABLE element #-} element elementTag cfg child = do -- https://www.w3.org/TR/html-markup/syntax.html#syntax-elements 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 (Map.insert "data-ssr" "" $ 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 -- TODO should this be coming from initialAtttributes 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 --TODO: Support setValue event (e, _domElement) <- element "textarea" (cfg ^. textAreaElementConfig_elementConfig) $ return () let v0 = constDyn $ cfg ^. textAreaElementConfig_initialValue let hasFocus = constDyn False -- TODO should this be coming from initialAtttributes 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 --TODO: How do we make sure this is correct? , _selectElement_element = e , _selectElement_raw = () } return (wrapped, result) placeRawElement () = return () wrapRawElement () _ = return $ Element (EventSelector $ const never) () --TODO: Make this more abstract --TODO: Put the WithWebView underneath PerformEventT - I think this would perform better 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')