packages feed

ghcjs-vdom-0.2.0.0: src/GHCJS/VDOM/QQ.hs

{-# LANGUAGE QuasiQuotes, DeriveDataTypeable, TemplateHaskell #-}
{-
  More efficient JavaScript literals with QuasiQuoters

  mostly experimental, might not stay
 -}
module GHCJS.VDOM.QQ (ch, children, att, attributes) where

import           Language.Haskell.TH.Quote
import           Language.Haskell.TH.Syntax

import           GHCJS.VDOM.Internal.Types

import           GHCJS.Types
import           GHCJS.Marshal

import           Control.Applicative

import           Data.Char
import qualified Data.List as L
import           Data.List.Split
import           Data.Typeable

import           System.IO.Unsafe

att :: QuasiQuoter
att = attributes

ch :: QuasiQuoter
ch = children

-- example: [props|a:1, b: null, c: x, d: y |]
-- every value is either a literal or a variable referring to a convertible Haskell name
-- fixme, this does not have a proper parser
attributes :: QuasiQuoter
attributes = QuasiQuoter { quoteExp = quoteProps }

quoteProps :: String -> Q Exp
quoteProps pat = jsExpQQ ('{':ffiPat++"}") (map mkName names)
                 (\x -> AppE (VarE 'unsafePerformIO) (AppE (VarE 'toJSVal) x))
                 (AppE (ConE 'Attributes'))
  where
    (names, ffiPat) = genpat 1 $ map (break (==':') . trim) (linesBy (==',') pat)
    isName [] = False
    isName (x:xs) = isAlpha x && all isAlphaNum xs
    genpat :: Int -> [(String,String)] -> ([String], String)
    genpat _ [] = ([], "")
    genpat k ((x,':':n):xs)
      | isName n' = (n':ns, x ++ ": $" ++ (show k) ++ p)
      | otherwise = (ns, x ++ ':' : n ++ sep ++ p)
      where
        n'       = trim n
        ~(ns, p) = genpat (k+1) xs
        sep      = if null xs then "" else ","
    genpat _ _ = error "invalid pattern"


-- example: [children|x,y,z|] for haskell names x,y,z :: VNode
children :: QuasiQuoter
children = QuasiQuoter { quoteExp = quoteChildren }

quoteChildren :: String -> Q Exp
quoteChildren pat = jsExpQQ ffiPat names (AppE (VarE 'unVNode)) (AppE (ConE 'Children'))
  where
    names  = map (mkName.trim) (linesBy (==',') pat)
    ffiPat = '[' : L.intercalate "," (map (('$':).show) (take (length names) [(1::Int)..])) ++ "]"

trim :: String -> String
trim = let f = reverse . dropWhile isSpace in f . f

newtype QQCounter = QQCounter { getCount :: Int } deriving (Typeable, Show)

jsExpQQ :: String -> [Name] -> (Exp -> Exp) -> (Exp -> Exp) -> Q Exp
jsExpQQ pat args unwrap wrap = do
  c <- maybe 0 getCount <$> qGetQ
  n <- newName ("__ghcjs_vdom_qq_spliced_" ++ show c)
  let ffiDecl      = ForeignD (ImportF CCall Unsafe pat' n (ty $ length args))
      ty :: Int -> Type
      ty 0         = ref
      ty n         = AppT (AppT ArrowT ref) (ty (n-1))
      ref          = ConT ''JSVal
      ffiCall []     = (VarE n)
      ffiCall (y:ys) = AppE (ffiCall ys) (unwrap (VarE y))
      pat'           = "__ghcjs_javascript_" ++ L.intercalate "_" (map (show . ord) pat)
  qAddTopDecls [ffiDecl]
  qPutQ (QQCounter (c+1))
  return $ wrap (ffiCall $ reverse args)