{-# LANGUAGE BangPatterns #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Heist.Common where
import Control.Applicative (Alternative (..))
import Control.Exception (SomeException)
import qualified Control.Exception.Lifted as C
import Control.Monad (liftM, mplus)
import qualified Data.Attoparsec.Text as AP
import Data.ByteString (ByteString)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BC
import Data.Hashable (Hashable)
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as Map
import Data.List (isSuffixOf)
import Data.Map.Syntax
import Data.Maybe (isJust)
import Data.Monoid ((<>))
import Data.Text (Text)
import qualified Data.Text as T
import Heist.Internal.Types.HeistState
import System.FilePath (pathSeparator)
import qualified Text.XmlHtml as X
#if !MIN_VERSION_base(4,8,0)
import Control.Applicative (Applicative (..), (<$>))
import Data.Monoid (Monoid (..))
#endif
runHashMap
:: Splices s
-> Either [String] (HashMap T.Text s)
runHashMap ms =
case runMapSyntax Map.lookup Map.insert ms of
Left keys -> Left $ map (T.unpack . mkMsg) keys
Right hm -> Right hm
where
mkMsg k = "You tried to bind "<>k<>" more than once!"
runMapNoErrors :: (Eq k, Hashable k) => MapSyntaxM k v a -> HashMap k v
runMapNoErrors = either (const mempty) id .
runMapSyntax' (\_ new _ -> Just new) Map.lookup Map.insert
applySpliceMap :: HeistState n
-> (HeistState n -> HashMap Text v)
-> MapSyntaxM Text v a
-> HashMap Text v
applySpliceMap hs f = (flip Map.union (f hs)) .
runMapNoErrors .
mapK (mappend pre)
where
pre = _splicePrefix hs
orError :: Monad m => HeistT n m b -> String -> HeistT n m b
orError silent msg = do
hs <- getHS
if _preprocessingMode hs
then do fullMsg <- heistErrMsg (T.pack msg)
error $ T.unpack fullMsg
else silent
heistErrMsg :: Monad m => Text -> HeistT n m Text
heistErrMsg msg = do
tf <- getsHS _curTemplateFile
return $ (maybe "" ((`mappend` ": ") . T.pack) tf) `mappend` msg
tellSpliceError :: Monad m => Text -> HeistT n m ()
tellSpliceError msg = do
hs <- getHS
node <- getParamNode
let spliceError = SpliceError
{ spliceHistory = _splicePath hs
, spliceTemplateFile = _curTemplateFile hs
, visibleSplices = Map.keys $ _compiledSpliceMap hs
, contextNode = node
, spliceMsg = msg
}
modifyHS (\hs' -> hs { _spliceErrors = spliceError : _spliceErrors hs' })
showTPath :: TPath -> String
showTPath = BC.unpack . (`BC.append` ".tpl") . tpathName
tpathName :: TPath -> ByteString
tpathName = BC.intercalate "/" . reverse
setCurTemplateFile :: Maybe FilePath -> HeistState n -> HeistState n
setCurTemplateFile Nothing ts = ts
setCurTemplateFile fp ts = ts { _curTemplateFile = fp }
setCurContext :: TPath -> HeistState n -> HeistState n
setCurContext tp ts = ts { _curContext = tp }
attParser :: AP.Parser [AttAST]
attParser = liftM ($! []) (loop id)
where
append !dl !x = dl . (x:)
loop !dl = go id
where
finish subDL = let !txt = T.concat $! subDL []
lit = Literal $! T.concat $! subDL []
in return $! if T.null txt
then dl
else append dl lit
go !subDL = (gobbleText >>= go . append subDL)
<|> (AP.endOfInput *> finish subDL)
<|> (do
idp <- identParser
dl' <- finish subDL
loop $! append dl' idp)
gobbleText = AP.takeWhile1 (AP.notInClass "$")
identParser = AP.char '$' *> (ident <|> return (Literal "$"))
ident = (AP.char '{' *> (Ident <$> AP.takeWhile (/='}')) <* AP.string "}")
splitPathWith :: Char -> ByteString -> TPath
splitPathWith s p = if BC.null p then [] else (reverse $ BC.split s path)
where
path = if BC.head p == s then BC.tail p else p
splitLocalPath :: ByteString -> TPath
splitLocalPath = splitPathWith pathSeparator
splitTemplatePath :: ByteString -> TPath
splitTemplatePath = splitPathWith '/'
lookupTemplate :: ByteString
-> HeistState n
-> (HeistState n -> HashMap TPath t)
-> Maybe (t, TPath)
lookupTemplate nameStr ts tm = f (tm ts) path name
where
(name:p) = case splitTemplatePath nameStr of
[] -> [""]
ps -> ps
ctx = if B.isPrefixOf "/" nameStr then [] else _curContext ts
path = p ++ ctx
f = if '/' `BC.elem` nameStr
then singleLookup
else traversePath
hasTemplate :: ByteString -> HeistState n -> Bool
hasTemplate nameStr ts =
isJust $ lookupTemplate nameStr ts _templateMap
singleLookup :: (Eq a, Hashable a)
=> HashMap [a] t -> [a] -> a -> Maybe (t, [a])
singleLookup tm path name = fmap (\a -> (a,path)) $ Map.lookup (name:path) tm
traversePath :: (Eq a, Hashable a)
=> HashMap [a] t -> [a] -> a -> Maybe (t, [a])
traversePath tm [] name = fmap (\a -> (a,[])) (Map.lookup [name] tm)
traversePath tm path name =
singleLookup tm path name `mplus`
traversePath tm (tail path) name
mapSplices :: (Monad m, Monoid b)
=> (a -> m b)
-> [a]
-> m b
mapSplices f vs = liftM mconcat $ mapM f vs
{-# INLINE mapSplices #-}
getContext :: Monad m => HeistT n m TPath
getContext = getsHS _curContext
getTemplateFilePath :: Monad m => HeistT n m (Maybe FilePath)
getTemplateFilePath = getsHS _curTemplateFile
loadTemplate :: String
-> String
-> IO [Either String (TPath, DocumentFile)]
loadTemplate templateRoot fname = do
c <- loadTemplate' fname
return $ map (fmap (\t -> (splitLocalPath $ BC.pack tName, t))) c
where
isHTMLTemplate = ".tpl" `isSuffixOf` fname
correction = if last templateRoot == '/' then 0 else 1
extLen = if isHTMLTemplate then 4 else 5
tName = drop ((length templateRoot)+correction) $
take ((length fname) - extLen) fname
loadTemplate' :: String -> IO [Either String DocumentFile]
loadTemplate' fullDiskPath
| isHTMLTemplate = liftM (:[]) $ getDoc fullDiskPath
| isXMLTemplate = liftM (:[]) $ getXMLDoc fullDiskPath
| otherwise = return []
where
isHTMLTemplate = ".tpl" `isSuffixOf` fullDiskPath
isXMLTemplate = ".xtpl" `isSuffixOf` fullDiskPath
type ParserFun = String -> ByteString -> Either String X.Document
getDocWith :: ParserFun -> String -> IO (Either String DocumentFile)
getDocWith parser f = do
bs <- C.catch (liftM Right $ B.readFile f)
(\(e::SomeException) -> return $ Left $ show e)
let eitherDoc = either Left (parser f) bs
return $ either (\s -> Left $ f ++ " " ++ s)
(\d -> Right $ DocumentFile d (Just f)) eitherDoc
getDoc :: String -> IO (Either String DocumentFile)
getDoc = getDocWith X.parseHTML
getXMLDoc :: String -> IO (Either String DocumentFile)
getXMLDoc = getDocWith X.parseXML
setTemplates :: HashMap TPath DocumentFile -> HeistState n -> HeistState n
setTemplates m ts = ts { _templateMap = m }
insertTemplate :: TPath
-> DocumentFile
-> HeistState n
-> HeistState n
insertTemplate p t st =
setTemplates (Map.insert p t (_templateMap st)) st
mimeType :: X.Document -> MIMEType
mimeType d = case d of
(X.HtmlDocument e _ _) -> "text/html;charset=" `BC.append` enc e
(X.XmlDocument e _ _) -> "text/xml;charset=" `BC.append` enc e
where
enc X.UTF8 = "utf-8"
enc X.UTF16BE = "utf-16"
enc X.UTF16LE = "utf-16"
enc X.ISO_8859_1 = "iso-8859-1"
bindAttributeSplices :: Splices (AttrSplice n)
-> HeistState n
-> HeistState n
bindAttributeSplices ss hs =
hs { _attrSpliceMap = applySpliceMap hs _attrSpliceMap ss }
addDoctype :: Monad m => [X.DocType] -> HeistT n m ()
addDoctype dt = do
modifyHS (\s -> s { _doctypes = _doctypes s `mappend` dt })