jsmw-0.1: Language/JSMW/Monad.hs
------------------------------------------------------------------
-- |
-- Module : Language.JSMW.Monad
-- Copyright : (c) Dmitry Golubovsky, 2009
-- License : BSD-style
--
-- Maintainer : golubovsky@gmail.com
-- Stability : experimental
-- Portability : portable
--
--
--
-- A special monad: the core of the Writer.
------------------------------------------------------------------
module Language.JSMW.Monad (
-- * The Monad itself
JSMW
,runJSMW
,runJSMWWith
,getBlock
,currDocBody
-- * Compilation control
,once
,mkNewVar
,writeStmt
-- * Create Javascript values, Monadic versions
,stringM
,numberM
,boolM
-- * Layout control
,ECRF
,passive
,nest
,container
-- * Element references
-- $ref
,ref
,ref2ecrf
,inside
-- * Inline style and other decorations
,CSSDeco (..)
,setStyle
-- * Event handling
,OnHandler
,setHandler
,ask
)where
import Control.Monad
import Control.Monad.State
import Control.Monad.RWS
import BrownPLT.JavaScript
import Data.DOM
import Data.DOM.Dom
import Data.DOM.Node
import Data.DOM.Html2
import Data.DOM.Events
import Data.DOM.HTMLBodyElement
import Data.DOM.CSSStyleDeclaration
-- | A type of the writer: based on the 'RWS' Monad. The Reader part holds an expression
-- to reference the curent HTML container element. The Writer part is the list of Javascript
-- statements being formed. Container may be any DOM Element, but not a Text node
-- or anything else.
type JSMW e a = RWS (Expression e) [Statement ()] Int a
-- | Run the code writer (raw way, returns both state and log). Container will be
-- initialized into the body of the current HTML document. Same as
-- @ 'runJSMWWith' 'currDocBody' @
runJSMW :: Int -- ^ Initial state (usually 0)
-> JSMW THTMLBodyElement (Expression a) -- ^ The JSMW expression to process
-> (Expression a, Int, [Statement ()]) -- ^ Result: (final expression,
-- final state,
-- produced statements),
runJSMW st q = runJSMWWith currDocBody st q
-- | Run the code writer (raw way, returns both state and log) with explicitly
-- specified container.
runJSMWWith :: (CElement e)
=> (Expression e) -- ^ container
-> Int -- ^ Initial state (usually 0)
-> JSMW e (Expression a) -- ^ The JSMW expression to process
-> (Expression a, Int, [Statement ()]) -- ^ Result: (final expression,
-- final state,
-- produced statements),
runJSMWWith e st q = runRWS q e st
-- | Body of the current document: use it to start the toplevel instance
-- of the Writer as a container for 'runJSMWWith'.
currDocBody :: Expression THTMLBodyElement
currDocBody = VarRef THTMLBodyElement (Id THTMLBodyElement "window.document.body")
-- | Obtain a block statement from the result of 'runJSWM'. The last expression
-- forms a \'return\' statement, so the resulting block may be used as a function\'s body.
getBlock :: (Expression a, Int, [Statement ()]) -> Statement ()
getBlock (fin, _, stmts) = BlockStmt () (stmts ++ [ReturnStmt () (Just (fin /\ ()))])
-- | Type of a function creating HTML elements, e. g. 'mkButton', 'mkDiv'
type ECRF e n = Expression THTMLDocument -> JSMW e (Expression n)
-- | Insert a passive element into the current container.
passive :: (CNode n, CElement e)
=> ECRF e n -- ^ function that creates a HTML element
-> JSMW e (Expression ()) -- ^ 'passive' does not return a value
passive crf = do
cntr <- ask
doc <- get'ownerDocument cntr
e <- once =<< crf doc
once =<< addChild e cntr
return $ NullLit ()
-- | Nest an element inside another element via monadic composition.
-- Example usage:
--
-- @'ask' >>= 'nest' 'mkButton' >>= 'nest' ('mkText' $ 'string' \"Foo\")@
--
-- inserts a button with text \"Foo\" into the current container.
--
-- The type system makes sure that only an instance of a DOM Element
-- can nest other elements, e. g.
--
-- @ ... mkText (string \"Foo\") >>= nest mkDiv@
--
-- would not typecheck.
--
-- Example: a text, a newline, and two buttons: @'ask'@ retrieves the current
-- container.
--
-- @
-- q = do
-- passive (mkText $ string \"Hello\")
-- passive mkBr
-- ask >>= nest mkButton >>= nest (mkText $ string \"Foo\")
-- ask >>= nest mkButton >>= nest (mkText $ string \"Bar\")
-- @
nest :: (CNode n, CElement e, CElement p)
=> ECRF e n -- ^ function that creates a HTML element to nest
-> Expression p -- ^ parent element (implicit if '>>=' is used)
-> JSMW e (Expression n) -- ^ nested element is returned (per 'addChild')
nest crf par = do
doc <- once =<< get'ownerDocument par
c <- once =<< crf doc
once =<< addChild c par
-- | Specify a new container that in nested into the current one. As long as the container
-- is active, all subsequently defined elements will be inserted into it.
--
-- Example: a Button with two text labels separated with a newline:
--
-- @
-- mkButton \`container\` (do
-- passive (mkText $ string \"Hello\")
-- passive mkBr
-- passive (mkText $ string \"GoodBye\"))
-- @
--
-- Everything defined within a @do@ expression is inserted into the button
-- which is the new container.
container :: (CElement n, CElement e)
=> ECRF e n -- ^ function that creates a new container
-> JSMW n (Expression x) -- ^ whatever goes into that container
-> JSMW e (Expression ()) -- ^ 'container' does not return a value
container crf cnt = do
curc <- once =<< ask
doc <- once =<< get'ownerDocument curc
newc <- once =<< crf doc
once =<< addChild newc curc
carg <- mkNewVar
st <- get
let et = exprType newc
(finx, fins, stms) = runJSMWWith (VarRef et (Id et carg)) st cnt
blk = getBlock (finx, fins, stms)
fun = ParenExpr () (FuncExpr () [Id () carg] blk)
call = CallExpr () fun [newc /\ ()]
writeStmt (ExprStmt () call)
put fins
return $ NullLit ()
-- $ref Sometimes it may be necessary to create an element, but \"engage\" it later.
-- Element references help achieve this. References also can be useful when
-- several elements have to interact with each other: elements created
-- with 'nest' or 'container' are accessible only from the \"inside\" code.
-- To enable interaction, a reference to an element has to be made known
-- to another element's event handler.
--
-- A rather contrived example below shows how to create an input element, and insert it
-- into a button later.
--
-- @
-- import qualified Data.DOM.HTMLInputElement as I
-- ...
-- inp <- 'ref' 'I.mkInput' -- create a reference
-- inp \`inside\` ('setStyle' [\"border-color\" := \"green\"]) -- do something with the reference
-- ...
-- inpr <- 'ref2ecrf' inp -- simulate an element creation function
-- mkButton \`container\` (inpr \`container\` (ask >>= 'I.set'value' ('string' \"foo\")))
-- @
-- | Create an element for future use, and return a reference to it.
-- The element may be inserted into a container different from one it was
-- created with (when 'ref' was called). But it should be used within the same
-- document it was created inside.
ref :: (CNode e, CElement n) => ECRF e n -> JSMW e (Expression n)
ref crf = do
cntr <- ask
doc <- get'ownerDocument cntr
e <- once =<< crf doc
return e
-- | Turn an element reference into element creation function. It can be useful
-- when an element created earlier has to be used as a container, or a passive element,
-- or nested. The type signature of 'ref2ecrf' reflects the fact that the element
-- was created when one container was current, but may be used with another container.
ref2ecrf :: (CElement e1, CElement e2, CNode n) => Expression n -> JSMW e1 (ECRF e2 n)
ref2ecrf n = return $ \d -> return n
-- | Essentially same as 'container' except that a reference to an element
-- has to be supplied rather than an element creation function. Another
-- difference from 'container': element referenced is not added as a child to
-- the current container.
inside :: (CElement n, CElement e)
=> Expression n -- ^ reference to an element
-> JSMW n (Expression x) -- ^ whatever goes into that element
-> JSMW e (Expression ()) -- ^ 'inside' does not return a value
inside e cnt = do
newc <- once =<< return e
carg <- mkNewVar
st <- get
let et = exprType newc
(finx, fins, stms) = runJSMWWith (VarRef et (Id et carg)) st cnt
blk = getBlock (finx, fins, stms)
fun = ParenExpr () (FuncExpr () [Id () carg] blk)
call = CallExpr () fun [newc /\ ()]
writeStmt (ExprStmt () call)
put fins
return $ NullLit ()
-- | Data type for building inline style assignment expressions.
data CSSDeco = String := String
-- | An action to use within a container to update its inline style.
-- 'setStyle' called with an empty list does not change the inline
-- style. Note that style settings are compile-time only.
--
-- Example: a DIV element with style settings applied and a text:
--
-- @
-- mkDiv \`container\` (do
-- setStyle [\"display\" := \"inline\"
-- ,\"float\" := \"right\"
-- ,\"width\" := \"45%\"
-- ,\"text-align\" := \"center\"
-- ,\"background-color\" := \"green\"
-- ,\"color\" := \"white\"
-- ,\"font-weight\" := \"bold\"]
-- passive (mkText $ string \"Styled\"))
-- @
setStyle :: (CHTMLElement e) => [CSSDeco] -> JSMW e (Expression ())
setStyle csp = do
istd <- once =<< (ask >>= inlineStyleDecl)
mapM (\(p := v) -> once =<< setProperty (string p) (string v) (string "") istd) csp
return $ NullLit ()
-- | A type for a on-style event handler. It represents a function which
-- takes an event and returns a boolean.
type OnHandler e c = Expression e -> JSMW c (Expression Bool)
-- | Set a on-style (e. g. onclick) event handler on the current container.
--
-- Example: a button with a click handler which shows the X coordinate of the click.
--
-- @
-- mkButton \`container\` (do
-- passive (mkText $ string \"Click Me\")
-- setHandler \"click\" clickHandler)
-- ...
-- clickHandler :: OnHandler TMouseEvent THTMLButtonElement
-- clickHandler e = do
-- getm'clientX e >>= toString >>= alert
-- return true
-- @
--
-- A handler function has one argument which gets the reference to the event caught.
-- The handler function also may implicitly address the container it was set on by
-- calling 'ask' or 'passive'. For example, calling @passive (mkText $ string \"x\")@
-- within a handler will result in a text node being added to the container.
--
-- Also note that the 'OnHandler' type may be parameterized by the type of containers
-- it can be set on. In the example above, the handler may only be set on buttons.
--
-- The MSIE-specific code to obtain event from the static attribute of the current
-- window is inserted in the beginning of the handler automatically.
setHandler :: (CHTMLElement c, CEvent e) => String -> OnHandler e c -> JSMW c (Expression ())
setHandler s x = do
ctr <- once =<< ask
earg <- mkNewVar
st <- get
let et = undefined :: e
prop = "on" ++ s
evar = VarRef et (Id et earg)
(finx, fins, stms) = runJSMWWith ctr st (x evar)
msievent = IfSingleStmt () (PrefixExpr () PrefixLNot (evar /\ ()))
(BlockStmt () [ExprStmt ()
(AssignExpr () OpAssign (evar /\ ())
(VarRef () (Id () "window.event")))])
blk = getBlock (finx, fins, msievent : stms)
fun = FuncExpr () [Id () earg] blk
seth = ExprStmt () $ AssignExpr () OpAssign (DotRef () (ctr /\ ()) (Id () prop)) (fun /\ ())
writeStmt seth
put fins
return (NullLit ())
-- | Create a unique variable name. This function increments the internal state of the
-- monad and produces a string consisting of the letter \'v\' and a unique number.
mkNewVar :: JSMW e String
mkNewVar = do
modify (+ 1)
n <- get
return ('v' : show n)
-- | Write out a statement. This function utilizes the Writer part of the monad, and
-- adds the Javascript statement provided to the Writer's log.
writeStmt :: Statement () -> JSMW e ()
writeStmt = tell . (: [])
-- | The JSMW code consists of monadic smart constructors forming Javascript
-- method calls. These constructors are inlined each time they are referenced.
-- The "once" combinator causes a variable assignment statement to be formed
-- with the variable assigned to the expression returned. All future references
-- will be to the variable rather than to the expression. Since the expression
-- will be evaluated when assigned to the variable, referencing the variable
-- will reference the result, and possible effect will not be repeated.
once :: Expression a -> JSMW e (Expression a)
once e@(VarRef _ _) = return e
once e = do
nv <- mkNewVar
let et = exprType e
eo = e /\ ()
stm = VarDeclStmt () [VarDecl () (Id () nv) (Just eo)]
vr = VarRef et (Id et nv)
writeStmt stm
return vr
-- | Create a Javascript string literal out of a string, monadic version.
stringM :: String -> JSMW e (Expression String)
stringM s = once =<< (return $ string s)
-- | Create a Javascript numeric literal out of a numeric value, monadic version.
numberM :: (Integral n) => n -> JSMW e (Expression Double)
numberM = return . number
-- | Create a Javascript boolean literal out of a Boolean, monadic version.
boolM :: Bool -> JSMW e (Expression Bool)
boolM = return . bool