doctemplates-0.6: src/Text/DocTemplates/Internal.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveFunctor #-}
{- |
Module : Text.DocTemplates.Internal
Copyright : Copyright (C) 2009-2019 John MacFarlane
License : BSD3
Maintainer : John MacFarlane <jgm@berkeley.edu>
Stability : alpha
Portability : portable
-}
module Text.DocTemplates.Internal
( renderTemplate
, TemplateMonad(..)
, Context(..)
, Val(..)
, ToContext(..)
, FromContext(..)
, TemplateTarget(..)
, Template(..)
, Variable(..)
) where
import Safe (lastMay, initDef)
import Data.Aeson (Value(..), ToJSON(..), FromJSON(..), Result(..), fromJSON)
import Control.Monad.Identity
import qualified Data.Text as T
import qualified Data.Text.Lazy as TL
import qualified Data.Text.IO as TIO
import qualified Text.DocLayout as DL
import Data.String (IsString(..))
import Data.Data (Data)
import Data.Typeable (Typeable)
import GHC.Generics (Generic)
import Data.Text (Text)
import qualified Data.Map as M
import qualified Data.HashMap.Strict as H
import qualified Data.Vector as V
import Data.Scientific (floatingOrInteger)
import Data.List (intersperse, intercalate)
#if MIN_VERSION_base(4,11,0)
#else
import Data.Semigroup
#endif
-- | A template.
data Template =
Interpolate Variable
| Conditional Variable Template Template
| Iterate Variable Template Template
| Nested Int Template
| Partial Template
| Literal Text
| Concat Template Template
| BreakingSpace
| Empty
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
instance Semigroup Template where
x <> Empty = x
Empty <> x = x
x <> y = Concat x y
instance Monoid Template where
mappend = (<>)
mempty = Empty
-- | A variable which may have several parts (@foo.bar.baz@).
newtype Variable = Variable { unVariable :: [Text] }
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
instance Semigroup Variable where
Variable xs <> Variable ys = Variable (xs <> ys)
instance Monoid Variable where
mempty = Variable []
mappend = (<>)
-- | A type to which templates can be rendered.
class Monoid a => TemplateTarget a where
fromText :: Text -> a
toText :: a -> Text
removeFinalNewline :: a -> a
isEmpty :: a -> Bool
indent :: Int -> a -> a
breakingSpace :: a
instance TemplateTarget Text where
fromText = id
toText = id
removeFinalNewline t =
case T.unsnoc t of
Just (t', '\n') -> t'
_ -> t
isEmpty = T.null
indent 0 = id
indent ind = T.intercalate ("\n" <> T.replicate ind " ") . T.lines
breakingSpace = " "
instance TemplateTarget TL.Text where
fromText = TL.fromStrict
toText = TL.toStrict
removeFinalNewline t =
case TL.unsnoc t of
Just (t', '\n') -> t'
_ -> t
isEmpty = TL.null
indent 0 = id
indent ind = TL.intercalate ("\n" <> TL.replicate (fromIntegral ind) " ")
. TL.lines
breakingSpace = " "
instance TemplateTarget String where
fromText = T.unpack
toText = T.pack
removeFinalNewline t =
case lastMay t of
Just '\n' -> initDef t t
_ -> t
isEmpty = null
indent 0 = id
indent ind = intercalate ("\n" <> replicate ind ' ') . lines
breakingSpace = " "
instance (DL.HasChars a, IsString a, Eq a)
=> TemplateTarget (DL.Doc a) where
fromText = DL.text . T.unpack
toText = T.pack . DL.foldrChar (:) [] . DL.render Nothing
removeFinalNewline = DL.chomp
indent = DL.nest
isEmpty (DL.Empty) = True
isEmpty (DL.Text 0 _) = True
isEmpty (DL.Concat x y) = isEmpty x && isEmpty y
isEmpty _ = False
breakingSpace = DL.space
-- | A 'Context' defines values for template's variables.
newtype Context a = Context { unContext :: M.Map Text (Val a) }
deriving (Show, Semigroup, Monoid, Traversable, Foldable, Functor)
-- | A variable value.
data Val a =
SimpleVal a
| ListVal [Val a]
| MapVal (Context a)
| NullVal
deriving (Show, Traversable, Foldable, Functor)
-- | The 'ToContext' class provides automatic conversion to
-- a 'Context' or 'Val'.
class ToContext a b where
toContext :: b -> Context a
toContext x = case toVal x of
MapVal c -> c
_ -> mempty
toVal :: b -> Val a
instance TemplateTarget a => ToContext a (Context a) where
toContext = id
toVal = MapVal
instance TemplateTarget a => ToContext a (Val a) where
toVal = id
instance TemplateTarget a => ToContext a a where
toVal = SimpleVal
-- This is needed because otherwise the compiler tries to
-- match on ToContext a [b], with a = b = Char, even though
-- we don't have ToContext Char Char. I don't understand why.
instance {-# OVERLAPS #-} ToContext String String where
toVal t = SimpleVal t
instance ToContext a b => ToContext a [b] where
toVal = ListVal . map toVal
instance TemplateTarget a => ToContext a Value where
toContext x = case fromJSON x of
Success y -> y
Error _ -> mempty
toVal x = case fromJSON x of
Success y -> y
Error _ -> NullVal
instance TemplateTarget a => ToContext a Bool where
toVal True = SimpleVal $ fromText "true"
toVal False = NullVal
instance (TemplateTarget a, DL.HasChars a) => ToContext (DL.Doc a) a where
toVal t = SimpleVal $ DL.Text (DL.realLength t) t
-- | The 'FromContext' class provides functions for extracting
-- values from 'Val' and 'Context'.
class FromContext a b where
fromVal :: Val a -> Maybe b
lookupContext :: Text -> Context a -> Maybe b
lookupContext t (Context m) = M.lookup t m >>= fromVal
instance TemplateTarget a => FromContext a (Val a) where
fromVal = Just
instance TemplateTarget a => FromContext a a where
fromVal (SimpleVal x) = Just x
fromVal _ = Nothing
-- This is needed because otherwise the compiler tries to
-- match on FromContext a [b], with a = b = Char, even though
-- we don't have FromContext Char Char. I don't understand why.
instance {-# OVERLAPS #-} FromContext String String where
fromVal (SimpleVal x) = Just x
fromVal _ = Nothing
instance FromContext a b => FromContext a [b] where
fromVal (ListVal xs) = mapM fromVal xs
fromVal x = sequence [fromVal x]
instance TemplateTarget a => FromJSON (Context a) where
parseJSON v = do
val <- parseJSON v
case val of
MapVal o -> return o
_ -> fail "Not a MapVal"
instance TemplateTarget a => FromJSON (Val a) where
parseJSON v =
case v of
Array vec -> ListVal <$> mapM parseJSON (V.toList vec)
String t -> return $ SimpleVal $ fromText t
Number n -> return $ SimpleVal $ fromText . fromString $
case floatingOrInteger n of
Left (r :: Double) -> show r
Right (i :: Integer) -> show i
Bool True -> return $ SimpleVal $ fromText "true"
Object o -> MapVal . Context . M.fromList . H.toList <$>
mapM parseJSON o
_ -> return NullVal
instance TemplateTarget a => ToJSON (Context a) where
toJSON (Context m) = toJSON m
instance TemplateTarget a => ToJSON (Val a) where
toJSON NullVal = Null
toJSON (MapVal m) = toJSON m
toJSON (ListVal xs) = toJSON xs
toJSON (SimpleVal t) = toJSON $ toText t
multiLookup :: [Text] -> Val a -> Val a
multiLookup [] x = x
multiLookup (t:vs) (MapVal (Context o)) =
case M.lookup t o of
Nothing -> NullVal
Just v' -> multiLookup vs v'
multiLookup _ _ = NullVal
resolveVariable :: TemplateTarget a => Variable -> Context a -> [a]
resolveVariable v ctx = resolveVariable' v (MapVal ctx)
resolveVariable' :: TemplateTarget a => Variable -> Val a -> [a]
resolveVariable' v val =
case multiLookup (unVariable v) val of
ListVal xs -> concatMap (resolveVariable' mempty) xs
SimpleVal t
| isEmpty t -> []
| otherwise -> [removeFinalNewline t]
MapVal _ -> [fromText "true"]
NullVal -> []
withVariable :: TemplateTarget a
=> Variable -> Context a -> (Context a -> a) -> [a]
withVariable v ctx f =
case multiLookup (unVariable v) (MapVal ctx) of
NullVal -> mempty
ListVal xs -> map (\iterval -> f $
Context $ M.insert "it" iterval $ unContext ctx) xs
val' -> [f $ Context $ M.insert "it" val' $ unContext ctx]
-- | Render a compiled template in a "context" which provides
-- values for the template's variables.
renderTemplate :: (TemplateTarget a, ToContext a b)
=> Template -> b -> a
renderTemplate t = renderTemp t . toContext
renderTemp :: forall a . TemplateTarget a
=> Template -> Context a -> a
renderTemp (Literal t) _ = fromText t
renderTemp BreakingSpace _ = breakingSpace
renderTemp (Interpolate v) ctx = mconcat $ resolveVariable v ctx
renderTemp (Conditional v ift elset) ctx =
let res = resolveVariable v ctx
in case res of
[] -> renderTemp elset ctx
_ -> renderTemp ift ctx
renderTemp (Iterate v t sep) ctx =
let sep' = renderTemp sep ctx
in mconcat . intersperse sep' $ withVariable v ctx (renderTemp t)
renderTemp (Nested n t) ctx = indent n $ renderTemp t ctx
renderTemp (Partial t) ctx = renderTemp t ctx
renderTemp (Concat t1 t2) ctx =
mappend (renderTemp t1 ctx) (renderTemp t2 ctx)
renderTemp Empty _ = mempty
-- | A 'TemplateMonad' defines a function to retrieve a partial
-- (from the file system, from a database, or using a default
-- value).
class Monad m => TemplateMonad m where
getPartial :: FilePath -> m Text
instance TemplateMonad Identity where
getPartial _ = return mempty
instance TemplateMonad IO where
getPartial = TIO.readFile