hackage-server-0.5.0: Distribution/Server/Framework/Templating.hs
-----------------------------------------------------------------------------
-- |
-- Module : Distribution.Server.Framework.Templating
-- Copyright : (c) Duncan Coutts 2013
-- License : BSD-like
--
-- Maintainer : duncan@community.haskell.org
--
-- Support for templates, html and text, based on @HStringTemplate@ package.
-----------------------------------------------------------------------------
{-# LANGUAGE OverloadedStrings, RankNTypes #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Distribution.Server.Framework.Templating (
Template,
renderTemplate,
Templates,
TemplatesMode(..),
loadTemplates,
reloadTemplates,
getTemplate,
tryGetTemplate,
TemplateAttr,
($=),
TemplateVal,
templateDict,
templateVal,
templateEnumDesriptor,
) where
import Text.StringTemplate
import Text.StringTemplate.Classes
import Happstack.Server (ToMessage(..), toResponseBS)
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy.Char8 as LBS
--TODO: switch to bytestring builder, once we can depend on bytestring-0.10
--import qualified Data.ByteString.Lazy.Builder as Builder
--import Data.ByteString.Lazy.Builder (Builder)
import Blaze.ByteString.Builder (Builder)
import qualified Blaze.ByteString.Builder as Builder
import qualified Blaze.ByteString.Builder.Html.Utf8 as Builder
import qualified Text.XHtml.Strict as XHtml
import Network.URI (URI)
import qualified Data.Aeson as JSON
import Distribution.Package (PackageName, PackageIdentifier)
import Distribution.Version (Version)
import Distribution.Text (display)
import Control.Monad (when)
import Control.Monad.Trans (MonadIO, liftIO)
import Data.List
import Data.Maybe (isJust)
import Data.IORef
import System.FilePath ((<.>), takeExtension)
import qualified Data.Map as Map
type RawTemplate = StringTemplate Builder
type RawTemplateGroup = STGroup Builder
data Template = Template !TemplateKind !RawTemplate
data TemplateKind = TextTemplate | HtmlTemplate | XmlTemplate | OtherTemplate
renderTemplate :: Template -> LBS.ByteString
renderTemplate (Template _ st) = Builder.toLazyByteString (render st)
instance ToMessage Template where
toResponse t@(Template tkind _) =
toResponseBS (templateContentType tkind) (renderTemplate t)
newtype TemplateAttr = TemplateAttr (RawTemplate -> RawTemplate)
infix 0 $=
($=) :: ToSElem a => String -> a -> TemplateAttr
($=) k v = TemplateAttr (setAttribute k v)
newtype TemplateVal = TemplateVal (forall b. Stringable b => SElem b)
instance ToSElem TemplateVal where
toSElem (TemplateVal se) = se
templateDict :: [(String, TemplateVal)] -> TemplateVal
templateDict kvs =
TemplateVal (SM (Map.fromList (fmap (\(k, TemplateVal v) -> (k, v)) kvs)))
templateVal :: ToSElem a => String -> a -> (String, TemplateVal)
templateVal k v = (k, TemplateVal (toSElem v))
-- | Helper to make it easier to construct forms that use enumeration types
templateEnumDesriptor :: (Eq a, JSON.ToJSON a) => (a -> String) ->
[a] -> a -> [TemplateVal]
templateEnumDesriptor tostr xs x =
[ templateDict
[ templateVal "index" i
, templateVal "selected" (x' == x)
, templateVal "asstring" (tostr x')
, templateVal "asjson" (JSON.encode x')
]
| (i, x') <- zip [0::Int ..] xs ]
instance ToSElem XHtml.Html where
-- The use of SBLE here is to prevent the html being escaped
toSElem = SBLE . stFromString . XHtml.showHtmlFragment
instance ToSElem URI where toSElem = toSElem . show
instance ToSElem PackageName where toSElem = toSElem . display
instance ToSElem Version where toSElem = toSElem . display
instance ToSElem PackageIdentifier where toSElem = toSElem . display
data Templates = TemplatesNormalMode !(IORef RawTemplateGroup)
[FilePath] [String]
| TemplatesDesignMode [FilePath] [String]
data TemplatesMode = NormalMode | DesignMode
loadTemplates :: TemplatesMode -> [FilePath] -> [String] -> IO Templates
loadTemplates templateMode templateDirs expectedTemplates = do
templateGroup <- loadTemplateGroup templateDirs
checkTemplates templateGroup templateDirs expectedTemplates
case templateMode of
NormalMode -> do
templateGroupRef <- newIORef templateGroup
return (TemplatesNormalMode templateGroupRef
templateDirs expectedTemplates)
DesignMode ->
return (TemplatesDesignMode templateDirs expectedTemplates)
reloadTemplates :: Templates -> IO ()
reloadTemplates (TemplatesNormalMode templateGroupRef
templateDirs expectedTemplates) = do
templateGroup' <- loadTemplateGroup templateDirs
checkTemplates templateGroup' templateDirs expectedTemplates
writeIORef templateGroupRef templateGroup'
reloadTemplates (TemplatesDesignMode _ _) = return ()
getTemplate :: MonadIO m => Templates -> String -> m ([TemplateAttr] -> Template)
getTemplate templates@(TemplatesNormalMode _ _ expectedTemplates) name = do
when (name `notElem` expectedTemplates) $ failMissingTemplate name
tryGetTemplate templates name >>= maybe (failMissingTemplate name) return
getTemplate templates@(TemplatesDesignMode _ expectedTemplates) name = do
when (name `notElem` expectedTemplates) $ failMissingTemplate name
tryGetTemplate templates name >>= maybe (failMissingTemplate name) return
tryGetTemplate :: MonadIO m => Templates -> String -> m (Maybe ([TemplateAttr] -> Template))
tryGetTemplate (TemplatesNormalMode templateGroupRef _ _) name = do
templateGroup <- liftIO $ readIORef templateGroupRef
let tkind = templateKindFromExt name
mtemplate = fmap (\t -> Template tkind
. applyEscaping tkind
. applyTemplateAttrs t)
(getStringTemplate name templateGroup)
return mtemplate
tryGetTemplate (TemplatesDesignMode templateDirs expectedTemplates) name = do
templateGroup <- liftIO $ loadTemplateGroup templateDirs
checkTemplates templateGroup templateDirs expectedTemplates
let tkind = templateKindFromExt name
mtemplate = fmap (\t -> Template tkind
. applyEscaping tkind
. applyTemplateAttrs t)
(getStringTemplate name templateGroup)
return mtemplate
templateKindFromExt :: FilePath -> TemplateKind
templateKindFromExt tname =
case takeExtension tname of
".txt" -> TextTemplate
".html" -> HtmlTemplate
".xml" -> XmlTemplate
_ -> OtherTemplate
applyEscaping :: TemplateKind -> RawTemplate -> RawTemplate
applyEscaping TextTemplate = id
applyEscaping HtmlTemplate = setEncoder escapeHtml
applyEscaping XmlTemplate = setEncoder escapeHtml -- ok to reuse
applyEscaping OtherTemplate = id
escapeHtml :: Builder -> Builder
escapeHtml = Builder.fromHtmlEscapedString
. LBS.unpack
. Builder.toLazyByteString
templateContentType :: TemplateKind -> BS.ByteString
templateContentType TextTemplate = "text/plain; charset=utf-8"
templateContentType HtmlTemplate = "text/html; charset=utf-8"
templateContentType XmlTemplate = "application/xml"
templateContentType OtherTemplate = "text/plain"
applyTemplateAttrs :: RawTemplate -> [TemplateAttr] -> RawTemplate
applyTemplateAttrs = foldl' (\t' (TemplateAttr a) -> a t')
failMissingTemplate :: Monad m => String -> m a
failMissingTemplate name =
fail $ "getTemplate: request for unexpected template " ++ name
++ ". So we can do load-time checking, all templates used "
++ "must be listed in the call to loadTemplates."
loadTemplateGroup :: [FilePath] -> IO RawTemplateGroup
loadTemplateGroup [] = return nullGroup
loadTemplateGroup templateDirs = do
templateGroup <- mapM directoryGroup templateDirs
-- `catchJust` IOError
return (foldr1 (flip addSuperGroup) templateGroup)
checkTemplates :: Monad m => RawTemplateGroup -> [FilePath] -> [String] -> m ()
checkTemplates templateGroup templateDirs expectedTemplates = do
let checks = [ (t, fmap checkTemplate
(getStringTemplate t templateGroup))
| t <- expectedTemplates ]
missing = [ t | (t,Nothing) <- checks ]
problems = [ (t, p) | (t,Just p@(es,_ma,mt)) <- checks
, isJust es || {-isJust ma ||-} isJust mt ]
when (not (null missing)) $
fail $ "Missing template files: " ++ intercalate ", " (map (<.> "st") missing)
++ ". Search path was: " ++ intercalate " " templateDirs
when (not (null problems)) $
fail $ reportTemplateProblems problems
where
reportTemplateProblems :: [(String, (Maybe String, Maybe [String], Maybe [String]))] -> String
reportTemplateProblems problems =
unlines
[ "Problem with template " ++ t ++ ":\n"
++ formatTemplateProblem p
| (t, p) <- problems ]
formatTemplateProblem :: (Maybe String, Maybe [String], Maybe [String]) -> String
formatTemplateProblem (Just e, _ma, _mt) = e
formatTemplateProblem (_es, _ma, Just mt) =
"References to missing templates: " ++ intercalate ", " mt
formatTemplateProblem _ = "Unknown error with template"