{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FunctionalDependencies #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE InstanceSigs #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE PartialTypeSignatures #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeFamilies #-}
{-# LANGUAGE TypeOperators #-}
{-# LANGUAGE UndecidableInstances #-}
{-# OPTIONS_GHC -Wno-unused-imports #-}
module Shpadoinkle.Core (
Html(..), Prop(..), Props(..), fromProps, toProps
, dataProp, flagProp, textProp, listenerProp, bakedProp
, listenRaw, listen, listenM, listenM_, listenC, listener
, h, baked, text
, hoistHtml, hoistProp
, cataH, cataProp
, mapProps, injectProps, eitherH, htmlDecode
, RawNode(..), RawEvent(..)
, Backend (..)
, type (~>)
, shpadoinkle
, JSM, MonadJSM, askJSM, runJSM, MonadUnliftIO(..), UnliftIO(..), liftJSM
, module UnliftIO.STM
) where
import Control.Applicative (liftA2)
import Control.Category ((.))
import Data.Kind (Type)
import Data.Map as M (Map, foldl', insert,
mapEither, singleton,
toList, unionWithKey)
import Data.String (IsString (..))
import Data.Text (Text, pack)
import Data.Text.Lazy (toStrict)
import Data.Text.Lazy.Builder (toLazyText)
import GHCJS.DOM.Types (JSM, MonadJSM, liftJSM)
#ifndef ghcjs_HOST_OS
import Language.Javascript.JSaddle (FromJSVal (..), JSVal,
ToJSVal (..), JSString, askJSM, runJSM, fromJSString, toJSString)
#else
import Language.Javascript.JSaddle (FromJSVal (..), JSVal,
ToJSVal (..), JSString, askJSM, runJSM)
#endif
import Prelude hiding ((.))
import UnliftIO (MonadUnliftIO (..),
UnliftIO (..))
import UnliftIO.STM (STM, TVar, atomically,
modifyTVar, newTVarIO, readTVar,
readTVarIO, retrySTM, writeTVar)
import Shpadoinkle.Continuation (Continuation, Continuous (..),
causes, eitherC, hoist, impur,
pur, shouldUpdate)
#ifndef ghcjs_HOST_OS
import HTMLEntities.Decoder (htmlEncodedText)
#endif
newtype Html m a = Html
{ unHtml
:: forall r. (Text -> [(Text, Prop m a)] -> [r] -> r)
-> (JSM (RawNode, STM (Continuation m a)) -> r)
-> (Text -> r)
-> r
}
data Prop :: (Type -> Type) -> Type -> Type where
PData :: JSVal -> Prop m a
PText :: Text -> Prop m a
PFlag :: Bool -> Prop m a
PPotato :: (RawNode -> JSM (STM (Continuation m a))) -> Prop m a
PListener :: (RawNode -> RawEvent -> JSM (Continuation m a)) -> Prop m a
instance Eq (Prop m a) where
x == y = case (x,y) of
(PText x', PText y') -> x' == y'
(PFlag x', PFlag y') -> x' == y'
_ -> False
listenM :: Applicative m => Text -> m (a -> a) -> (Text, Prop m a)
listenM k = listenC k . impur
listenM_ :: Applicative m => Text -> m () -> (Text, Prop m a)
listenM_ k = listenC k . causes
newtype Props m a = Props { getProps :: Map Text (Prop m a) }
{-# SPECIALIZE toProps :: [(Text, Prop JSM a)] -> Props JSM a #-}
toProps :: Applicative m => [(Text, Prop m a)] -> Props m a
toProps = foldMap $ Props . uncurry singleton
fromProps :: Props m a -> [(Text, Prop m a)]
fromProps = M.toList . getProps
instance Applicative m => Semigroup (Props m a) where
Props xs <> Props ys = Props $ unionWithKey go xs ys
where
go k old new = case (old, new) of
(PText t, PText t') | k == "className" -> PText (t <> " " <> t')
(PText t, PText t') | k == "style" -> PText (t <> "; " <> t')
(PListener l, PListener l') -> PListener $
\raw evt -> mappend <$> l raw evt <*> l' raw evt
_ -> new
instance Applicative m => Monoid (Props m a) where
mempty = Props mempty
hoistHtml :: Functor m => (m ~> n) -> Html m a -> Html n a
hoistHtml f (Html h') = Html $ \n p t -> h'
(\t' ps cs -> n t' (fmap (hoistProp f) <$> ps) cs) (p . fmap (fmap (fmap (hoist f)))) t
{-# INLINE hoistHtml #-}
hoistProp :: Functor m => (m ~> n) -> Prop m a -> Prop n a
hoistProp f = \case
PListener g -> PListener $ \x -> fmap (hoist f) . g x
PData t -> PData t
PText t -> PText t
PFlag t -> PFlag t
PPotato p -> PPotato $ fmap (fmap (hoist f)) . p
{-# INLINE hoistProp #-}
instance IsString (Html m a) where
fromString = text . pack
{-# INLINE fromString #-}
instance IsString (Prop m a) where
fromString = PText . pack
{-# INLINE fromString #-}
instance Continuous Html where
mapC f (Html h') = Html $ \n p t -> h' (\t' ps cs -> n t' (fmap (mapC f) <$> ps) cs)
(p . fmap (fmap (fmap (mapC f)))) t
{-# INLINE mapC #-}
instance Continuous Props where
mapC f = Props . fmap (mapC f) . getProps
{-# INLINE mapC #-}
instance Continuous Prop where
mapC _ (PData t) = PData t
mapC _ (PText t) = PText t
mapC _ (PFlag b) = PFlag b
mapC f (PListener g) = PListener $ \r -> fmap f . g r
mapC f (PPotato b) = PPotato $ fmap (fmap f) . b
{-# INLINE mapC #-}
dataProp :: JSVal -> Prop m a
dataProp = PData
{-# INLINE dataProp #-}
textProp :: Text -> Prop m a
textProp = PText
{-# INLINE textProp #-}
flagProp :: Bool -> Prop m a
flagProp = PFlag
{-# INLINE flagProp #-}
listenerProp :: (RawNode -> RawEvent -> JSM (Continuation m a)) -> Prop m a
listenerProp = PListener
{-# INLINE listenerProp #-}
bakedProp :: (RawNode -> JSM (STM (Continuation m a))) -> Prop m a
bakedProp = PPotato
{-# INLINE bakedProp #-}
cataProp
:: (JSVal -> b)
-> (Text -> b)
-> (Bool -> b)
-> ((RawNode -> RawEvent -> JSM (Continuation m a)) -> b)
-> ((RawNode -> JSM (STM (Continuation m a))) -> b)
-> Prop m a
-> b
cataProp d t f l p = \case
PData x -> d x
PText x -> t x
PFlag x -> f x
PListener x -> l x
PPotato x -> p x
h :: Text -> [(Text, Prop m a)] -> [Html m a] -> Html m a
h t ps cs = Html $ \a b c -> a t ps ((\(Html h') -> h' a b c) <$> cs)
{-# INLINE h #-}
baked :: JSM (RawNode, STM (Continuation m a)) -> Html m a
baked jr = Html $ \_ p _ -> p jr
{-# INLINE baked #-}
text :: Text -> Html m a
text t = Html $ \_ _ f -> f t
{-# INLINE text #-}
eitherH :: Applicative m => (a -> Html m a) -> (b -> Html m b) -> Either a b -> Html m (Either a b)
eitherH = eitherC
{-# INLINE eitherH #-}
cataH :: (Text -> [(Text, Prop m a)] -> [b] -> b)
-> (JSM (RawNode, STM (Continuation m a)) -> b)
-> (Text -> b)
-> Html m a -> b
cataH f g h' (Html h'') = h'' f g h'
type m ~> n = forall a. m a -> n a
newtype RawNode = RawNode { unRawNode :: JSVal }
instance ToJSVal RawNode where toJSVal = return . unRawNode
instance FromJSVal RawNode where fromJSVal = return . Just . RawNode
newtype RawEvent = RawEvent { unRawEvent :: JSVal }
instance ToJSVal RawEvent where toJSVal = return . unRawEvent
instance FromJSVal RawEvent where fromJSVal = return . Just . RawEvent
instance {-# OVERLAPPING #-} IsString [(Text, Prop m a)] where
fromString = pure . ("className", ) . textProp . pack
{-# INLINE fromString #-}
listener :: Continuation m a -> Prop m a
listener = listenerProp . const . const . return
{-# INLINE listener #-}
listenRaw :: Text -> (RawNode -> RawEvent -> JSM (Continuation m a)) -> (Text, Prop m a)
listenRaw k = (,) k . listenerProp
{-# INLINE listenRaw #-}
listenC :: Text -> Continuation m a -> (Text, Prop m a)
listenC k = listenRaw k . const . const . return
{-# INLINE listenC #-}
listen :: Text -> (a -> a) -> (Text, Prop m a)
listen k = listenC k . pur
{-# INLINE listen #-}
mapProps :: ([(Text, Prop m a)] -> [(Text, Prop m a)]) -> Html m a -> Html m a
mapProps f (Html h') = Html $ \n p t -> h' (\t' ps cs -> n t' (f ps) cs) p t
{-# INLINE mapProps #-}
injectProps :: [(Text, Prop m a)] -> Html m a -> Html m a
injectProps ps = mapProps (<> ps)
{-# INLINE injectProps #-}
#ifndef ghcjs_HOST_OS
htmlDecode :: JSString -> JSM JSString
htmlDecode = pure . toJSString . toStrict . toLazyText . htmlEncodedText . fromJSString
#else
foreign import javascript unsafe
"{var ta = document.createElement('textarea'); ta.innerHTML = $1; $r = ta.childNodes.length == 0 ? '' : ta.childNodes[0].nodeValue;}"
htmlDecode :: JSString -> JSM JSString
#endif
class Backend b m a | b m -> a where
type VNode b m
interpret
:: (m ~> JSM)
-> Html (b m) a
-> b m (VNode b m)
patch
:: RawNode
-> Maybe (VNode b m)
-> VNode b m
-> b m (VNode b m)
setup :: JSM () -> JSM ()
shpadoinkle
:: forall b m a
. Backend b m a => Monad (b m) => Eq a
=> (m ~> JSM)
-> (TVar a -> b m ~> m)
-> TVar a
-> (a -> Html (b m) a)
-> b m RawNode
-> JSM ()
shpadoinkle toJSM toM model view stage = setup @b @m @a $ do
c <- j stage
initial <- readTVarIO model
n <- go c Nothing initial
() <$ shouldUpdate (go c . Just) n model
where
j :: b m ~> JSM
j = toJSM . toM model
go :: RawNode -> Maybe (VNode b m) -> a -> JSM (VNode b m)
go c n a = j $ patch c n =<< interpret toJSM (view a)