heist-extra-0.2.0.0: src/Heist/Extra/TemplateState.hs
{-# LANGUAGE RecordWildCards #-}
module Heist.Extra.TemplateState (
TemplateState,
TemplateName,
emptyTemplateState,
addTemplateFile,
removeTemplateFile,
renderHeistTemplate,
) where
import Control.Monad.Except (MonadError (throwError), runExcept)
import Data.ByteString.Builder (toLazyByteString)
import Data.Default (Default (..))
import Data.HashMap.Strict qualified as HM
import Data.Map.Strict qualified as Map
import Data.Text qualified as T
import GHC.IO.Unsafe (unsafePerformIO)
import Heist qualified as H
import Heist.Internal.Types qualified as HT
import Heist.Interpreted qualified as HI
import System.FilePath (splitExtension)
import Text.XmlHtml qualified as XmlHtml
type TemplateName = ByteString
-- | Holds a set of Heist template files that are importing one another.
data TemplateState = TemplateState (H.HeistState Identity) TemplateErrors
instance Default TemplateState where
def = unsafePerformIO emptyTemplateState
emptyTemplateState :: MonadIO m => m TemplateState
emptyTemplateState = do
let heistCfg :: H.HeistConfig Identity =
H.emptyHeistConfig
& H.hcNamespace (const $ Identity "")
& runIdentity
eSt <- liftIO $ H.initHeist heistCfg
let st = either (error . T.intercalate "," . fmap toText) id eSt
pure $ TemplateState st mempty
getTemplateState :: MonadError Text m => TemplateState -> m (HT.HeistState Identity)
getTemplateState (TemplateState st errs) =
if not $ null errs
then throwError $ showErrors errs
else pure st
addTemplate :: TemplateName -> FilePath -> TemplateState -> Either Text HT.Template -> TemplateState
addTemplate name fp (TemplateState st errs) = \case
Left err ->
TemplateState st (assignError name err errs)
Right doc ->
let newSt = HI.addTemplate name doc (Just fp) st
in TemplateState newSt (clearError name errs)
addTemplateFile ::
HasCallStack =>
-- | Absolute path
FilePath ->
-- | Relative path (to template base)
FilePath ->
-- | Contents of the .tmpl file
ByteString ->
TemplateState ->
TemplateState
addTemplateFile fp (tmplName -> name) s tmplSt =
addTemplate name fp tmplSt $
XmlHtml.parseHTML fp s & \case
Left (toText -> err) ->
Left err
Right XmlHtml.XmlDocument {} ->
Left "Xml unsupported"
Right XmlHtml.HtmlDocument {..} ->
Right docContent
removeTemplate :: HasCallStack => TemplateName -> TemplateState -> TemplateState
removeTemplate name (TemplateState st errs) =
let tpath = H.splitTemplatePath name
newSt = st {HT._templateMap = HM.delete tpath (HT._templateMap st)}
in TemplateState newSt (clearError name errs)
removeTemplateFile :: HasCallStack => FilePath -> TemplateState -> TemplateState
removeTemplateFile (tmplName -> name) = removeTemplate name
tmplName :: HasCallStack => String -> TemplateName
tmplName fp = fromMaybe (error "Not a .tpl file") $ do
let (base, ext) = splitExtension fp
guard $ ext == ".tpl"
pure $ encodeUtf8 base
renderHeistTemplate ::
HasCallStack =>
TemplateName ->
H.Splices (HI.Splice Identity) ->
TemplateState ->
Either Text LByteString
renderHeistTemplate name splices tmplSt =
runExcept $ do
st <- getTemplateState tmplSt
(builder, _mimeType) <-
tryJust ("Unable to render template '" <> decodeUtf8 name <> "'") $
runIdentity $
HI.renderTemplate (HI.bindSplices splices st) name
pure $ toLazyByteString builder
where
-- A 'fromJust' that fails in the 'ExceptT' monad
tryJust :: Monad m => e -> Maybe a -> ExceptT e m a
tryJust e = hoistEither . maybeToRight e
-- | Type to track errors on a per template basis
type TemplateErrors = Map TemplateName Text
showErrors :: TemplateErrors -> Text
showErrors m =
T.intercalate "\n" $
Map.toList m <&> \(k, v) ->
decodeUtf8 k <> ":\n" <> unlines (indent <$> lines v)
where
indent :: Text -> Text
indent s = "\t" <> s
-- | Assign a new error for the given template
assignError :: TemplateName -> Text -> TemplateErrors -> TemplateErrors
assignError =
Map.insert
-- | Indicate that the given template has no errors
clearError :: TemplateName -> TemplateErrors -> TemplateErrors
clearError =
Map.delete