doctemplates-0.10: src/Text/DocTemplates/Internal.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE ConstraintKinds #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE DeriveDataTypeable #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveTraversable #-}
{- |
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(..)
, Pipe(..)
, Alignment(..)
, Border(..)
) where
import Data.Text.Conversions (FromText(..), ToText(..))
import Data.Aeson (Value(..), ToJSON(..), FromJSON(..), Result(..), fromJSON)
import Data.YAML (ToYAML(..), FromYAML(..), Node(..), Scalar(..))
import Control.Monad.Identity
import qualified Control.Monad.State.Strict as S
import Data.Char (chr, ord)
import Data.Maybe (fromMaybe)
import qualified Data.Text.Read as T
import qualified Data.Text as T
import qualified Data.Text.IO as TIO
import Text.DocLayout (Doc, HasChars)
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)
#if MIN_VERSION_base(4,11,0)
#else
import Data.Semigroup
#endif
-- | A template.
data Template a =
Interpolate Variable
| Conditional Variable (Template a) (Template a)
| Iterate Variable (Template a) (Template a)
| Nested (Template a)
| Partial [Pipe] (Template a)
| Literal (Doc a)
| Concat (Template a) (Template a)
| Empty
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord,
Foldable, Traversable, Functor)
instance Semigroup a => Semigroup (Template a) where
x <> Empty = x
Empty <> x = x
x <> y = Concat x y
instance Semigroup a => Monoid (Template a) where
mappend = (<>)
mempty = Empty
data Pipe =
ToPairs
| ToUppercase
| ToLowercase
| ToLength
| Reverse
| FirstItem
| LastItem
| Rest
| AllButLast
| Chomp
| ToAlpha
| ToRoman
| NoWrap
| Block Alignment Int Border
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
data Alignment =
LeftAligned
| Centered
| RightAligned
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
data Border = Border
{ borderLeft :: Text
, borderRight :: Text
}
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
-- | A variable which may have several parts (@foo.bar.baz@).
data Variable =
Variable
{ varParts :: [Text]
, varPipes :: [Pipe]
}
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord)
instance Semigroup Variable where
Variable xs fs <> Variable ys gs = Variable (xs <> ys) (fs <> gs)
instance Monoid Variable where
mempty = Variable mempty mempty
mappend = (<>)
type TemplateTarget a =
(HasChars a, ToText a, FromText a)
-- | 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,
Data, Typeable)
-- | A variable value.
data Val a =
SimpleVal (Doc a)
| ListVal [Val a]
| MapVal (Context a)
| BoolVal Bool
| NullVal
deriving (Show, Traversable, Foldable, Functor, Data, Typeable)
-- | 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 ToContext a (Context a) where
toContext = id
toVal = MapVal
instance ToContext a (Val a) where
toVal = id
instance TemplateTarget a => ToContext a a where
toVal = SimpleVal . DL.literal
instance ToContext a a => ToContext a (Doc a) where
toVal = SimpleVal
instance ToContext a b => ToContext a [b] where
toVal = ListVal . map toVal
instance {-# OVERLAPPING #-} TemplateTarget [a] => ToContext [a] [a] where
toVal = SimpleVal . DL.literal
instance ToContext a b => ToContext a (M.Map Text b) where
toVal = MapVal . toContext
toContext = Context . M.map toVal
instance TemplateTarget a => ToContext a Bool where
toVal True = BoolVal True
toVal False = BoolVal False
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
-- | 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 (Doc a) where
fromVal (SimpleVal x) = Just x
fromVal _ = Nothing
instance TemplateTarget a => FromContext a a where
fromVal (SimpleVal x) = Just (DL.render Nothing x)
fromVal _ = Nothing
instance {-# OVERLAPPING #-} TemplateTarget [a] => FromContext [a] [a] where
fromVal (SimpleVal x) = Just (DL.render Nothing 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 (Val a) where
parseJSON v =
case v of
Array vec -> ListVal <$> mapM parseJSON (V.toList vec)
String t -> return $ SimpleVal $ DL.literal $ fromText t
Number n -> return $ SimpleVal $ fromString $
case floatingOrInteger n of
Left (r :: Double) -> show r
Right (i :: Integer) -> show i
Bool b -> return $ BoolVal b
Object o -> MapVal . Context . M.fromList . H.toList <$>
mapM parseJSON o
_ -> return NullVal
instance TemplateTarget a => FromJSON (Context a) where
parseJSON v = do
val <- parseJSON v
case val of
MapVal o -> return o
_ -> fail "Expecting MapVal"
instance TemplateTarget a => FromYAML (Val a) where
parseYAML v =
case v of
Mapping _ _ m -> MapVal . Context . M.fromList <$>
mapM (\(key, val) -> do
val' <- parseYAML val
key' <- parseYAML key
return (key', val')) (M.toList m)
Sequence _ _ xs -> ListVal <$> mapM parseYAML xs
Scalar _ (SStr t) -> return $ SimpleVal $ fromString . fromText $ t
Scalar _ (SFloat n) -> return $ SimpleVal $ fromString . show $ n
Scalar _ (SInt n) -> return $ SimpleVal $ fromString . show $ n
Scalar _ (SBool b) -> return $ BoolVal b
_ -> return NullVal
instance TemplateTarget a => FromYAML (Context a) where
parseYAML v = do
val <- parseYAML v
case val of
MapVal o -> return o
_ -> fail "Expecting MapVal"
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 d) = toJSON $ toText $ DL.render Nothing d
toJSON (BoolVal b) = toJSON b
instance TemplateTarget a => ToYAML (Context a) where
toYAML (Context m) = toYAML m
instance TemplateTarget a => ToYAML (Val a) where
toYAML NullVal = toYAML (Nothing :: Maybe Text)
toYAML (MapVal m) = toYAML m
toYAML (ListVal xs) = toYAML xs
toYAML (SimpleVal d) = toYAML $ toText $ DL.render Nothing d
toYAML (BoolVal b) = toYAML b
mapDoc :: TemplateTarget a => (Doc a -> Doc a) -> Val a -> Val a
mapDoc f val =
case val of
SimpleVal d -> SimpleVal (f d)
MapVal (Context m) -> MapVal (Context $ M.map (mapDoc f) m)
ListVal xs -> ListVal $ map (mapDoc f) xs
BoolVal b -> BoolVal b
NullVal -> NullVal
mapText :: TemplateTarget a => (Text -> Text) -> Val a -> Val a
mapText f val =
runIdentity (traverse (return . fromText . f . toText) val)
applyPipe :: TemplateTarget a => Pipe -> Val a -> Val a
applyPipe ToLength val = SimpleVal $ fromString . show $ len
where
len = case val of
SimpleVal d -> T.length . toText $ DL.render Nothing d
MapVal (Context m) -> M.size m
ListVal xs -> length xs
BoolVal _ -> 0
NullVal -> 0
applyPipe ToUppercase val = mapText T.toUpper val
applyPipe ToLowercase val = mapText T.toLower val
applyPipe ToPairs val =
case val of
MapVal (Context m) ->
ListVal $ map toPair $ M.toList m
ListVal xs ->
ListVal $ map toPair $ zip (map (fromString . show) [(1::Int)..]) xs
_ -> val
where
toPair (k, v) = MapVal $ Context $ M.fromList
[ ("key", SimpleVal $ fromString . T.unpack $ k)
, ("value", v) ]
applyPipe FirstItem val =
case val of
ListVal (x:_) -> x
_ -> val
applyPipe LastItem val =
case val of
ListVal xs@(_:_) -> last xs
_ -> val
applyPipe Rest val =
case val of
ListVal (_:xs) -> ListVal xs
_ -> val
applyPipe AllButLast val =
case val of
ListVal xs@(_:_) -> ListVal (init xs)
_ -> val
applyPipe Reverse val =
case val of
ListVal xs -> ListVal (reverse xs)
SimpleVal{} -> mapText T.reverse val
_ -> val
applyPipe Chomp val = mapDoc DL.chomp val
applyPipe ToAlpha val = mapText toAlpha val
where toAlpha t =
case T.decimal t of
Right (y,"") -> fromString [chr (ord 'a' + (y `mod` 26) - 1)]
_ -> t
applyPipe ToRoman val = mapText toRoman' val
where toRoman' t =
case T.decimal t of
Right (y,"") -> fromMaybe t (toRoman y)
_ -> t
applyPipe NoWrap val = mapDoc DL.nowrap val
applyPipe (Block align n border) val =
let constructor = case align of
LeftAligned -> DL.lblock
Centered -> DL.cblock
RightAligned -> DL.rblock
toBorder y = if T.null y
then mempty
else DL.vfill (fromText y)
in case nullToSimple val of
SimpleVal d -> SimpleVal $
toBorder (borderLeft border) <>
constructor n d <>
toBorder (borderRight border)
_ -> val
nullToSimple :: Monoid a => Val a -> Val a
nullToSimple NullVal = SimpleVal mempty
nullToSimple x = x
-- | Convert number 0 < x < 4000 to lowercase roman numeral.
toRoman :: Int -> Maybe Text
toRoman x
| x >= 1000
, x < 4000 = ("m" <>) <$> toRoman (x - 1000)
| x >= 900 = ("cm" <>) <$> toRoman (x - 900)
| x >= 500 = ("d" <>) <$> toRoman (x - 500)
| x >= 400 = ("cd" <>) <$> toRoman (x - 400)
| x >= 100 = ("c" <>) <$> toRoman (x - 100)
| x >= 90 = ("xc" <>) <$> toRoman (x - 90)
| x >= 50 = ("l" <>) <$> toRoman (x - 50)
| x >= 40 = ("xl" <>) <$> toRoman (x - 40)
| x >= 10 = ("x" <>) <$> toRoman (x - 10)
| x == 9 = return "ix"
| x >= 5 = ("v" <>) <$> toRoman (x - 5)
| x == 4 = return "iv"
| x >= 1 = ("i" <>) <$> toRoman (x - 1)
| x == 0 = return ""
| otherwise = Nothing
applyPipes :: TemplateTarget a => [Pipe] -> Val a -> Val a
applyPipes fs x = foldr applyPipe x $ reverse fs
multiLookup :: TemplateTarget a => [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
-- The Bool indicates whether it's a true or false value.
data Resolved a = Resolved Bool [Doc a]
deriving (Show, Read, Data, Typeable, Generic, Eq, Ord,
Foldable, Traversable, Functor)
instance Semigroup (Resolved a) where
Resolved b1 x1 <> Resolved b2 x2 = Resolved (b1 || b2) (x1 <> x2)
instance Monoid (Resolved a) where
mappend = (<>)
mempty = Resolved False []
resolveVariable :: TemplateTarget a
=> Variable -> Context a -> Resolved a
resolveVariable v ctx = resolveVariable' v (MapVal ctx)
resolveVariable' :: TemplateTarget a
=> Variable -> Val a -> Resolved a
resolveVariable' v val =
case applyPipes (varPipes v) $ multiLookup (varParts v) val of
ListVal xs -> mconcat $ map (resolveVariable' mempty) xs
SimpleVal d
| DL.isEmpty d -> Resolved False []
| otherwise -> Resolved True [removeFinalNl d]
MapVal _ -> Resolved True ["true"]
BoolVal True -> Resolved True ["true"]
BoolVal False -> Resolved False ["false"]
NullVal -> Resolved False []
removeFinalNl :: Doc a -> Doc a
removeFinalNl DL.NewLine = mempty
removeFinalNl DL.CarriageReturn = mempty
removeFinalNl (DL.Concat d1 d2) = d1 <> removeFinalNl d2
removeFinalNl x = x
withVariable :: (Monad m, TemplateTarget a)
=> Variable -> Context a -> (Context a -> m (Doc a))
-> m [Doc a]
withVariable var ctx f =
case applyPipes (varPipes var) $ multiLookup (varParts var) (MapVal ctx) of
NullVal -> return mempty
ListVal xs -> mapM (\iterval -> f $ setVarVal iterval) xs
MapVal ctx' -> (:[]) <$> f (setVarVal (MapVal ctx'))
val' -> (:[]) <$> f (setVarVal val')
where
setVarVal x =
addToContext var x $ Context $ M.insert "it" x $ unContext ctx
addToContext (Variable [] _) _ (Context ctx') = Context ctx'
addToContext (Variable (v:vs) fs) x (Context ctx') =
Context $ M.adjust
(\z -> case z of
_ | null vs -> x
MapVal m ->
MapVal $ addToContext (Variable vs fs) x m
_ -> z) v ctx'
type RenderState = S.State Int
-- | Render a compiled template in a "context" which provides
-- values for the template's variables.
renderTemplate :: (TemplateTarget a, ToContext a b)
=> Template a -> b -> Doc a
renderTemplate t x = S.evalState (renderTemp t (toContext x)) 0
updateColumn :: TemplateTarget a => Doc a -> RenderState (Doc a)
updateColumn x = do
S.modify $ DL.updateColumn x
return x
renderTemp :: forall a . TemplateTarget a
=> Template a -> Context a -> RenderState (Doc a)
renderTemp (Literal t) _ = updateColumn t
renderTemp (Interpolate v) ctx =
case resolveVariable v ctx of
Resolved _ xs -> updateColumn (mconcat xs)
renderTemp (Conditional v ift elset) ctx =
case resolveVariable v ctx of
Resolved False _ -> renderTemp elset ctx
Resolved True _ -> renderTemp ift ctx
renderTemp (Iterate v t sep) ctx = do
xs <- withVariable v ctx (renderTemp t)
sep' <- renderTemp sep ctx
return . mconcat . intersperse sep' $ xs
renderTemp (Nested t) ctx = do
n <- S.get
DL.nest n <$> renderTemp t ctx
renderTemp (Partial fs t) ctx = do
val' <- renderTemp t ctx
return $ case applyPipes fs (SimpleVal val') of
SimpleVal x -> x
_ -> mempty
renderTemp (Concat t1 t2) ctx =
mappend <$> renderTemp t1 ctx <*> renderTemp t2 ctx
renderTemp Empty _ = return 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