mustache-2.1.4: src/Text/Mustache/Render.hs
{-|
Module : $Header$
Description : Functions for rendering mustache templates.
Copyright : (c) Justus Adam, 2015
License : BSD3
Maintainer : dev@justus.science
Stability : experimental
Portability : POSIX
-}
{-# LANGUAGE OverloadedStrings #-}
module Text.Mustache.Render
(
-- * Substitution
substitute, substituteValue
-- * Checked substitution
, checkedSubstitute, checkedSubstituteValue, SubstitutionError(..)
-- * Working with Context
, Context(..), search, innerSearch
-- * Util
, toString
) where
import Control.Applicative ((<|>))
import Control.Arrow (first, second)
import Control.Monad
import Data.Foldable (for_)
import Data.HashMap.Strict as HM hiding (keys, map)
import Data.Maybe (fromMaybe)
import Data.Scientific (floatingOrInteger)
import Data.Text as T (Text, isSuffixOf, pack, replace,
stripSuffix)
import qualified Data.Vector as V
import Prelude hiding (length, lines, unlines)
import Control.Monad.Writer
import qualified Data.Text as T
import Text.Mustache.Internal
import Text.Mustache.Types
-- | Type of errors we may encounter during substitution.
data SubstitutionError
= VariableNotFound [Key] -- ^ The template contained a variable for which there was no data counterpart in the current context
| InvalidImplicitSectionContextType String -- ^ When substituting an implicit section the current context had an unsubstitutable type
| InvertedImplicitSection -- ^ Inverted implicit sections should never occur
| SectionTargetNotFound [Key] -- ^ The template contained a section for which there was no data counterpart in the current context
| PartialNotFound FilePath -- ^ The template contained a partial for which there was no data counterpart in the current context
| DirectlyRenderedValue Value -- ^ A complex value such as an Object or Array was directly rendered into the template (warning)
deriving (Show)
type Substitution = Writer ([SubstitutionError], [Text])
tellError :: SubstitutionError -> Substitution ()
tellError e = tell ([e], [])
tellSuccess :: Text -> Substitution ()
tellSuccess s = tell ([], [s])
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure.
Equivalent to @substituteValue . toMustache@.
-}
substitute :: ToMustache k => Template -> k -> Text
substitute t = substituteValue t . toMustache
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure and report any errors and warnings encountered during
substitution.
This function always produces results, as in a fully substituted/rendered template,
it never halts on errors. It simply reports them in the first part of the tuple.
Sites with errors are usually substituted with empty string.
The second value in the tuple is a template rendered with errors ignored.
Therefore if you must enforce that there were no errors during substitution
you must check that the error list in the first tuple value is empty.
Equivalent to @checkedSubstituteValue . toMustache@.
-}
checkedSubstitute :: ToMustache k => Template -> k -> ([SubstitutionError], Text)
checkedSubstitute t = checkedSubstituteValue t . toMustache
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure.
-}
substituteValue :: Template -> Value -> Text
substituteValue = (snd .) . checkedSubstituteValue
{-|
Substitutes all mustache defined tokens (or tags) for values found in the
provided data structure and report any errors and warnings encountered during
substitution.
This function always produces results, as in a fully substituted/rendered template,
it never halts on errors. It simply reports them in the first part of the tuple.
Sites with errors are usually substituted with empty string.
The second value in the tuple is a template rendered with errors ignored.
Therefore if you must enforce that there were no errors during substitution
you must check that the error list in the first tuple value is empty.
-}
checkedSubstituteValue :: Template -> Value -> ([SubstitutionError], Text)
checkedSubstituteValue template dataStruct =
second T.concat $ execWriter $ substituteASTWithValAndCache (ast template) (partials template) (Context mempty dataStruct)
substituteASTWithValAndCache :: STree -> TemplateCache -> Context Value -> Substitution ()
substituteASTWithValAndCache cAst cPartials ctx =
mapM_ (substitute' ctx) cAst
where
-- Main substitution function
substitute' :: Context Value -> Node Text -> Substitution ()
-- subtituting text
substitute' _ (TextBlock t) = tellSuccess t
-- substituting a whole section (entails a focus shift)
substitute' (Context parents focus@(Array a)) (Section Implicit secSTree)
| V.null a = return ()
| otherwise = for_ a $ \focus' ->
let
newContext = Context (focus:parents) focus'
in
mapM_ (substitute' newContext) secSTree
substitute' context@(Context _ (Object _)) (Section Implicit secSTree) =
mapM_ (substitute' context) secSTree
substitute' (Context _ v) (Section Implicit _) =
tellError $ InvalidImplicitSectionContextType $ showValueType v
substitute' context@(Context parents focus) (Section (NamedData secName) secSTree) =
case search context secName of
Just arr@(Array arrCont) ->
if V.null arrCont
then return ()
else for_ arrCont $ \focus' ->
let
newContext = Context (arr:focus:parents) focus'
in
mapM_ (substitute' newContext) secSTree
Just (Bool False) -> return ()
Just Null -> return ()
Just (Lambda l) -> mapM_ (substitute' context) (l context secSTree)
Just focus' ->
let
newContext = Context (focus:parents) focus'
in
mapM_ (substitute' newContext) secSTree
Nothing -> tellError $ SectionTargetNotFound secName
-- substituting an inverted section
substitute' _ (InvertedSection Implicit _ ) = tellError InvertedImplicitSection
substitute' context (InvertedSection (NamedData secName) invSecSTree) =
case search context secName of
Just (Bool False) -> contents
Just (Array a) | V.null a -> contents
Nothing -> contents
_ -> return ()
where
contents = mapM_ (substitute' context) invSecSTree
-- substituting a variable
substitute' (Context _ current) (Variable _ Implicit) = toString current >>= tellSuccess
substitute' context (Variable escaped (NamedData varName)) =
maybe
(tellError $ VariableNotFound varName)
(toString >=> tellSuccess . (if escaped then escapeXMLText else id))
$ search context varName
-- substituting a partial
substitute' context (Partial indent pName) =
case HM.lookup pName cPartials of
Nothing -> tellError $ PartialNotFound pName
Just t ->
let ast' = handleIndent indent $ ast t
in substituteASTWithValAndCache ast' (partials t `HM.union` cPartials) context
showValueType :: Value -> String
showValueType Null = "Null"
showValueType (Object _) = "Object"
showValueType (Array _) = "Array"
showValueType (String _) = "String"
showValueType (Lambda _) = "Lambda"
showValueType (Number _) = "Number"
showValueType (Bool _) = "Bool"
handleIndent :: Maybe Text -> STree -> STree
handleIndent Nothing ast' = ast'
handleIndent (Just indentation) ast' = preface <> content
where
preface = if T.null indentation then [] else [TextBlock indentation]
content = if T.null indentation
then ast'
else
let
fullIndented = fmap (indentBy indentation) ast'
dropper (TextBlock t) = TextBlock $
if ("\n" <> indentation) `isSuffixOf` t
then fromMaybe t $ stripSuffix indentation t
else t
dropper a = a
in
reverse $ fromMaybe [] (uncurry (:) . first dropper <$> uncons (reverse fullIndented))
-- | Search for a key in the current context.
--
-- The search is conducted inside out mening the current focus
-- is searched first. If the key is not found the outer scopes are recursively
-- searched until the key is found, then 'innerSearch' is called on the result.
search :: Context Value -> [Key] -> Maybe Value
search _ [] = Nothing
search ctx keys@(_:nextKeys) = go ctx keys >>= innerSearch nextKeys
where
go _ [] = Nothing
go (Context parents focus) val@(x:_) = searchCurrentContext <|> searchParentContext
where
searchCurrentContext = case focus of
(Object o) -> HM.lookup x o
_ -> Nothing
searchParentContext = do
(newFocus, newParents) <- uncons parents
go (Context newParents newFocus) val
indentBy :: Text -> Node Text -> Node Text
indentBy indent p@(Partial (Just indent') name')
| T.null indent = p
| otherwise = Partial (Just (indent <> indent')) name'
indentBy indent (Partial Nothing name') = Partial (Just indent) name'
indentBy indent (TextBlock t) = TextBlock $ replace "\n" ("\n" <> indent) t
indentBy _ a = a
-- | Searches nested scopes navigating inward. Fails if it encunters something
-- other than an object before the key is expended.
innerSearch :: [Key] -> Value -> Maybe Value
innerSearch [] v = Just v
innerSearch (y:ys) (Object o) = HM.lookup y o >>= innerSearch ys
innerSearch _ _ = Nothing
-- | Converts values to Text as required by the mustache standard
toString :: Value -> Substitution Text
toString (String t) = return t
toString (Number n) = return $ either (pack . show) (pack . show) (floatingOrInteger n :: Either Double Integer)
toString e = do
tellError $ DirectlyRenderedValue e
return $ pack $ show e