{-# LANGUAGE OverloadedStrings, RecordWildCards, DeriveGeneric, ScopedTypeVariables #-} {-| Module : Network.N2O.Nitro Description : Nitro DSL Copyright : (c) Marat Khafizov, 2018 License : BSD-3 Maintainer : xafizoff@gmail.com Stability : experimental Portability : not portable Nitro DSL to build interactive user interfaces -} module Network.N2O.Nitro where import Control.Monad (forM_, void) import qualified Data.Binary as B import qualified Data.ByteString as BS import qualified Data.ByteString.Base64.Lazy as B64 import qualified Data.ByteString.Char8 as C8 import qualified Data.ByteString.Lazy as BL import qualified Data.ByteString.Lazy.Char8 as CL8 import Data.Char (isAlphaNum, ord, toLower) import Data.IORef import Data.List (intercalate) import Data.Map.Strict ((!?)) import Data.String import qualified Data.Text.Lazy as TL import qualified Data.Text.Lazy.Encoding as TL import qualified Data.Text.Encoding as T import GHC.Generics (Generic) import Network.N2O hiding (Event) import Numeric (showHex) import Prelude hiding (id) -- | An HTML element data Element a = Element { name :: BS.ByteString , id :: BS.ByteString , body :: [Element a] , postback :: Maybe a , source :: [BS.ByteString] , noBody :: Bool , noClosing :: Bool } | Text TL.Text deriving (Show, Generic) instance (B.Binary a) => B.Binary (Element a) -- | Action that can be rendered as JavaScript events data Action a = AEvent (Event a) | AElement (Element a) | ARaw BL.ByteString deriving (Show, Generic) instance (B.Binary a) => B.Binary (Action a) -- | A JavaScript event data Event a = Event { eventTarget :: BS.ByteString , eventPostback :: a , eventType :: BS.ByteString , eventSource :: [BS.ByteString] } deriving (Show, Generic) instance (B.Binary a) => B.Binary (Event a) -- | Wire an element wireEl :: (B.Binary a) => Element a -> N2O f a (Result a) wireEl = wire . AElement -- | Wire action wire :: forall f a. (B.Binary a) => Action a -> N2O f a (Result a) wire a = do actions <- getActions putActions (a : actions) return Empty -- | Render list of actions to JavaScript renderActions :: (B.Binary a) => [Action a] -> N2O f a BL.ByteString renderActions [] = return "" renderActions (a:as) = do r <- renderAction a rs <- renderActions as return (r <> ";" <> rs) -- | Render an action renderAction :: (B.Binary a) => Action a -> N2O f a BL.ByteString renderAction (ARaw bs) = return bs renderAction (AEvent ev) = renderEvent ev renderAction (AElement el@Element {..}) = do case postback of Nothing -> return () Just pb -> void (wire $ AEvent Event {eventType = "click", eventPostback = pb, eventTarget = id, eventSource = source}) return "" -- | Render list of elements to the HTML renderElements :: (B.Binary a) => [Element a] -> N2O f a BL.ByteString renderElements [] = return "" renderElements (e:es) = do r <- renderElement e rs <- renderElements es return (r <> rs) -- | Render element to the HTML renderElement :: (B.Binary a) => Element a -> N2O f a BL.ByteString renderElement (Text t) = return $ TL.encodeUtf8 t renderElement Element {..} = do case postback of Nothing -> return () Just pb -> void (wire $ AEvent Event {eventType = "click", eventPostback = pb, eventTarget = id, eventSource = source}) case name of "br" -> return "
" _ -> do content <- renderElements body return $ if noBody then "<" <> BL.fromStrict name <> " " <> idProp id <> "/>" else "<" <> BL.fromStrict name <> " " <> idProp id <> ">" <> content <> " BL.fromStrict name <> ">" where idProp x = if x == "" then "" else "id=\"" <> BL.fromStrict x <> "\"" -- | Render event renderEvent :: Event a -> N2O f a BL.ByteString renderEvent Event {..} = do ref <- ask cx@Context {cxPickle = pickle} <- lift $ readIORef ref case eventSource of [] -> return BL.empty src -> return $ "{ var x=qi('" <> BL.fromStrict eventTarget <> "'); x && x.addEventListener('" <> BL.fromStrict eventType <> "',function(event){ if (validateSources(" <> strJoin (map (\x -> "'" <> x <> "'") src) <> ")) { ws.send(enc(tuple(atom('pickle'),bin('" <> BL.fromStrict eventTarget <> "'),bin('" <> pickle eventPostback <> "')," <> strJoin (map renderSource src) <> "))); } else console.log('Validation error'); })}" where renderSource :: BS.ByteString -> BS.ByteString renderSource s = "tuple(atom('" <> s <> "'),querySource('" <> s <> "'))" strJoin :: [BS.ByteString] -> BL.ByteString strJoin = BL.fromStrict . BS.intercalate "," -- | Element constructor baseElement :: Element a baseElement = Element { id = "" , name = undefined , postback = Nothing , body = [] , source = [] , noBody = False , noClosing = False } -- | An HTML button button :: Element a button = baseElement {name = "button", source = []} -- | A @panel@ widget panel = baseElement {name = "div"} -- | Text node text :: TL.Text -> Element a text = Text -- | @
@ element br = baseElement {name = "br", noBody = True, noClosing = True} -- | A @textbox@ widget textbox :: Element a textbox = baseElement {name = "input type=\"text\"", noBody = True} -- | Update text content of the element with the specified @id@ updateText :: (B.Binary a) => BS.ByteString -> TL.Text -> N2O f a (Result a) updateText target s = wire (ARaw ("qi('" <> BL.fromStrict target <> "').innerText='" <> TL.encodeUtf8 s <> "'")) insertBottom :: (B.Binary a) => BS.ByteString -> Element a -> N2O f a (Result a) insertBottom target elem = do content <- renderElement elem let action = "(function(){ var div = qn('div'); div.innerHTML = '" <> TL.decodeUtf8 content <> "';qi('" <> TL.decodeUtf8 (BL.fromStrict target) <> "').appendChild(div.firstChild); })();" wire $ ARaw $ TL.encodeUtf8 action -- | Escape untrusted text to prevent XSS jsEscapeT :: TL.Text -> TL.Text jsEscapeT t = TL.pack (escape (TL.unpack t) "") where escape "" acc = acc escape (x:xs) acc = escape xs $ if isAlphaNum x then acc ++ [x] else acc <> "\\x" <> (flip showHex "" . ord $ x) -- | Escape untrusted text to prevent XSS jsEscape :: CL8.ByteString -> TL.Text jsEscape = jsEscapeT . TL.decodeUtf8 -- | Default pickler defPickle :: (Show a) => a -> BL.ByteString defPickle = B64.encode . CL8.pack . show -- | Default depickler defDePickle :: (Read a) => BL.ByteString -> Maybe a defDePickle bs = case B64.decode bs of Right x -> Just $ read $ CL8.unpack x _ -> Nothing -- | Get action list from the local mutable state getActions :: (B.Binary a) => N2O f a [Action a] getActions = do mbActions <- get (C8.pack "actions") return $ case mbActions of Just actions -> actions _ -> [] -- | Put actions to the local mutable state putActions :: (B.Binary a) => [Action a] -> N2O f a () putActions = put (C8.pack "actions")