{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE NoMonomorphismRestriction #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
module Heist.Compiled.Internal where
import Blaze.ByteString.Builder
import Blaze.ByteString.Builder.Char.Utf8
import Control.Arrow
import Control.Exception
import Control.Monad
import Control.Monad.RWS.Strict
import Control.Monad.State.Strict
import qualified Data.Attoparsec.Text as AP
import Data.ByteString (ByteString)
import Data.DList (DList)
import qualified Data.DList as DL
import qualified Data.HashMap.Strict as H
import qualified Data.HashSet as S
import qualified Data.HeterogeneousEnvironment as HE
import Data.Map.Syntax
import Data.Maybe
import Data.Text (Text)
import qualified Data.Text as T
import qualified Data.Text.Encoding as T
import qualified Data.Vector as V
import Text.Printf
import qualified Text.XmlHtml as X
import qualified Text.XmlHtml.HTML.Meta as X
#if !MIN_VERSION_base(4,8,0)
import Data.Foldable (Foldable)
#endif
import qualified Data.Foldable as Foldable
import Heist.Common
import Heist.Internal.Types.HeistState
type Splice n = HeistT n IO (DList (Chunk n))
runChildren :: Monad n => Splice n
runChildren = runNodeList . X.childNodes =<< getParamNode
{-# INLINE runChildren #-}
renderFragment :: Markup -> [X.Node] -> Builder
renderFragment markup ns =
case markup of
Html -> X.renderHtmlFragment X.UTF8 ns
Xml -> X.renderXmlFragment X.UTF8 ns
pureTextChunk :: Text -> Chunk n
pureTextChunk t = Pure $ T.encodeUtf8 t
{-# INLINE pureTextChunk #-}
yieldPure :: Builder -> DList (Chunk n)
yieldPure = DL.singleton . Pure . toByteString
{-# INLINE yieldPure #-}
yieldRuntime :: RuntimeSplice n Builder -> DList (Chunk n)
yieldRuntime = DL.singleton . RuntimeHtml
{-# INLINE yieldRuntime #-}
yieldRuntimeEffect :: Monad n => RuntimeSplice n () -> DList (Chunk n)
yieldRuntimeEffect = DL.singleton . RuntimeAction
{-# INLINE yieldRuntimeEffect #-}
yieldPureText :: Text -> DList (Chunk n)
yieldPureText = DL.singleton . pureTextChunk
{-# INLINE yieldPureText #-}
yieldRuntimeText :: Monad n => RuntimeSplice n Text -> DList (Chunk n)
yieldRuntimeText = yieldRuntime . liftM fromText
{-# INLINE yieldRuntimeText #-}
runNodeList :: Monad n => [X.Node] -> Splice n
runNodeList = mapSplices runNode
runDocumentFile :: Monad n
=> TPath
-> DocumentFile
-> Splice n
runDocumentFile tpath df = do
let markup = case dfDoc df of
X.XmlDocument _ _ _ -> Xml
X.HtmlDocument _ _ _ -> Html
modifyHS (\hs -> hs { _curMarkup = markup })
let inDoctype = X.docType $ dfDoc df
addDoctype $ maybeToList inDoctype
modifyHS (setCurTemplateFile curPath . setCurContext tpath)
res <- runNodeList nodes
dt <- getsHS (listToMaybe . _doctypes)
let enc = X.docEncoding $ dfDoc df
return $! (yieldPure (X.renderDocType enc dt) `mappend` res)
where
curPath = dfFile df
nodes = X.docContent $! dfDoc df
compileTemplate
:: Monad n
=> TPath
-> DocumentFile
-> HeistT n IO [Chunk n]
compileTemplate tpath df = do
!chunks <- runDocumentFile tpath df
return $! consolidate chunks
compileTemplates
:: Monad n
=> (TPath -> Bool)
-> HeistState n
-> IO (Either [String] (HeistState n))
compileTemplates f hs = do
(tmap, hs') <- runHeistT (compileTemplates' f) (X.TextNode "") hs
let pre = _splicePrefix hs'
let canError = _errorNotBound hs'
let errs = _spliceErrors hs'
let nsErr = if not (T.null pre) && (_numNamespacedTags hs' == 0)
then Left [noNamespaceSplicesMsg $ T.unpack pre]
else Right ()
return $ if canError
then case errs of
[] -> nsErr >>
(Right $! hs { _compiledTemplateMap = tmap })
es -> Left $ either (++) (const id) nsErr $
map (T.unpack . spliceErrorText) es
else nsErr >> (Right $! hs { _compiledTemplateMap = tmap
, _spliceErrors = errs
})
noNamespaceSplicesMsg :: String -> String
noNamespaceSplicesMsg pre = unwords
[ printf "You are using a namespace of '%s', but you don't have any" ns
, printf "tags starting with '%s'. If you have not defined any" pre
, "splices, then change your namespace to the empty string to get rid"
, "of this message."
]
where
ns = reverse $ drop 1 $ reverse pre
compileTemplates'
:: Monad n
=> (TPath -> Bool)
-> HeistT n IO (H.HashMap TPath ([Chunk n], MIMEType))
compileTemplates' f = do
hs <- getHS
let tpathDocfiles :: [(TPath, DocumentFile)]
tpathDocfiles = filter (f . fst)
(H.toList $ _templateMap hs)
foldM runOne H.empty tpathDocfiles
where
runOne tmap (tpath, df) = do
modifyHS (\hs -> hs { _doctypes = []})
!mHtml <- compileTemplate tpath df
return $! H.insert tpath (mHtml, mimeType $! dfDoc df) tmap
consolidate :: (Monad n) => DList (Chunk n) -> [Chunk n]
consolidate = consolidateL . DL.toList
where
consolidateL [] = []
consolidateL (y:ys) = boilDown [] $! go [] y ys
where
go soFar x [] = x : soFar
go soFar (Pure a) ((Pure b) : xs) =
go soFar (Pure $! a `mappend` b) xs
go soFar (RuntimeHtml a) ((RuntimeHtml b) : xs) =
go soFar (RuntimeHtml $! a `mappend` b) xs
go soFar (RuntimeHtml a) ((RuntimeAction b) : xs) =
go soFar (RuntimeHtml $! a >>= \x -> b >> return x) xs
go soFar (RuntimeAction a) ((RuntimeHtml b) : xs) =
go soFar (RuntimeHtml $! a >> b) xs
go soFar (RuntimeAction a) ((RuntimeAction b) : xs) =
go soFar (RuntimeAction $! a >> b) xs
go soFar a (b : xs) = go (a : soFar) b xs
boilDown soFar [] = soFar
boilDown soFar ((Pure h) : xs) = boilDown ((Pure $! h) : soFar) xs
boilDown soFar (x : xs) = boilDown (x : soFar) xs
codeGen :: Monad n => DList (Chunk n) -> RuntimeSplice n Builder
codeGen l = V.foldr mappend mempty $!
V.map toAct $! V.fromList $! consolidate l
where
toAct !(RuntimeHtml !m) = m
toAct !(Pure !h) = return $! fromByteString h
toAct !(RuntimeAction !m) = m >> return mempty
{-# INLINE codeGen #-}
lookupSplice :: Text -> HeistT n IO (Maybe (Splice n))
lookupSplice nm = do
pre <- getsHS _splicePrefix
res <- getsHS (H.lookup nm . _compiledSpliceMap)
if isNothing res && T.isPrefixOf pre nm && not (T.null pre)
then do
tellSpliceError $ "No splice bound for " `mappend` nm
return Nothing
else return res
runNode :: Monad n => X.Node -> Splice n
runNode node = localParamNode (const node) $ do
hs <- getHS
let pre = _splicePrefix hs
let hasPrefix = (T.isPrefixOf pre `fmap` X.tagName node) == Just True
when (not (T.null pre) && hasPrefix) incNamespacedTags
hs' <- getHS
(res, hs'') <- liftIO $ catches (compileIO hs')
[ Handler (\(ex :: CompileException) -> throwIO ex)
, Handler (\(ex :: SomeException) -> handleError ex hs')]
putHS hs''
return res
where
localSplicePath =
localHS (\hs -> hs {_splicePath = (_curContext hs,
_curTemplateFile hs,
X.elementTag node):
(_splicePath hs)})
compileIO hs = runHeistT compile node hs
compile = do
isStatic <- subtreeIsStatic node
dl <- compile' isStatic
liftIO $ evaluate $ DL.fromList $! consolidate dl
compile' True = do
markup <- getsHS _curMarkup
return $! yieldPure $! renderFragment markup [parseAttrs node]
compile' False = localSplicePath $ compileNode node
handleError ex hs = do
errs <- evalHeistT (do localSplicePath $ tellSpliceError $ T.pack $
"Exception in splice compile: " ++ show ex
getsHS _spliceErrors) node hs
throwIO $ CompileException ex errs
parseAttrs :: X.Node -> X.Node
parseAttrs (X.Element nm attrs ch) = newAttrs `seq` X.Element nm newAttrs ch
where
newAttrs = map parseAttr attrs
parseAttrs !n = n
parseAttr :: (Text, Text) -> (Text, Text)
parseAttr (k,v) = (k, T.concat $! map cvt ast)
where
!ast = case AP.feed (AP.parse attParser v) "" of
(AP.Done _ res) -> res
(AP.Fail _ _ _) -> []
(AP.Partial _ ) -> []
cvt (Literal x) = x
cvt (Ident i) = T.concat ["${", i, "}"]
subtreeIsStatic :: X.Node -> HeistT n IO Bool
subtreeIsStatic (X.Element nm attrs ch) = do
isNodeDynamic <- liftM isJust $ lookupSplice nm
attrSplices <- getsHS _attrSpliceMap
let hasSubstitutions (k,v) = hasAttributeSubstitutions v ||
H.member k attrSplices
if isNodeDynamic
then return False
else do
let hasDynamicAttrs = any hasSubstitutions attrs
if hasDynamicAttrs
then return False
else do
staticSubtrees <- mapM subtreeIsStatic ch
return $ and staticSubtrees
subtreeIsStatic _ = return True
hasAttributeSubstitutions :: Text -> Bool
hasAttributeSubstitutions txt = any isIdent ast
where
ast = case AP.feed (AP.parse attParser txt) "" of
(AP.Done _ res) -> res
(AP.Fail _ _ _) -> []
(AP.Partial _ ) -> []
compileNode :: Monad n => X.Node -> Splice n
compileNode (X.Element nm attrs ch) = do
msplice <- lookupSplice nm
fromMaybe compileStaticElement msplice
where
tag0 = T.append "<" nm
end = T.concat [ "</" , nm , ">"]
compileStaticElement = do
compiledAttrs <- runAttributes attrs
childHtml <- runNodeList ch
return $! if null (DL.toList childHtml) && nm `S.member` X.voidTags
then DL.concat [ DL.singleton $! pureTextChunk $! tag0
, DL.concat compiledAttrs
, DL.singleton $! pureTextChunk " />"
]
else DL.concat [ DL.singleton $! pureTextChunk $! tag0
, DL.concat compiledAttrs
, DL.singleton $! pureTextChunk ">"
, childHtml
, DL.singleton $! pureTextChunk $! end
]
compileNode _ = error "impossible"
parseAtt :: Monad n => (Text, Text) -> HeistT n IO (DList (Chunk n))
parseAtt (k,v) = do
mas <- getsHS (H.lookup k . _attrSpliceMap)
maybe doInline (return . doAttrSplice) mas
where
cvt (Literal x) = return $ yieldPureText x
cvt (Ident x) =
localParamNode (const $ X.Element x [] []) $ getAttributeSplice x
doInline = do
let ast = case AP.feed (AP.parse attParser v) "" of
(AP.Done _ res) -> res
(AP.Fail _ _ _) -> []
(AP.Partial _ ) -> []
chunks <- mapM cvt ast
let value = DL.concat chunks
return $ attrToChunk k value
doAttrSplice splice = DL.singleton $ RuntimeHtml $ do
res <- splice v
return $ mconcat $ map attrToBuilder res
parseAtt2 :: Monad n
=> (Text, Text)
-> HeistT n IO (RuntimeSplice n [(Text, Text)])
parseAtt2 (k,v) = do
mas <- getsHS (H.lookup k . _attrSpliceMap)
maybe doInline (return . doAttrSplice) mas
where
cvt (Literal x) = return $ return x
cvt (Ident x) =
localParamNode (const $ X.Element x [] []) $ getAttributeSplice2 x
doInline = do
let ast = case AP.feed (AP.parse attParser v) "" of
(AP.Done _ res) -> res
(AP.Fail _ _ _) -> []
(AP.Partial _ ) -> []
chunks <- mapM cvt ast
return $ do
list <- sequence chunks
return [(k, T.concat list)]
doAttrSplice splice = splice v
runAttributes :: Monad n
=> [(Text, Text)]
-> HeistT n IO [DList (Chunk n)]
runAttributes = mapM parseAtt
runAttributesRaw :: Monad n
=> [(Text, Text)]
-> HeistT n IO (RuntimeSplice n [(Text, Text)])
runAttributesRaw attrs = do
arrs <- mapM parseAtt2 attrs
return $ liftM concat $ sequence arrs
attrToChunk :: Text -> DList (Chunk n) -> DList (Chunk n)
attrToChunk !k !v = do
DL.concat
[ DL.singleton $! pureTextChunk $! T.concat [" ", k, "=\""]
, v, DL.singleton $! pureTextChunk "\"" ]
attrToBuilder :: (Text, Text) -> Builder
attrToBuilder (k,v)
| T.null v = mconcat
[ fromText " "
, fromText k
]
| otherwise = mconcat
[ fromText " "
, fromText k
, fromText "=\""
, fromText v
, fromText "\""
]
getAttributeSplice :: Text -> HeistT n IO (DList (Chunk n))
getAttributeSplice name =
lookupSplice name >>= fromMaybe
(return $ DL.singleton $ Pure $ T.encodeUtf8 $
T.concat ["${", name, "}"])
{-# INLINE getAttributeSplice #-}
getAttributeSplice2 :: Monad n => Text -> HeistT n IO (RuntimeSplice n Text)
getAttributeSplice2 name = do
mSplice <- lookupSplice name
case mSplice of
Nothing -> return $ return $ T.concat ["${", name, "}"]
Just splice -> do
res <- splice
return $ liftM (T.decodeUtf8 . toByteString) $ codeGen res
{-# INLINE getAttributeSplice2 #-}
newtype Promise a = Promise (HE.Key a)
getPromise :: (Monad n) => Promise a -> RuntimeSplice n a
getPromise (Promise k) = do
mb <- gets (HE.lookup k)
return $ fromMaybe e mb
where
e = error $ "getPromise: dereferenced empty key (id "
++ show (HE.getKeyId k) ++ ")"
{-# INLINE getPromise #-}
putPromise :: (Monad n) => Promise a -> a -> RuntimeSplice n ()
putPromise (Promise k) x = modify (HE.insert k x)
{-# INLINE putPromise #-}
adjustPromise :: Monad n => Promise a -> (a -> a) -> RuntimeSplice n ()
adjustPromise (Promise k) f = modify (HE.adjust f k)
{-# INLINE adjustPromise #-}
newEmptyPromise :: HeistT n IO (Promise a)
newEmptyPromise = do
keygen <- getsHS _keygen
key <- liftIO $ HE.makeKey keygen
return $! Promise key
{-# INLINE newEmptyPromise #-}
bindSplice :: Text
-> Splice n
-> HeistState n
-> HeistState n
bindSplice n v ts =
ts { _compiledSpliceMap = H.insert n' v (_compiledSpliceMap ts) }
where
n' = _splicePrefix ts `mappend` n
bindSplices :: Splices (Splice n)
-> HeistState n
-> HeistState n
bindSplices ss hs =
hs { _compiledSpliceMap = applySpliceMap hs _compiledSpliceMap ss }
withLocalSplices :: Splices (Splice n)
-> Splices (AttrSplice n)
-> HeistT n IO a
-> HeistT n IO a
withLocalSplices ss as = localHS (bindSplices ss . bindAttributeSplices as)
renderTemplate :: Monad n
=> HeistState n
-> ByteString
-> Maybe (n Builder, MIMEType)
renderTemplate hs nm =
fmap (first (interpret . DL.fromList) . fst) $!
lookupTemplate nm hs _compiledTemplateMap
callTemplate :: Monad n
=> ByteString
-> Splice n
callTemplate nm = do
hs <- getHS
maybe (error err) call $ lookupTemplate nm hs _templateMap
where
err = "callTemplate: "++(T.unpack $ T.decodeUtf8 nm)++(" does not exist")
call (df,_) = localHS (\hs' -> hs' {_curTemplateFile = dfFile df}) $
runNodeList $ X.docContent $ dfDoc df
interpret :: Monad n => DList (Chunk n) -> n Builder
interpret = flip evalStateT HE.empty . unRT . codeGen
textSplice :: (a -> Text) -> a -> Builder
textSplice f = fromText . f
nodeSplice :: (a -> [X.Node]) -> a -> Builder
nodeSplice f = X.renderHtmlFragment X.UTF8 . f
{-# DEPRECATED nodeSplice
"Use xmlNodeSplice or htmlNodeSplice, will be removed in Heist 1.1" #-}
xmlNodeSplice :: (a -> [X.Node]) -> a -> Builder
xmlNodeSplice f = X.renderXmlFragment X.UTF8 . f
htmlNodeSplice :: (a -> [X.Node]) -> a -> Builder
htmlNodeSplice f = X.renderHtmlFragment X.UTF8 . f
pureSplice :: Monad n => (a -> Builder) -> RuntimeSplice n a -> Splice n
pureSplice f n = return $ yieldRuntime (return . f =<< n)
withSplices :: Monad n
=> Splice n
-> Splices (RuntimeSplice n a -> Splice n)
-> RuntimeSplice n a
-> Splice n
withSplices splice splices runtimeAction =
withLocalSplices splices' mempty splice
where
splices' = mapV ($runtimeAction) splices
{-# INLINE foldMapM #-}
foldMapM :: (Monad f, Monoid m, Foldable list)
=> (a -> f m)
-> list a
-> f m
foldMapM f =
Foldable.foldlM (\xs x -> xs `seq` liftM (xs <>) (f x)) mempty
manyWithSplices :: (Foldable f, Monad n)
=> Splice n
-> Splices (RuntimeSplice n a -> Splice n)
-> RuntimeSplice n (f a)
-> Splice n
manyWithSplices splice splices runtimeAction =
manyWith splice splices mempty runtimeAction
manyWith :: (Foldable f, Monad n)
=> Splice n
-> Splices (RuntimeSplice n a -> Splice n)
-> Splices (RuntimeSplice n a -> AttrSplice n)
-> RuntimeSplice n (f a)
-> Splice n
manyWith splice splices attrSplices runtimeAction = do
p <- newEmptyPromise
let splices' = mapV ($ getPromise p) splices
let attrSplices' = mapV ($ getPromise p) attrSplices
chunks <- withLocalSplices splices' attrSplices' splice
return $ yieldRuntime $ do
items <- runtimeAction
foldMapM (\item -> putPromise p item >> codeGen chunks) items
deferMany :: (Foldable f, Monad n)
=> (RuntimeSplice n a -> Splice n)
-> RuntimeSplice n (f a)
-> Splice n
deferMany f getItems = do
promise <- newEmptyPromise
chunks <- f $ getPromise promise
return $ yieldRuntime $ do
items <- getItems
foldMapM (\item -> putPromise promise item >> codeGen chunks) items
defer :: Monad n
=> (RuntimeSplice n a -> Splice n)
-> RuntimeSplice n a -> Splice n
defer pf n = do
p2 <- newEmptyPromise
let action = yieldRuntimeEffect $ putPromise p2 =<< n
res <- pf $ getPromise p2
return $ action `mappend` res
deferMap :: Monad n
=> (a -> RuntimeSplice n b)
-> (RuntimeSplice n b -> Splice n)
-> RuntimeSplice n a -> Splice n
deferMap f pf n = defer pf $ f =<< n
mayDeferMap :: Monad n
=> (a -> RuntimeSplice n (Maybe b))
-> (RuntimeSplice n b -> Splice n)
-> RuntimeSplice n a -> Splice n
mayDeferMap f pf n = deferMany pf $ f =<< n
bindLater :: (Monad n)
=> (a -> RuntimeSplice n Builder)
-> RuntimeSplice n a
-> Splice n
bindLater f p = return $ yieldRuntime $ f =<< p