2013-05-16 23:00:58 -07:00
|
|
|
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, CPP,
|
|
|
|
OverloadedStrings, GeneralizedNewtypeDeriving #-}
|
2009-12-31 01:08:38 +00:00
|
|
|
{-
|
2013-05-16 23:00:58 -07:00
|
|
|
Copyright (C) 2009-2013 John MacFarlane <jgm@berkeley.edu>
|
2009-12-31 01:08:38 +00:00
|
|
|
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
|
|
it under the terms of the GNU General Public License as published by
|
|
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
|
|
(at your option) any later version.
|
|
|
|
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
|
|
GNU General Public License for more details.
|
|
|
|
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
|
|
along with this program; if not, write to the Free Software
|
|
|
|
Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
|
|
|
|
-}
|
|
|
|
|
|
|
|
{- |
|
|
|
|
Module : Text.Pandoc.Templates
|
2013-05-16 23:00:58 -07:00
|
|
|
Copyright : Copyright (C) 2009-2013 John MacFarlane
|
2009-12-31 01:08:38 +00:00
|
|
|
License : GNU GPL, version 2 or above
|
|
|
|
|
|
|
|
Maintainer : John MacFarlane <jgm@berkeley.edu>
|
|
|
|
Stability : alpha
|
|
|
|
Portability : portable
|
|
|
|
|
|
|
|
A simple templating system with variable substitution and conditionals.
|
2013-05-16 23:00:58 -07:00
|
|
|
The following program illustrates its use:
|
|
|
|
|
|
|
|
> {-# LANGUAGE OverloadedStrings #-}
|
|
|
|
> import Data.Text
|
|
|
|
> import Data.Aeson
|
|
|
|
> import Text.Pandoc.Templates
|
|
|
|
>
|
|
|
|
> data Employee = Employee { firstName :: String
|
|
|
|
> , lastName :: String
|
|
|
|
> , salary :: Maybe Int }
|
|
|
|
> instance ToJSON Employee where
|
|
|
|
> toJSON e = object [ "name" .= object [ "first" .= firstName e
|
|
|
|
> , "last" .= lastName e ]
|
|
|
|
> , "salary" .= salary e ]
|
|
|
|
>
|
|
|
|
> employees :: [Employee]
|
|
|
|
> employees = [ Employee "John" "Doe" Nothing
|
|
|
|
> , Employee "Omar" "Smith" (Just 30000)
|
|
|
|
> , Employee "Sara" "Chen" (Just 60000) ]
|
|
|
|
>
|
|
|
|
> template :: Template
|
|
|
|
> template = either error id $ compileTemplate
|
|
|
|
> "$for(employee)$Hi, $employee.name.first$. $if(employee.salary)$You make $employee.salary$.$else$No salary data.$endif$$sep$\n$endfor$"
|
|
|
|
>
|
|
|
|
> main = putStrLn $ renderTemplate template $ object ["employee" .= employees ]
|
2009-12-31 01:08:38 +00:00
|
|
|
|
|
|
|
A slot for an interpolated variable is a variable name surrounded
|
|
|
|
by dollar signs. To include a literal @$@ in your template, use
|
|
|
|
@$$@. Variable names must begin with a letter and can contain letters,
|
2013-05-16 23:00:58 -07:00
|
|
|
numbers, @_@, @-@, and @.@.
|
|
|
|
|
|
|
|
The values of variables are determined by a JSON object that is
|
|
|
|
passed as a parameter to @renderTemplate@. So, for example,
|
|
|
|
@title@ will return the value of the @title@ field, and
|
|
|
|
@employee.salary@ will return the value of the @salary@ field
|
|
|
|
of the object that is the value of the @employee@ field.
|
2009-12-31 01:08:38 +00:00
|
|
|
|
2009-12-31 01:14:11 +00:00
|
|
|
The value of a variable will be indented to the same level as the
|
|
|
|
variable.
|
|
|
|
|
2009-12-31 01:08:38 +00:00
|
|
|
A conditional begins with @$if(variable_name)$@ and ends with @$endif$@.
|
|
|
|
It may optionally contain an @$else$@ section. The if section is
|
|
|
|
used if @variable_name@ has a non-null value, otherwise the else section
|
|
|
|
is used.
|
2009-12-31 01:14:11 +00:00
|
|
|
|
|
|
|
Conditional keywords should not be indented, or unexpected spacing
|
|
|
|
problems may occur.
|
2009-12-31 01:15:33 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
The @$for$@ keyword can be used to iterate over an array. If
|
|
|
|
the value of the associated variable is not an array, a single
|
|
|
|
iteration will be performed on its value.
|
2009-12-31 01:15:33 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
You may optionally specify separators using @$sep$@, as in the
|
|
|
|
example above.
|
2009-12-31 01:15:33 +00:00
|
|
|
|
2009-12-31 01:08:38 +00:00
|
|
|
-}
|
|
|
|
|
2009-12-31 01:14:35 +00:00
|
|
|
module Text.Pandoc.Templates ( renderTemplate
|
2013-05-10 22:53:35 -07:00
|
|
|
, renderTemplate'
|
2013-05-16 23:00:58 -07:00
|
|
|
, TemplateTarget(..)
|
|
|
|
, varListToJSON
|
|
|
|
, compileTemplate
|
|
|
|
, Template
|
2010-01-18 05:06:08 +00:00
|
|
|
, getDefaultTemplate ) where
|
2009-12-31 01:08:38 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
import Data.Char (isAlphaNum)
|
|
|
|
import Control.Monad (guard, when)
|
|
|
|
import Data.Aeson (ToJSON(..), Value(..))
|
|
|
|
import qualified Data.Attoparsec.Text as A
|
|
|
|
import Data.Attoparsec.Text (Parser)
|
|
|
|
import Control.Applicative
|
|
|
|
import qualified Data.Text as T
|
|
|
|
import Data.Text (Text)
|
|
|
|
import Data.Text.Encoding (encodeUtf8)
|
2013-08-08 10:41:39 -07:00
|
|
|
import Text.Pandoc.Compat.Monoid ((<>), Monoid(..))
|
2013-05-16 23:00:58 -07:00
|
|
|
import Data.List (intersperse, nub)
|
|
|
|
import System.FilePath ((</>), (<.>))
|
|
|
|
import qualified Data.Map as M
|
|
|
|
import qualified Data.HashMap.Strict as H
|
|
|
|
import Data.Foldable (toList)
|
|
|
|
import qualified Control.Exception.Extensible as E (try, IOException)
|
2012-05-10 09:13:14 -07:00
|
|
|
#if MIN_VERSION_blaze_html(0,5,0)
|
2012-04-23 19:53:04 +10:00
|
|
|
import Text.Blaze.Html (Html)
|
2013-05-16 23:00:58 -07:00
|
|
|
import Text.Blaze.Internal (preEscapedText)
|
2012-05-10 09:13:14 -07:00
|
|
|
#else
|
2013-05-16 23:00:58 -07:00
|
|
|
import Text.Blaze (preEscapedText, Html)
|
2012-05-10 09:13:14 -07:00
|
|
|
#endif
|
2013-05-16 23:00:58 -07:00
|
|
|
import Data.ByteString.Lazy (ByteString, fromChunks)
|
2012-12-29 17:44:02 -08:00
|
|
|
import Text.Pandoc.Shared (readDataFileUTF8)
|
2013-10-21 12:31:07 -07:00
|
|
|
import Data.Vector ((!?))
|
2010-01-11 03:43:49 +00:00
|
|
|
|
2010-01-18 05:06:08 +00:00
|
|
|
-- | Get default template for the specified writer.
|
2012-07-26 22:32:53 -07:00
|
|
|
getDefaultTemplate :: (Maybe FilePath) -- ^ User data directory to search first
|
|
|
|
-> String -- ^ Name of writer
|
2010-01-18 05:06:08 +00:00
|
|
|
-> IO (Either E.IOException String)
|
|
|
|
getDefaultTemplate user writer = do
|
2012-08-09 20:19:06 -07:00
|
|
|
let format = takeWhile (`notElem` "+-") writer -- strip off extensions
|
|
|
|
case format of
|
|
|
|
"native" -> return $ Right ""
|
|
|
|
"json" -> return $ Right ""
|
|
|
|
"docx" -> return $ Right ""
|
2012-08-09 22:30:44 -07:00
|
|
|
"odt" -> getDefaultTemplate user "opendocument"
|
2013-07-02 09:04:07 -07:00
|
|
|
"markdown_strict" -> getDefaultTemplate user "markdown"
|
|
|
|
"multimarkdown" -> getDefaultTemplate user "markdown"
|
|
|
|
"markdown_github" -> getDefaultTemplate user "markdown"
|
|
|
|
"markdown_mmd" -> getDefaultTemplate user "markdown"
|
|
|
|
"markdown_phpextra" -> getDefaultTemplate user "markdown"
|
2012-08-09 20:19:06 -07:00
|
|
|
_ -> let fname = "templates" </> "default" <.> format
|
2012-12-29 17:44:02 -08:00
|
|
|
in E.try $ readDataFileUTF8 user fname
|
2009-12-31 01:14:04 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
newtype Template = Template { unTemplate :: Value -> Text }
|
|
|
|
deriving Monoid
|
2009-12-31 01:14:04 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
type Variable = [Text]
|
2009-12-31 01:14:04 +00:00
|
|
|
|
2009-12-31 01:14:35 +00:00
|
|
|
class TemplateTarget a where
|
2013-05-16 23:00:58 -07:00
|
|
|
toTarget :: Text -> a
|
2009-12-31 01:14:35 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
instance TemplateTarget Text where
|
2009-12-31 01:14:35 +00:00
|
|
|
toTarget = id
|
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
instance TemplateTarget String where
|
|
|
|
toTarget = T.unpack
|
|
|
|
|
2012-07-26 22:32:53 -07:00
|
|
|
instance TemplateTarget ByteString where
|
2013-05-16 23:00:58 -07:00
|
|
|
toTarget = fromChunks . (:[]) . encodeUtf8
|
2009-12-31 01:14:35 +00:00
|
|
|
|
|
|
|
instance TemplateTarget Html where
|
2013-05-16 23:00:58 -07:00
|
|
|
toTarget = preEscapedText
|
|
|
|
|
|
|
|
varListToJSON :: [(String, String)] -> Value
|
|
|
|
varListToJSON assoc = toJSON $ M.fromList assoc'
|
|
|
|
where assoc' = [(T.pack k, toVal [T.pack z | (y,z) <- assoc,
|
|
|
|
not (null z),
|
|
|
|
y == k])
|
|
|
|
| k <- nub $ map fst assoc ]
|
|
|
|
toVal [x] = toJSON x
|
|
|
|
toVal [] = Null
|
|
|
|
toVal xs = toJSON xs
|
|
|
|
|
|
|
|
renderTemplate :: (ToJSON a, TemplateTarget b) => Template -> a -> b
|
2013-05-10 22:53:35 -07:00
|
|
|
renderTemplate (Template f) context = toTarget $ f $ toJSON context
|
2013-05-16 23:00:58 -07:00
|
|
|
|
|
|
|
compileTemplate :: Text -> Either String Template
|
|
|
|
compileTemplate template = A.parseOnly pTemplate template
|
|
|
|
|
2013-05-10 22:53:35 -07:00
|
|
|
-- | Like 'renderTemplate', but compiles the template first,
|
|
|
|
-- raising an error if compilation fails.
|
|
|
|
renderTemplate' :: (ToJSON a, TemplateTarget b) => String -> a -> b
|
|
|
|
renderTemplate' template =
|
|
|
|
renderTemplate (either error id $ compileTemplate $ T.pack template)
|
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
var :: Variable -> Template
|
|
|
|
var = Template . resolveVar
|
|
|
|
|
|
|
|
resolveVar :: Variable -> Value -> Text
|
|
|
|
resolveVar var' val =
|
|
|
|
case multiLookup var' val of
|
2013-10-21 12:31:07 -07:00
|
|
|
Just (Array vec) -> maybe mempty (resolveVar []) $ vec !? 0
|
2013-05-16 23:00:58 -07:00
|
|
|
Just (String t) -> T.stripEnd t
|
|
|
|
Just (Number n) -> T.pack $ show n
|
|
|
|
Just (Bool True) -> "true"
|
|
|
|
Just _ -> mempty
|
|
|
|
Nothing -> mempty
|
|
|
|
|
|
|
|
multiLookup :: [Text] -> Value -> Maybe Value
|
|
|
|
multiLookup [] x = Just x
|
|
|
|
multiLookup (v:vs) (Object o) = H.lookup v o >>= multiLookup vs
|
|
|
|
multiLookup _ _ = Nothing
|
|
|
|
|
|
|
|
lit :: Text -> Template
|
|
|
|
lit = Template . const
|
|
|
|
|
|
|
|
cond :: Variable -> Template -> Template -> Template
|
|
|
|
cond var' (Template ifyes) (Template ifno) = Template $ \val ->
|
|
|
|
case resolveVar var' val of
|
|
|
|
"" -> ifno val
|
|
|
|
_ -> ifyes val
|
|
|
|
|
|
|
|
iter :: Variable -> Template -> Template -> Template
|
|
|
|
iter var' template sep = Template $ \val -> unTemplate
|
|
|
|
(case multiLookup var' val of
|
|
|
|
Just (Array vec) -> mconcat $ intersperse sep
|
|
|
|
$ map (setVar template var')
|
|
|
|
$ toList vec
|
2013-09-08 12:04:47 -07:00
|
|
|
Just x -> cond var' (setVar template var' x) mempty
|
2013-05-16 23:00:58 -07:00
|
|
|
Nothing -> mempty) val
|
|
|
|
|
|
|
|
setVar :: Template -> Variable -> Value -> Template
|
|
|
|
setVar (Template f) var' val = Template $ f . replaceVar var' val
|
|
|
|
|
|
|
|
replaceVar :: Variable -> Value -> Value -> Value
|
|
|
|
replaceVar [] new _ = new
|
|
|
|
replaceVar (v:vs) new (Object o) =
|
|
|
|
Object $ H.adjust (\x -> replaceVar vs new x) v o
|
|
|
|
replaceVar _ _ old = old
|
|
|
|
|
|
|
|
--- parsing
|
|
|
|
|
|
|
|
pTemplate :: Parser Template
|
|
|
|
pTemplate = do
|
|
|
|
sp <- A.option mempty pInitialSpace
|
|
|
|
rest <- mconcat <$> many (pConditional <|>
|
|
|
|
pFor <|>
|
|
|
|
pNewline <|>
|
|
|
|
pVar <|>
|
|
|
|
pLit <|>
|
|
|
|
pEscapedDollar)
|
|
|
|
return $ sp <> rest
|
|
|
|
|
|
|
|
pLit :: Parser Template
|
|
|
|
pLit = lit <$> A.takeWhile1 (\x -> x /='$' && x /= '\n')
|
|
|
|
|
|
|
|
pNewline :: Parser Template
|
|
|
|
pNewline = do
|
|
|
|
A.char '\n'
|
|
|
|
sp <- A.option mempty pInitialSpace
|
|
|
|
return $ lit "\n" <> sp
|
|
|
|
|
|
|
|
pInitialSpace :: Parser Template
|
|
|
|
pInitialSpace = do
|
|
|
|
sps <- A.takeWhile1 (==' ')
|
|
|
|
let indentVar = if T.null sps
|
|
|
|
then id
|
|
|
|
else indent (T.length sps)
|
|
|
|
v <- A.option mempty $ indentVar <$> pVar
|
|
|
|
return $ lit sps <> v
|
|
|
|
|
|
|
|
pEscapedDollar :: Parser Template
|
|
|
|
pEscapedDollar = lit "$" <$ A.string "$$"
|
|
|
|
|
|
|
|
pVar :: Parser Template
|
|
|
|
pVar = var <$> (A.char '$' *> pIdent <* A.char '$')
|
|
|
|
|
|
|
|
pIdent :: Parser [Text]
|
|
|
|
pIdent = do
|
|
|
|
first <- pIdentPart
|
|
|
|
rest <- many (A.char '.' *> pIdentPart)
|
|
|
|
return (first:rest)
|
|
|
|
|
|
|
|
pIdentPart :: Parser Text
|
|
|
|
pIdentPart = do
|
|
|
|
first <- A.letter
|
|
|
|
rest <- A.takeWhile (\c -> isAlphaNum c || c == '_' || c == '-')
|
|
|
|
let id' = T.singleton first <> rest
|
|
|
|
guard $ id' `notElem` reservedWords
|
|
|
|
return id'
|
|
|
|
|
|
|
|
reservedWords :: [Text]
|
2009-12-31 01:15:33 +00:00
|
|
|
reservedWords = ["else","endif","for","endfor","sep"]
|
2009-12-31 01:08:38 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
skipEndline :: Parser ()
|
|
|
|
skipEndline = A.skipWhile (`elem` " \t") >> A.char '\n' >> return ()
|
2009-12-31 01:15:33 +00:00
|
|
|
|
2013-05-16 23:00:58 -07:00
|
|
|
pConditional :: Parser Template
|
|
|
|
pConditional = do
|
|
|
|
A.string "$if("
|
|
|
|
id' <- pIdent
|
|
|
|
A.string ")$"
|
2009-12-31 01:14:04 +00:00
|
|
|
-- if newline after the "if", then a newline after "endif" will be swallowed
|
2013-05-16 23:00:58 -07:00
|
|
|
multiline <- A.option False (True <$ skipEndline)
|
|
|
|
ifContents <- pTemplate
|
|
|
|
elseContents <- A.option mempty $
|
|
|
|
do A.string "$else$"
|
|
|
|
when multiline $ A.option () skipEndline
|
|
|
|
pTemplate
|
|
|
|
A.string "$endif$"
|
|
|
|
when multiline $ A.option () skipEndline
|
|
|
|
return $ cond id' ifContents elseContents
|
|
|
|
|
|
|
|
pFor :: Parser Template
|
|
|
|
pFor = do
|
|
|
|
A.string "$for("
|
|
|
|
id' <- pIdent
|
|
|
|
A.string ")$"
|
2010-07-05 00:11:02 -07:00
|
|
|
-- if newline after the "for", then a newline after "endfor" will be swallowed
|
2013-05-16 23:00:58 -07:00
|
|
|
multiline <- A.option False $ skipEndline >> return True
|
|
|
|
contents <- pTemplate
|
|
|
|
sep <- A.option mempty $
|
|
|
|
do A.string "$sep$"
|
|
|
|
when multiline $ A.option () skipEndline
|
|
|
|
pTemplate
|
|
|
|
A.string "$endfor$"
|
|
|
|
when multiline $ A.option () skipEndline
|
|
|
|
return $ iter id' contents sep
|
|
|
|
|
|
|
|
indent :: Int -> Template -> Template
|
|
|
|
indent 0 x = x
|
|
|
|
indent ind (Template f) = Template $ \val -> indent' (f val)
|
|
|
|
where indent' t = T.concat
|
|
|
|
$ intersperse ("\n" <> T.replicate ind " ") $ T.lines t
|