stache-2.4.0: Text/Mustache/Render.hs
{-# LANGUAGE OverloadedStrings #-}
-- |
-- Module : Text.Mustache.Render
-- Copyright : © 2016–present Mark Karpov
-- License : BSD 3 clause
--
-- Maintainer : Mark Karpov <markkarpov92@gmail.com>
-- Stability : experimental
-- Portability : portable
--
-- Functions for rendering Mustache templates. You don't usually need to
-- import the module, because "Text.Mustache" re-exports everything you may
-- need, import that module instead.
module Text.Mustache.Render
( renderMustache,
renderMustacheW,
)
where
import Control.Monad (forM_, unless, when)
import Data.Aeson hiding (Key)
import Data.Aeson.Key qualified as Aeson.Key
import Data.Aeson.KeyMap qualified as Aeson.KeyMap
import Data.Foldable (asum)
import Data.List (tails)
import Data.List.NonEmpty qualified as NE
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Builder qualified as TLB
import Data.Text.Lazy.Encoding qualified as TL
import Data.Vector qualified as V
import Text.Mustache.Render.Internal
import Text.Mustache.Type
----------------------------------------------------------------------------
-- High-level interface
-- | Render a Mustache 'Template' using Aeson's 'Value' to get actual values
-- for interpolation.
renderMustache :: Template -> Value -> TL.Text
renderMustache t = snd . renderMustacheW t
-- | Like 'renderMustache', but also returns a collection of warnings.
--
-- @since 1.1.1
renderMustacheW :: Template -> Value -> ([MustacheWarning], TL.Text)
renderMustacheW template value =
runR (renderTemplate (templateActual template)) value (templateCache template)
-- | Main template rendering function.
renderTemplate :: PName -> R ()
renderTemplate name = do
mns <- lookupTemplate name
case mns of
Just ns -> mapM_ renderNode ns
Nothing -> return ()
----------------------------------------------------------------------------
-- Node rendering
-- | Render a single 'Node'.
renderNode :: Node -> R ()
renderNode (TextBlock text) = txt IndentEveryLine text
renderNode (EscapedVar k) =
lookupKey k >>= renderValue k >>= txt IndentOnlyFirstLine . escapeHtml
renderNode (UnescapedVar k) =
lookupKey k >>= renderValue k >>= txt IndentOnlyFirstLine
renderNode (Section k ns) = do
val <- lookupKey k
unless (isBlank val) $
addPrefix k $
case val of
Array xs ->
forM_ (V.toList xs) $ \x ->
addContext x (mapM_ renderNode ns)
_ ->
addContext val (mapM_ renderNode ns)
renderNode (InvertedSection k ns) = do
val <- lookupKey k
when (isBlank val) $
mapM_ renderNode ns
renderNode (Partial pname mindent) = do
mns <- lookupTemplate pname
let adjustIndent = case mindent of
Nothing -> id
Just indent -> incrementIndentBy indent
adjustIndent . resetPrefix $ case mns of
Nothing -> txt IndentEveryLine ""
Just ns -> mapM_ renderNode ns
----------------------------------------------------------------------------
-- Value lookup
-- | Lookup a 'Value' by its 'Key'.
lookupKey :: Key -> R Value
lookupKey (Key []) = NE.head <$> getContext
lookupKey k = do
context <- getContext
prefix <- getPrefix
let f x = asum (simpleLookup False (x <> k) <$> context)
case asum (fmap (f . Key) . reverse . tails $ unKey prefix) of
Nothing ->
Null <$ addWarning (MustacheVariableNotFound (prefix <> k))
Just r ->
return r
-- | Lookup a 'Value' by traversing another 'Value' using given 'Key' as "path".
simpleLookup ::
-- | At least one part of the path matched, in this case we are
-- “committed” to this lookup and cannot say “there is nothing, try
-- other level”. This is necessary to pass the “Dotted Names—Context
-- Precedence” test from the “interpolation.yml” spec.
Bool ->
-- | The key to lookup
Key ->
-- | Source value
Value ->
-- | Looked-up value
Maybe Value
simpleLookup _ (Key []) obj = return obj
simpleLookup c (Key (k : ks)) (Object m) =
case Aeson.KeyMap.lookup (Aeson.Key.fromText k) m of
Nothing -> if c then Just Null else Nothing
Just v -> simpleLookup True (Key ks) v
simpleLookup _ _ _ = Nothing
----------------------------------------------------------------------------
-- Helper functions
-- | Select invisible values.
isBlank :: Value -> Bool
isBlank Null = True
isBlank (Bool False) = True
isBlank (Object m) = Aeson.KeyMap.null m
isBlank (Array a) = V.null a
isBlank (String s) = T.null s
isBlank _ = False
-- | Render Aeson's 'Value' /without/ HTML escaping.
renderValue :: Key -> Value -> R Text
renderValue k v =
case v of
Null -> return ""
String str -> return str
Object _ -> do
addWarning (MustacheDirectlyRenderedValue k)
return $ TL.toStrict $ TL.decodeUtf8 $ encode v
Array _ -> do
addWarning (MustacheDirectlyRenderedValue k)
return $ TL.toStrict $ TL.decodeUtf8 $ encode v
_ -> return $ TL.toStrict $ TL.decodeUtf8 $ encode v
-- | Escape HTML represented as strict 'Text' in a single pass.
escapeHtml :: Text -> Text
escapeHtml = TL.toStrict . TLB.toLazyText . T.foldl' escapeChar mempty
where
escapeChar :: TLB.Builder -> Char -> TLB.Builder
escapeChar acc c =
acc <> case c of
'&' -> TLB.fromText "&"
'<' -> TLB.fromText "<"
'>' -> TLB.fromText ">"
'"' -> TLB.fromText """
'\'' -> TLB.fromText "'"
_ -> TLB.singleton c