{-# LANGUAGE FlexibleContexts, FlexibleInstances, MultiParamTypeClasses, UndecidableInstances, QuasiQuotes, TypeSynonymInstances, OverloadedStrings, TypeFamilies #-} {-# OPTIONS_GHC -fno-warn-orphans #-} -- | This module provides support for: -- -- 1. embedding Javascript generated by JMacro into HSX. -- -- 2. turning XML generated by HSX into a DOM node in Javascript -- -- It provides the following instances: -- -- > instance (XMLGenerator m, IntegerSupply m) => EmbedAsChild m JStat -- > instance (IntegerSupply m, IsName n, EmbedAsAttr m (Attr Name String)) => EmbedAsAttr m (Attr n JStat) -- > instance ToJExpr XML -- > instance ToJExpr DOMNode -- > instance ToJExpr XMLToInnerHTML -- > instance ToJExpr XMLToDOM -- -- In order to ensure that each embedded 'JStat' block has unique -- variable names, the monad must supply a source of unique -- names. This is done by adding an instance of 'IntegerSupply' for -- the monad being used with 'XMLGenerator'. -- -- For example, we can use 'StateT' to provide an 'IntegerSupply' instance for 'ServerPartT': -- -- > instance IntegerSupply (ServerPartT (StateT Integer IO)) where -- > nextInteger = nextInteger' -- -- Alternatively, we can exploit the IO monad to provide an 'IntegerSupply' instance for 'ServerPartT': -- -- > instance IntegerSupply (ServerPartT IO) where -- > nextInteger = fmap (fromIntegral . (`mod` 1024) . hashUnique) (liftIO newUnique) -- -- The @ToJExpr XML@ instance allows you to splice in XML lifted out of an -- arbitrary monad to generate DOM nodes with JMacro antiquotation: -- -- > js = do html <- unXMLGenT <p>I'm in a Monad!</p> -- > return [jmacro| document.getElementById("messages").appendChild(`(html)`); |] -- -- The @ToJExpr DOMNode@ instance allows you to run HSP in the Identity -- monad to render JMacro in pure code: -- -- > html :: DOMNode -- > html = <p>I'm using <em>JavaScript</em>!</p> -- > js = [jmacro| var language = `(html)`.getElementsByTagName("em")[0].textContent; |] -- -- You can see here that you get an actual DOM tree in JavaScript. This is -- also compatible with libraries such as jQuery and YUI which are able to -- wrap DOM nodes in their own type, for example with jQuery: -- -- > js = [jmacro| var languages = $(`(html)`).find("em").text(); |] -- -- Or with YUI: -- -- > js = [jmacro| var languages = Y.one(`(html)`).one("em").get("text"); |] -- -- There are two ways to turn HTML into a a DOM node in the -- browser. One way is to render the HTML to a string, and pass the -- string to @element.innerHTML@. The other way is to us the use the -- DOM functions like @createElement@, @setAttribute@, to -- programatically create the DOM on the client. -- -- In webkit-based browsers like Chrome and Safari, the DOM method -- appears to be slightly faster. In other browsers, the @innerHTML@ -- method appears to be faster. The @innerHTML@ method will almost -- always required fewer bytes to be transmitted. Additionally, if -- your XML/HTML contains pre-escaped content, you are required to use -- @innerHTML@ anyway. -- -- So, by default the 'ToJExpr' 'XML' instance uses the @innerHTML@ -- method. Though, that could change in the future. If you care about -- using one method over the other you can use the @newtype@ wrappers -- 'XMLToInnerHTML' or 'XMLToDOM' to select which method to use. module HSP.JMacro where import Control.Monad.Identity import Control.Monad.Trans (lift) import Control.Monad.State (MonadState(get,put)) import Data.Text.Lazy (Text, unpack) import HSP.XML import HSP.HTML4 (renderAsHTML) import HSP.XMLGenerator import HSP.Monad (HSPT(..)) import Language.Javascript.JMacro (JStat(..), JExpr(..), JVal(..), Ident(..), ToJExpr(..), toStat, jmacroE, jLam, jVarTy, jsToDoc, jsSaturate, renderPrefixJs) import Text.PrettyPrint.Leijen.Text (Doc, displayT, renderOneLine) -- | This class provides a monotonically increasing supply of non-duplicate 'Integer' values class IntegerSupply m where nextInteger :: m Integer -- | This help function allows you to easily create an 'IntegerSupply' -- instance for monads that have a 'MonadState' 'Integer' instance. -- -- For example: -- -- > instance IntegerSupply (ServerPartT (StateT Integer IO)) where -- > nextInteger = nextInteger' nextInteger' :: (MonadState Integer m) => m Integer nextInteger' = do i <- get put (succ i) return i instance (XMLGenerator m, IntegerSupply m, EmbedAsChild m Text, StringType m ~ Text) => EmbedAsChild m JStat where asChild jstat = do i <- lift nextInteger asChild $ genElement (Nothing, fromStringLit "script") [asAttr ((fromStringLit "type" := fromStringLit "text/javascript") :: Attr Text Text)] [asChild (displayT $ renderOneLine $ renderPrefixJs (show i) jstat)] instance (XMLGen m, IntegerSupply m, EmbedAsAttr m (Attr n Text)) => EmbedAsAttr m (Attr n JStat) where asAttr (n := jstat) = do i <- lift nextInteger asAttr $ (n := (displayT $ renderOneLine $ renderPrefixJs (show i) jstat)) -- | Provided for convenience since @Ident@ is exported by both -- @HSP.Identity@ and @JMacro@. Using this you can avoid the need for an -- extra and qualified import. type DOMNode = HSPT XML Identity XML instance ToJExpr DOMNode where toJExpr = toJExpr . runIdentity . unHSPT -- | newtype which can be used with 'toJExpr' to specify that the XML -- should be converted to a DOM in javascript by using 'innerHTML' newtype XMLToInnerHTML = XMLToInnerHTML XML instance ToJExpr XMLToInnerHTML where toJExpr (XMLToInnerHTML xml) = [jmacroE| (function { var node = document.createElement('div') ; node.innerHTML = `(unpack $ renderAsHTML xml)` ; return node.childNodes[0] })() |] -- | newtype which can be used with 'toJExpr' to specify that the XML -- should be converted to a DOM in javascript by using -- @createElement@, @appendChild@, and other DOM functions. -- -- WARNING: @CDATA FALSE@ values are assumed to be pre-escaped HTML and will be converted to a DOM node by using @innerHTML@. Additionally, if the call to @innerHTML@ returns more than one node, only the first node is used. newtype XMLToDOM = XMLToDOM XML instance ToJExpr XMLToDOM where toJExpr (XMLToDOM (Element (dm', n') attrs children)) = let dm = fmap unpack dm' n = unpack n' in [jmacroE| (function { var node = `(createElement (dm) (n))` ; `(map (setAttribute node) attrs)` ; `(map (appendChild node . XMLToDOM) children)` ; return node })() |] where createElement Nothing n = [jmacroE| document.createElement(`(n)`) |] createElement (Just ns) n = [jmacroE| document.createElementNS(`(ns)`, `(n)`) |] appendChild node c' = [jmacroE| (function () { var c = `(c')`; if (Object.prototype.toString.call(c) === '[object Array]') { for (var i = 0; i < c.length; i++) `(node)`.appendChild(c[i]); } else { `(node)`.appendChild(`(c)`); } })() |] setAttribute node (MkAttr ((Nothing, nm'), (Value True val'))) = let nm = unpack nm' val = unpack val' in [jmacroE| `(node)`.setAttribute(`(nm)`, `(val)`) |] setAttribute node (MkAttr ((Just ns', nm'), (Value True val'))) = let ns = unpack ns' nm = unpack nm' val = unpack val' in [jmacroE| `(node)`.setAttributeNS(`(ns)`, `(nm)`, `(val)`) |] toJExpr (XMLToDOM (CDATA True txt')) = let txt = unpack txt' in [jmacroE| document.createTextNode(`(txt)`) |] toJExpr (XMLToDOM (CDATA False txt')) = let txt = unpack txt' in [jmacroE| (function { var node = document.createElement('div') ; node.innerHTML = `(txt)` ; return node })() |] instance ToJExpr XML where toJExpr = toJExpr . XMLToInnerHTML