{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, CPP #-} {- Copyright (C) 2009-2010 John MacFarlane 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 Copyright : Copyright (C) 2009-2010 John MacFarlane License : GNU GPL, version 2 or above Maintainer : John MacFarlane Stability : alpha Portability : portable A simple templating system with variable substitution and conditionals. Example: > renderTemplate [("name","Sam"),("salary","50,000")] $ > "Hi, $name$. $if(salary)$You make $$$salary$.$else$No salary data.$endif$" > "Hi, John. You make $50,000." 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, numbers, @_@, and @-@. The value of a variable will be indented to the same level as the variable. 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. Conditional keywords should not be indented, or unexpected spacing problems may occur. If a variable name is associated with multiple values in the association list passed to 'renderTemplate', you may use the @$for$@ keyword to iterate over them: > renderTemplate [("name","Sam"),("name","Joe")] $ > "$for(name)$\nHi, $name$.\n$endfor$" > "Hi, Sam.\nHi, Joe." You may optionally specify separators using @$sep$@: > renderTemplate [("name","Sam"),("name","Joe"),("name","Lynn")] $ > "Hi, $for(name)$$name$$sep$, $endfor$" > "Hi, Sam, Joe, Lynn." -} module Text.Pandoc.Templates ( renderTemplate , TemplateTarget , getDefaultTemplate ) where import Text.Parsec import Control.Monad (liftM, when, forM, mzero) import System.FilePath import Data.List (intercalate, intersperse) #if MIN_VERSION_blaze_html(0,5,0) import Text.Blaze.Html (Html) import Text.Blaze.Internal (preEscapedString) #else import Text.Blaze (preEscapedString, Html) #endif import Data.ByteString.Lazy.UTF8 (ByteString, fromString) import Text.Pandoc.Shared (readDataFile) import qualified Control.Exception.Extensible as E (try, IOException) -- | Get default template for the specified writer. getDefaultTemplate :: (Maybe FilePath) -- ^ User data directory to search first -> String -- ^ Name of writer -> IO (Either E.IOException String) getDefaultTemplate user writer = do let format = takeWhile (`notElem` "+-") writer -- strip off extensions case format of "native" -> return $ Right "" "json" -> return $ Right "" "docx" -> return $ Right "" "odt" -> getDefaultTemplate user "opendocument" "epub" -> return $ Right "" _ -> let fname = "templates" "default" <.> format in E.try $ readDataFile user fname data TemplateState = TemplateState Int [(String,String)] adjustPosition :: String -> Parsec [Char] TemplateState String adjustPosition str = do let lastline = takeWhile (/= '\n') $ reverse str updateState $ \(TemplateState pos x) -> if str == lastline then TemplateState (pos + length lastline) x else TemplateState (length lastline) x return str class TemplateTarget a where toTarget :: String -> a instance TemplateTarget String where toTarget = id instance TemplateTarget ByteString where toTarget = fromString instance TemplateTarget Html where toTarget = preEscapedString -- | Renders a template renderTemplate :: TemplateTarget a => [(String,String)] -- ^ Assoc. list of values for variables -> String -- ^ Template -> a renderTemplate vals templ = case runParser (do x <- parseTemplate; eof; return x) (TemplateState 0 vals) "template" templ of Left e -> error $ show e Right r -> toTarget $ concat r reservedWords :: [String] reservedWords = ["else","endif","for","endfor","sep"] parseTemplate :: Parsec [Char] TemplateState [String] parseTemplate = many $ (plaintext <|> escapedDollar <|> conditional <|> for <|> variable) >>= adjustPosition plaintext :: Parsec [Char] TemplateState String plaintext = many1 $ noneOf "$" escapedDollar :: Parsec [Char] TemplateState String escapedDollar = try $ string "$$" >> return "$" skipEndline :: Parsec [Char] st () skipEndline = try $ skipMany (oneOf " \t") >> newline >> return () conditional :: Parsec [Char] TemplateState String conditional = try $ do TemplateState pos vars <- getState string "$if(" id' <- ident string ")$" -- if newline after the "if", then a newline after "endif" will be swallowed multiline <- option False $ try $ skipEndline >> return True ifContents <- liftM concat parseTemplate -- reset state for else block setState $ TemplateState pos vars elseContents <- option "" $ do try (string "$else$") when multiline $ optional skipEndline liftM concat parseTemplate string "$endif$" when multiline $ optional skipEndline let conditionSatisfied = case lookup id' vars of Nothing -> False Just "" -> False Just _ -> True return $ if conditionSatisfied then ifContents else elseContents for :: Parsec [Char] TemplateState String for = try $ do TemplateState pos vars <- getState string "$for(" id' <- ident string ")$" -- if newline after the "for", then a newline after "endfor" will be swallowed multiline <- option False $ try $ skipEndline >> return True let matches = filter (\(k,_) -> k == id') vars let indent = replicate pos ' ' contents <- forM matches $ \m -> do updateState $ \(TemplateState p v) -> TemplateState p (m:v) raw <- liftM concat $ lookAhead parseTemplate return $ intercalate ('\n':indent) $ lines $ raw ++ "\n" parseTemplate sep <- option "" $ do try (string "$sep$") when multiline $ optional skipEndline liftM concat parseTemplate string "$endfor$" when multiline $ optional skipEndline setState $ TemplateState pos vars return $ concat $ intersperse sep contents ident :: Parsec [Char] TemplateState String ident = do first <- letter rest <- many (alphaNum <|> oneOf "_-") let id' = first : rest if id' `elem` reservedWords then mzero else return id' variable :: Parsec [Char] TemplateState String variable = try $ do char '$' id' <- ident char '$' TemplateState pos vars <- getState let indent = replicate pos ' ' return $ case lookup id' vars of Just val -> intercalate ('\n' : indent) $ lines val Nothing -> ""