Blame src/Text/DocTemplates.hs

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