|
Packit |
938058 |
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances,
|
|
Packit |
938058 |
OverloadedStrings, GeneralizedNewtypeDeriving, ScopedTypeVariables #-}
|
|
Packit |
938058 |
{- |
|
|
Packit |
938058 |
Module : Text.Pandoc.Templates
|
|
Packit |
938058 |
Copyright : Copyright (C) 2009-2016 John MacFarlane
|
|
Packit |
938058 |
License : BSD3
|
|
Packit |
938058 |
|
|
Packit |
938058 |
Maintainer : John MacFarlane <jgm@berkeley.edu>
|
|
Packit |
938058 |
Stability : alpha
|
|
Packit |
938058 |
Portability : portable
|
|
Packit |
938058 |
|
|
Packit |
938058 |
A simple templating system with variable substitution and conditionals.
|
|
Packit |
938058 |
This module was formerly part of pandoc and is used for pandoc's
|
|
Packit |
938058 |
templates. The following program illustrates its use:
|
|
Packit |
938058 |
|
|
Packit |
938058 |
> {-# LANGUAGE OverloadedStrings #-}
|
|
Packit |
938058 |
> import Data.Text
|
|
Packit |
938058 |
> import Data.Aeson
|
|
Packit |
938058 |
> import Text.DocTemplates
|
|
Packit |
938058 |
>
|
|
Packit |
938058 |
> data Employee = Employee { firstName :: String
|
|
Packit |
938058 |
> , lastName :: String
|
|
Packit |
938058 |
> , salary :: Maybe Int }
|
|
Packit |
938058 |
> instance ToJSON Employee where
|
|
Packit |
938058 |
> toJSON e = object [ "name" .= object [ "first" .= firstName e
|
|
Packit |
938058 |
> , "last" .= lastName e ]
|
|
Packit |
938058 |
> , "salary" .= salary e ]
|
|
Packit |
938058 |
>
|
|
Packit |
938058 |
> template :: Text
|
|
Packit |
938058 |
> template = "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"
|
|
Packit |
938058 |
>
|
|
Packit |
938058 |
> main = case compileTemplate template of
|
|
Packit |
938058 |
> Left e -> error e
|
|
Packit |
938058 |
> Right t -> putStrLn $ renderTemplate t $ object
|
|
Packit |
938058 |
> ["employee" .=
|
|
Packit |
938058 |
> [ Employee "John" "Doe" Nothing
|
|
Packit |
938058 |
> , Employee "Omar" "Smith" (Just 30000)
|
|
Packit |
938058 |
> , Employee "Sara" "Chen" (Just 60000) ]
|
|
Packit |
938058 |
> ]
|
|
Packit |
938058 |
|
|
Packit |
938058 |
A slot for an interpolated variable is a variable name surrounded
|
|
Packit |
938058 |
by dollar signs. To include a literal @$@ in your template, use
|
|
Packit |
938058 |
@$$@. Variable names must begin with a letter and can contain letters,
|
|
Packit |
938058 |
numbers, @_@, @-@, and @.@.
|
|
Packit |
938058 |
|
|
Packit |
938058 |
The values of variables are determined by a JSON object that is
|
|
Packit |
938058 |
passed as a parameter to @renderTemplate@. So, for example,
|
|
Packit |
938058 |
@title@ will return the value of the @title@ field, and
|
|
Packit |
938058 |
@employee.salary@ will return the value of the @salary@ field
|
|
Packit |
938058 |
of the object that is the value of the @employee@ field.
|
|
Packit |
938058 |
|
|
Packit |
938058 |
The value of a variable will be indented to the same level as the
|
|
Packit |
938058 |
variable.
|
|
Packit |
938058 |
|
|
Packit |
938058 |
A conditional begins with @$if(variable_name)$@ and ends with @$endif$@.
|
|
Packit |
938058 |
It may optionally contain an @$else$@ section. The if section is
|
|
Packit |
938058 |
used if @variable_name@ has a non-null value, otherwise the else section
|
|
Packit |
938058 |
is used.
|
|
Packit |
938058 |
|
|
Packit |
938058 |
Conditional keywords should not be indented, or unexpected spacing
|
|
Packit |
938058 |
problems may occur.
|
|
Packit |
938058 |
|
|
Packit |
938058 |
The @$for$@ keyword can be used to iterate over an array. If
|
|
Packit |
938058 |
the value of the associated variable is not an array, a single
|
|
Packit |
938058 |
iteration will be performed on its value.
|
|
Packit |
938058 |
|
|
Packit |
938058 |
You may optionally specify separators using @$sep$@, as in the
|
|
Packit |
938058 |
example above.
|
|
Packit |
938058 |
|
|
Packit |
938058 |
-}
|
|
Packit |
938058 |
|
|
Packit |
938058 |
module Text.DocTemplates ( renderTemplate
|
|
Packit |
938058 |
, applyTemplate
|
|
Packit |
938058 |
, TemplateTarget(..)
|
|
Packit |
938058 |
, varListToJSON
|
|
Packit |
938058 |
, compileTemplate
|
|
Packit |
938058 |
, Template
|
|
Packit |
938058 |
) where
|
|
Packit |
938058 |
|
|
Packit |
938058 |
import Data.Char (isAlphaNum)
|
|
Packit |
938058 |
import Control.Monad (guard, when)
|
|
Packit |
938058 |
import Data.Aeson (ToJSON(..), Value(..))
|
|
Packit |
938058 |
import qualified Text.Parsec as P
|
|
Packit |
938058 |
import Text.Parsec.Text (Parser)
|
|
Packit |
938058 |
import qualified Data.Set as Set
|
|
Packit |
938058 |
import Data.Monoid
|
|
Packit |
938058 |
import Control.Applicative
|
|
Packit |
938058 |
import qualified Data.Text as T
|
|
Packit |
938058 |
import Data.Text (Text)
|
|
Packit |
938058 |
import Data.Text.Encoding (encodeUtf8)
|
|
Packit |
938058 |
import Data.List (intersperse)
|
|
Packit |
938058 |
import qualified Data.Map as M
|
|
Packit |
938058 |
import qualified Data.HashMap.Strict as H
|
|
Packit |
938058 |
import Data.Foldable (toList)
|
|
Packit |
938058 |
import Text.Blaze.Html (Html)
|
|
Packit |
938058 |
import Text.Blaze.Internal (preEscapedText)
|
|
Packit |
938058 |
import Data.ByteString.Lazy (ByteString, fromChunks)
|
|
Packit |
938058 |
import Data.Vector ((!?))
|
|
Packit |
938058 |
import Data.Scientific (floatingOrInteger)
|
|
Packit |
938058 |
|
|
Packit |
938058 |
-- | A 'Template' is essentially a function that takes
|
|
Packit |
938058 |
-- a JSON 'Value' and produces 'Text'.
|
|
Packit |
938058 |
newtype Template = Template { unTemplate :: Value -> Text }
|
|
Packit |
938058 |
deriving Monoid
|
|
Packit |
938058 |
|
|
Packit |
938058 |
type Variable = [Text]
|
|
Packit |
938058 |
|
|
Packit |
938058 |
class TemplateTarget a where
|
|
Packit |
938058 |
toTarget :: Text -> a
|
|
Packit |
938058 |
|
|
Packit |
938058 |
instance TemplateTarget Text where
|
|
Packit |
938058 |
toTarget = id
|
|
Packit |
938058 |
|
|
Packit |
938058 |
instance TemplateTarget String where
|
|
Packit |
938058 |
toTarget = T.unpack
|
|
Packit |
938058 |
|
|
Packit |
938058 |
instance TemplateTarget ByteString where
|
|
Packit |
938058 |
toTarget = fromChunks . (:[]) . encodeUtf8
|
|
Packit |
938058 |
|
|
Packit |
938058 |
instance TemplateTarget Html where
|
|
Packit |
938058 |
toTarget = preEscapedText
|
|
Packit |
938058 |
|
|
Packit |
938058 |
-- | A convenience function for passing in an association
|
|
Packit |
938058 |
-- list of string values instead of a JSON 'Value'.
|
|
Packit |
938058 |
varListToJSON :: [(String, String)] -> Value
|
|
Packit |
938058 |
varListToJSON assoc = toJSON $ M.fromList assoc'
|
|
Packit |
938058 |
where assoc' = [(T.pack k, toVal [T.pack z | (y,z) <- assoc,
|
|
Packit |
938058 |
not (null z),
|
|
Packit |
938058 |
y == k])
|
|
Packit |
938058 |
| k <- ordNub $ map fst assoc ]
|
|
Packit |
938058 |
toVal [x] = toJSON x
|
|
Packit |
938058 |
toVal [] = Null
|
|
Packit |
938058 |
toVal xs = toJSON xs
|
|
Packit |
938058 |
|
|
Packit |
938058 |
-- An efficient specialization of nub.
|
|
Packit |
938058 |
ordNub :: (Ord a) => [a] -> [a]
|
|
Packit |
938058 |
ordNub l = go Set.empty l
|
|
Packit |
938058 |
where
|
|
Packit |
938058 |
go _ [] = []
|
|
Packit |
938058 |
go s (x:xs) = if x `Set.member` s then go s xs
|
|
Packit |
938058 |
else x : go (Set.insert x s) xs
|
|
Packit |
938058 |
|
|
Packit |
938058 |
-- | Compile a template.
|
|
Packit |
938058 |
compileTemplate :: Text -> Either String Template
|
|
Packit |
938058 |
compileTemplate template =
|
|
Packit |
938058 |
case P.parse (pTemplate <* P.eof) "template" template of
|
|
Packit |
938058 |
Left e -> Left (show e)
|
|
Packit |
938058 |
Right x -> Right x
|
|
Packit |
938058 |
|
|
Packit |
938058 |
-- | Render a compiled template using @context@ to resolve variables.
|
|
Packit |
938058 |
renderTemplate :: (ToJSON a, TemplateTarget b) => Template -> a -> b
|
|
Packit |
938058 |
renderTemplate (Template f) context = toTarget $ f $ toJSON context
|
|
Packit |
938058 |
|
|
Packit |
938058 |
-- | Combines `renderTemplate` and `compileTemplate`.
|
|
Packit |
938058 |
applyTemplate :: (ToJSON a, TemplateTarget b) => Text -> a -> Either String b
|
|
Packit |
938058 |
applyTemplate t context =
|
|
Packit |
938058 |
case compileTemplate t of
|
|
Packit |
938058 |
Left e -> Left e
|
|
Packit |
938058 |
Right f -> Right $ renderTemplate f context
|
|
Packit |
938058 |
|
|
Packit |
938058 |
var :: Variable -> Template
|
|
Packit |
938058 |
var = Template . resolveVar
|
|
Packit |
938058 |
|
|
Packit |
938058 |
resolveVar :: Variable -> Value -> Text
|
|
Packit |
938058 |
resolveVar var' val =
|
|
Packit |
938058 |
case multiLookup var' val of
|
|
Packit |
938058 |
Just (Array vec) -> maybe mempty (resolveVar []) $ vec !? 0
|
|
Packit |
938058 |
Just (String t) -> T.stripEnd t
|
|
Packit |
938058 |
Just (Number n) -> case floatingOrInteger n of
|
|
Packit |
938058 |
Left (r :: Double) -> T.pack $ show r
|
|
Packit |
938058 |
Right (i :: Integer) -> T.pack $ show i
|
|
Packit |
938058 |
Just (Bool True) -> "true"
|
|
Packit |
938058 |
Just (Object _) -> "true"
|
|
Packit |
938058 |
Just _ -> mempty
|
|
Packit |
938058 |
Nothing -> mempty
|
|
Packit |
938058 |
|
|
Packit |
938058 |
multiLookup :: [Text] -> Value -> Maybe Value
|
|
Packit |
938058 |
multiLookup [] x = Just x
|
|
Packit |
938058 |
multiLookup (v:vs) (Object o) = H.lookup v o >>= multiLookup vs
|
|
Packit |
938058 |
multiLookup _ _ = Nothing
|
|
Packit |
938058 |
|
|
Packit |
938058 |
lit :: Text -> Template
|
|
Packit |
938058 |
lit = Template . const
|
|
Packit |
938058 |
|
|
Packit |
938058 |
cond :: Variable -> Template -> Template -> Template
|
|
Packit |
938058 |
cond var' (Template ifyes) (Template ifno) = Template $ \val ->
|
|
Packit |
938058 |
case resolveVar var' val of
|
|
Packit |
938058 |
"" -> ifno val
|
|
Packit |
938058 |
_ -> ifyes val
|
|
Packit |
938058 |
|
|
Packit |
938058 |
iter :: Variable -> Template -> Template -> Template
|
|
Packit |
938058 |
iter var' template sep = Template $ \val -> unTemplate
|
|
Packit |
938058 |
(case multiLookup var' val of
|
|
Packit |
938058 |
Just (Array vec) -> mconcat $ intersperse sep
|
|
Packit |
938058 |
$ map (setVar template var')
|
|
Packit |
938058 |
$ toList vec
|
|
Packit |
938058 |
Just x -> cond var' (setVar template var' x) mempty
|
|
Packit |
938058 |
Nothing -> mempty) val
|
|
Packit |
938058 |
|
|
Packit |
938058 |
setVar :: Template -> Variable -> Value -> Template
|
|
Packit |
938058 |
setVar (Template f) var' val = Template $ f . replaceVar var' val
|
|
Packit |
938058 |
|
|
Packit |
938058 |
replaceVar :: Variable -> Value -> Value -> Value
|
|
Packit |
938058 |
replaceVar [] new _ = new
|
|
Packit |
938058 |
replaceVar (v:vs) new (Object o) =
|
|
Packit |
938058 |
Object $ H.adjust (\x -> replaceVar vs new x) v o
|
|
Packit |
938058 |
replaceVar _ _ old = old
|
|
Packit |
938058 |
|
|
Packit |
938058 |
--- parsing
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pTemplate :: Parser Template
|
|
Packit |
938058 |
pTemplate = do
|
|
Packit |
938058 |
sp <- P.option mempty pInitialSpace
|
|
Packit |
938058 |
rest <- mconcat <$> many (pConditional <|>
|
|
Packit |
938058 |
pFor <|>
|
|
Packit |
938058 |
pNewline <|>
|
|
Packit |
938058 |
pVar <|>
|
|
Packit |
938058 |
pComment <|>
|
|
Packit |
938058 |
pLit <|>
|
|
Packit |
938058 |
pEscapedDollar)
|
|
Packit |
938058 |
return $ sp <> rest
|
|
Packit |
938058 |
|
|
Packit |
938058 |
takeWhile1 :: (Char -> Bool) -> Parser Text
|
|
Packit |
938058 |
takeWhile1 f = T.pack <$> P.many1 (P.satisfy f)
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pLit :: Parser Template
|
|
Packit |
938058 |
pLit = lit <$> takeWhile1 (\x -> x /='$' && x /= '\n')
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pNewline :: Parser Template
|
|
Packit |
938058 |
pNewline = do
|
|
Packit |
938058 |
P.char '\n'
|
|
Packit |
938058 |
sp <- P.option mempty pInitialSpace
|
|
Packit |
938058 |
return $ lit "\n" <> sp
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pInitialSpace :: Parser Template
|
|
Packit |
938058 |
pInitialSpace = do
|
|
Packit |
938058 |
sps <- takeWhile1 (==' ')
|
|
Packit |
938058 |
let indentVar = if T.null sps
|
|
Packit |
938058 |
then id
|
|
Packit |
938058 |
else indent (T.length sps)
|
|
Packit |
938058 |
v <- P.option mempty $ indentVar <$> pVar
|
|
Packit |
938058 |
return $ lit sps <> v
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pEscapedDollar :: Parser Template
|
|
Packit |
938058 |
pEscapedDollar = lit "$" <$ P.try (P.string "$$")
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pComment :: Parser Template
|
|
Packit |
938058 |
pComment = do
|
|
Packit |
938058 |
pos <- P.getPosition
|
|
Packit |
938058 |
P.try (P.string "$--")
|
|
Packit |
938058 |
P.skipMany (P.satisfy (/='\n'))
|
|
Packit |
938058 |
-- If the comment begins in the first column, the line ending
|
|
Packit |
938058 |
-- will be consumed; otherwise not.
|
|
Packit |
938058 |
when (P.sourceColumn pos == 1) $ () <$ P.char '\n'
|
|
Packit |
938058 |
return mempty
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pVar :: Parser Template
|
|
Packit |
938058 |
pVar = var <$> (P.try $ P.char '$' *> pIdent <* P.char '$')
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pIdent :: Parser [Text]
|
|
Packit |
938058 |
pIdent = do
|
|
Packit |
938058 |
first <- pIdentPart
|
|
Packit |
938058 |
rest <- many (P.char '.' *> pIdentPart)
|
|
Packit |
938058 |
return (first:rest)
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pIdentPart :: Parser Text
|
|
Packit |
938058 |
pIdentPart = P.try $ do
|
|
Packit |
938058 |
first <- P.letter
|
|
Packit |
938058 |
rest <- T.pack <$> P.many (P.satisfy (\c -> isAlphaNum c || c == '_' || c == '-'))
|
|
Packit |
938058 |
let id' = T.singleton first <> rest
|
|
Packit |
938058 |
guard $ id' `notElem` reservedWords
|
|
Packit |
938058 |
return id'
|
|
Packit |
938058 |
|
|
Packit |
938058 |
reservedWords :: [Text]
|
|
Packit |
938058 |
reservedWords = ["else","endif","for","endfor","sep"]
|
|
Packit |
938058 |
|
|
Packit |
938058 |
skipEndline :: Parser ()
|
|
Packit |
938058 |
skipEndline = P.try $ P.skipMany (P.satisfy (`elem` (" \t" :: String))) >> P.char '\n' >> return ()
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pConditional :: Parser Template
|
|
Packit |
938058 |
pConditional = do
|
|
Packit |
938058 |
P.try $ P.string "$if("
|
|
Packit |
938058 |
id' <- pIdent
|
|
Packit |
938058 |
P.string ")$"
|
|
Packit |
938058 |
-- if newline after the "if", then a newline after "endif" will be swallowed
|
|
Packit |
938058 |
multiline <- P.option False (True <$ skipEndline)
|
|
Packit |
938058 |
ifContents <- pTemplate
|
|
Packit |
938058 |
elseContents <- P.option mempty $ P.try $
|
|
Packit |
938058 |
do P.string "$else$"
|
|
Packit |
938058 |
when multiline $ P.option () skipEndline
|
|
Packit |
938058 |
pTemplate
|
|
Packit |
938058 |
P.string "$endif$"
|
|
Packit |
938058 |
when multiline $ P.option () skipEndline
|
|
Packit |
938058 |
return $ cond id' ifContents elseContents
|
|
Packit |
938058 |
|
|
Packit |
938058 |
pFor :: Parser Template
|
|
Packit |
938058 |
pFor = do
|
|
Packit |
938058 |
P.try $ P.string "$for("
|
|
Packit |
938058 |
id' <- pIdent
|
|
Packit |
938058 |
P.string ")$"
|
|
Packit |
938058 |
-- if newline after the "for", then a newline after "endfor" will be swallowed
|
|
Packit |
938058 |
multiline <- P.option False $ skipEndline >> return True
|
|
Packit |
938058 |
contents <- pTemplate
|
|
Packit |
938058 |
sep <- P.option mempty $
|
|
Packit |
938058 |
do P.try $ P.string "$sep$"
|
|
Packit |
938058 |
when multiline $ P.option () skipEndline
|
|
Packit |
938058 |
pTemplate
|
|
Packit |
938058 |
P.string "$endfor$"
|
|
Packit |
938058 |
when multiline $ P.option () skipEndline
|
|
Packit |
938058 |
return $ iter id' contents sep
|
|
Packit |
938058 |
|
|
Packit |
938058 |
indent :: Int -> Template -> Template
|
|
Packit |
938058 |
indent 0 x = x
|
|
Packit |
938058 |
indent ind (Template f) = Template $ \val -> indent' (f val)
|
|
Packit |
938058 |
where indent' t = T.concat
|
|
Packit |
938058 |
$ intersperse ("\n" <> T.replicate ind " ") $ T.lines t
|