{-# 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 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