packages feed

Hawk-0.0.2: src/Hawk/View/Template/Interpreter.hs

{-# LANGUAGE Arrows, TemplateHaskell #-}
module Hawk.View.Template.Interpreter
  ( evalTemplate, bind, bindAttribute, prepareDoc, bindTyped, BindTree, prepareDoc', hawkPrefix
  ) where

-- Hawk
import Hawk.Controller.Types
  ( ResponseState (..)
  , RequestEnv (..)
  , EnvController
  , runController
  , StateController
  )
import Data.ByteString.Lazy.UTF8 (fromString)
import Data.ByteString.Lazy (ByteString)
import qualified Data.EitherMapTree as E
import Hack

-- Xml processing
import Text.XML.HXT.Arrow as Arrow
import qualified Text.XML.HXT.DOM.ShowXml as ShowXml
import qualified Text.XML.HXT.DOM.QualifiedName as QN

-- other stuff
import qualified Data.Map as M
import Control.Monad.State (StateT, runStateT, liftIO, get, put)
import Control.Monad.Reader (ReaderT, ask, runReaderT)
import Control.Monad.Either (runEitherT, returnLeft)
import Data.Maybe (fromMaybe)
import Hawk.Controller.Responses

-- logging
import qualified System.Log.Logger as Logger
import System.Log.Logger.TH (deriveLoggers)

$(deriveLoggers "Logger" [Logger.DEBUG, Logger.WARNING])


type HawkArrow = IOSLA (XIOState (RequestEnv, ResponseState))

debugA :: ArrowIO a => a String ()
debugA = arrIO debugM
{-
debugA' :: (ArrowIO a, Show b) => a b b
debugA' = arr id &&& arrIO (debugM . show) >>> arr (\(x,_) -> x)
-}

-- | The Prefix of the hawk namespace
hawkPrefix :: String
hawkPrefix = "hawk"

-- | Create a qualified name for the hawk prefix
hawkQName :: String -> QN.QName
hawkQName l = QN.mkQName hawkPrefix l ""
-- TODO use url not prefix

-- TODO catch errors
evalTemplate :: (XmlTree -> StateController [XmlTree]) -> FilePath -> StateController ByteString
evalTemplate f fp = do
  env <- ask
  state <- get
  (state', res) <- liftIO $ runIOSLA (mainA f) (Arrow.initialState (env, state)) fp
  put $ snd $ xio_userState state'
  case res of
    (Left resp   : _) -> returnLeft resp
    (Right body' : _) -> return $ fromString body'
    _                 -> returnLeft $ errorResponse $ fromString "error in template"

--  liftM fromString $ EitherT $ return $ head res
  -- TODO handel result with not one result

-- --------------------------------------------------------------------------
-- General template processing
-- --------------------------------------------------------------------------

-- | process a file with all controllers. 
mainA :: (XmlTree -> StateController [XmlTree]) -> HawkArrow String (Either Response String)
mainA rw = prepareDoc
     >>> invokeController rw
     >>> Arrow.right (unlistA
                     >>> processTD interpreteLast
                     >>> headMerge
                     >>> clearNamespace
                     >>> writeDocumentToString [(a_no_empty_elements,v_1), (a_indent, v_1), (a_output_html,v_1), (a_output_encoding, utf8), (a_add_default_dtd, v_1)]
                     )


prepareDoc :: IOStateArrow s String XmlTree
prepareDoc = readDoc >>> processTD interpreteTag

prepareDoc' :: IOStateArrow s String XmlTree
prepareDoc' = readDoc >>> processTD interpreteTag'

isInterpretableTag :: ArrowXml a => a XmlTree XmlTree
isInterpretableTag = isElem >>> hasNamePrefix hawkPrefix

processTD :: ArrowXml a => a XmlTree XmlTree -> a XmlTree XmlTree
processTD = processTopDown . (`when` isInterpretableTag)

interpreteTag :: IOStateArrow s XmlTree XmlTree
interpreteTag = choiceA
    [ isEmbed    :-> embed
    , isIgnore   :-> (ignore >>> processTD interpreteTag)
    , isSurround :-> surround
    , this       :-> this
    ]

interpreteTag' :: IOStateArrow s XmlTree XmlTree
interpreteTag' = choiceA
    [ isIgnore   :-> (ignore >>> processTD interpreteTag')
    , isSurround :-> surround'
    , this       :-> this
    ]


interpreteLast :: HawkArrow XmlTree XmlTree
interpreteLast = choiceA
    [ isEmbed        :-> (removeEmbed >>> processTD interpreteLast)
    , isMessage      :-> message
    , isErrorMessage :-> errorMessage
    , this           :-> (remaining >>> processTD interpreteLast)
    ]

-- --------------------------------------------------------------------------
-- Arrows for specific tags
-- --------------------------------------------------------------------------

removeEmbed :: ArrowXml a => a XmlTree XmlTree
removeEmbed = getChildren -- TODO handle tag with no children

-- | Check whether the current element is an embed tag
isEmbed :: ArrowXml a => a XmlTree XmlTree
isEmbed = hasLocalPart "embed" >>> hasAttr "what"

-- | Embed another template into the current template
embed :: IOStateArrow s XmlTree XmlTree
embed = applyA $  getAttrValue "what" >>>  listA loadTemplateA >>> arr setChildren
{- proc t -> do
  what  <- getAttrValue "what"  -< t
  debugA -< "embedding template '" ++ what ++ "'"
  loadTemplateA                 -< what -}

isIgnore :: ArrowXml a => a XmlTree XmlTree
isIgnore = hasLocalPart "ignore"

ignore :: ArrowXml a => a XmlTree XmlTree
ignore = getChildren

isMessage :: ArrowXml a => a XmlTree XmlTree
isMessage = hasLocalPart "message" >>> hasAttr "type"

message :: HawkArrow XmlTree XmlTree
message = proc t -> do
  msgtype <- getAttrValue "type" -< t
  (_,us)  <- getUserState        -< ()
  case M.lookup msgtype (flash us) of
    Nothing  -> none   -< ()
    Just msg -> do
       text  <- mkText -< msg
       replace <-  arr replaceContent -< text
       (getChildren <<< replace) -<< t

isErrorMessage :: ArrowXml a => a XmlTree XmlTree
isErrorMessage = hasLocalPart "error" >>> hasAttr "for"

errorMessage :: HawkArrow XmlTree XmlTree
errorMessage = proc t -> do
  msgFor <- getAttrValue "for" -< t
  (_,us) <- getUserState       -< ()
  case M.findWithDefault [] msgFor (errors us) of
    [] -> none -< ()
    es -> do
       let errs = map (\(a,e) -> if null a then e else a ++ " : " ++ e) es
       texts <- (selem "ul" [unlistA >>> selem "li" [mkText]]) -< errs
       replace <- arr replaceContent -< texts
       (getChildren <<< replace) -<< t

isContent :: ArrowXml a => a XmlTree XmlTree
isContent = hasNamePrefix hawkPrefix >>> hasLocalPart "content"

replaceContent :: ArrowXml a => XmlTree -> a XmlTree XmlTree
replaceContent content = processTopDown (constA content `when` isContent)

isSurround :: ArrowXml a => a XmlTree XmlTree
isSurround = hasLocalPart "surround" >>> hasAttr "with" >>> hasAttr "at"

-- Surround template with another template.
surround :: IOStateArrow s XmlTree XmlTree
surround = proc t -> do
  children <- listA getChildren    -< t
  at       <- getAttrValue "at"    -< t
  with     <- getAttrValue "with"  -< t
  -- Load the surrounding template
  outer    <- loadTemplateA         -< with
  -- Insert the original template into the surrounding template
  debugA -< "surrounding with '" ++ with ++ "' at '" ++ at ++ "'"
  bound    <- bindA                -< ([(at, children)], outer)
  processTD interpreteTag          -< bound

surround' :: IOStateArrow s XmlTree XmlTree
surround' = proc t -> do
  children <- listA getChildren    -< t
  at       <- getAttrValue "at"    -< t
  with     <- getAttrValue "with"  -< t
  -- Load the surrounding template
  outer    <- loadTemplateA         -< with
  -- Insert the original template into the surrounding template
  debugA -< "surrounding with '" ++ with ++ "' at '" ++ at ++ "'"
  bound    <- bindA                -< ([(at, children)], outer)
  processTD interpreteTag'          -< bound

remaining :: HawkArrow XmlTree XmlTree
remaining = proc t -> do
    n  <- arr (ShowXml.xshow . (:[])) -< t
    arrIO warningM -< "There are remaining not replaced hawk-Tag: " ++ n
    s <- returnA -< "True" --arr (fromMaybe "" . lookup "hide_hol" .  environmentOptions) <<< getUserState -< t
    case s of
        "True" -> ignore -< t
        _      -> this   -< t


-- --------------------------------------------------------------------------
-- Helper arrows
-- --------------------------------------------------------------------------

loadTemplateA :: IOStateArrow s String XmlTree
loadTemplateA =  runInLocalURIContext readDoc -- [(a_validate, v_0)]
                 >>>
                 getChildren -- discard root node

readDoc :: IOStateArrow s String XmlTree
readDoc = readFromDocument [(a_parse_html, v_1)] -- >>> propagateNamespaces

-- | lift a IOState Monad in a IOState Arrow
arrM :: (t -> StateT s EnvController a) -> IOSLA (XIOState (RequestEnv, s)) t a
arrM f = proc a -> do
  (env,state) <- getUserState -< ()
  (b,state') <- arrIO (\(a,(e,s)) -> runReaderT (runController (runStateT (f a) s)) e) -< (a, (env,state))
  setUserState -< (env,state')
  returnA -< b


invokeController :: (XmlTree -> StateController [XmlTree]) -> HawkArrow XmlTree (Either Response [XmlTree])
invokeController c = arrM $ runEitherT . c

-- --------------------------------------------------------------------------
-- Head merge
-- --------------------------------------------------------------------------

-- | merge all head Elements in a XmlTree
-- TODO only to the first head, need to delete the others
headMerge :: ArrowXml a => a XmlTree XmlTree
headMerge = headList &&& removeHeadsFromBody
    >>>
    applyA (arr (\(heads,tree) -> constA tree >>> insertHeads heads))

-- | get the content of all head Elements
headList ::  ArrowXml a => a XmlTree [XmlTree]
headList = listA (heads >>> getChildren)
    where
    heads = deep $ isElem >>> hasName "head"

removeHeadsFromBody :: ArrowXml a => a XmlTree XmlTree
removeHeadsFromBody = processTopDown
    $ removeHeads `when` isBody
    where
    isBody = isElem >>> hasName "body"

removeHeads :: ArrowXml a => a XmlTree XmlTree
removeHeads = processTopDown
    $ none `when` isHead
    where
    isHead = isElem >>> hasName "head"

insertHeads :: ArrowXml a => [XmlTree] -> a XmlTree XmlTree
insertHeads h = processTopDown
        $ mkelem "head" [] (map constA h) `when` isHead
        where
        isHead = isElem >>> hasName "head"

-- --------------------------------------------------------------------------
-- Binding
-- --------------------------------------------------------------------------

type BindTree = E.EitherMapTree String [XmlTree]

bindTyped :: XmlTree
    -> BindTree
    -> [(String, [(String, String)])]
    -> [XmlTree]
bindTyped x t _ = runLA (bindTypedA t) x

bindTypedA :: (ArrowChoice a, ArrowXml a)  => BindTree -> a XmlTree XmlTree
bindTypedA t = processTopDown (bindNode `when` (isBind `orElse` isEmbed))
    where
    bindNode = applyA (getBindName >>> arr (helper t))
    helper :: (ArrowChoice a, ArrowXml a) => BindTree -> String -> a XmlTree XmlTree
    helper t' n = case E.lookup n t' of
        Just v -> case v of
            Left  t'' -> applyA (constA t'' >>> unlistA >>> arr bindTypedA)
            Right x  -> constA x >>> unlistA
        Nothing -> this

isBind :: ArrowXml a => a XmlTree XmlTree
isBind = isElem >>> hasQName (hawkQName "bind")

getBindName :: ArrowXml a => a XmlTree String
getBindName = choiceA [isBind :-> getAttrValue "name"
                  ,isEmbed :-> getAttrValue "what"
                  ]
                  
-- Arrow version of bind
bindA :: ArrowXml cat => cat ([(String, [XmlTree])], XmlTree) XmlTree
bindA = applyA $ arr $ \(m, tree) -> constA tree >>> bind m

-- |Inserts the values of a list of (name, value) pairs into a template at the
--  positions defined by bind-Tags.
bind :: (ArrowXml cat) => [(String, [XmlTree])] -> cat XmlTree XmlTree
bind m = processTopDown (replaceFrom m `when` isBind')
  where
  isBind' = isElem >>> hasQName (hawkQName "bind") >>> hasAttrValue "name" (flip elem keys)
  keys = map fst m

lookupA :: Arrow a  => a (String, [(String, [XmlTree])]) [XmlTree]
lookupA = arr $ fromMaybe [] . uncurry lookup

replaceFrom :: (ArrowXml cat) => [(String, [XmlTree])] -> cat XmlTree XmlTree
replaceFrom m = proc t -> do
  name       <- getAttrValue "name" -< t
  unlistA <<< lookupA               -< (name, m)

bindAttribute :: ArrowXml cat => [(String, [XmlTree])] -> cat XmlTree XmlTree
bindAttribute m = processTopDown $ replaceAttribute m `when` hasBindAttr
    where
    hasBindAttr = isElem >>> getQAttrValue0 (hawkQName "bind")

replaceAttribute :: ArrowXml cat => [(String, [XmlTree])] -> cat XmlTree XmlTree
replaceAttribute m = addAttrl (getQAttrValue0 (hawkQName "bind") &&& constA m >>> lookupA >>> unlistA)
    >>>
    removeQAttr (hawkQName "bind")

-- --------------------------------------------------------------------------
-- clearNamespace
-- --------------------------------------------------------------------------

-- | Remove the hawk namespace
clearNamespace :: (ArrowXml a) => a XmlTree XmlTree
clearNamespace = processTopDown (removeAttr ("xmlns:" ++ hawkPrefix) `when` isHtml) -- TODO do not use hawk prefix, use full Namespace
    where isHtml = isElem >>> hasName "html"