{-# LANGUAGE TypeSynonymInstances #-} {- 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.ParserCombinators.Parsec import Control.Monad (liftM, when, forM) import System.FilePath import Data.List (intercalate, intersperse) import Text.PrettyPrint (text, Doc) import Text.XHtml (primHtml, Html) 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 _ "native" = return $ Right "" getDefaultTemplate user "s5" = getDefaultTemplate user "html" getDefaultTemplate user "odt" = getDefaultTemplate user "opendocument" getDefaultTemplate user writer = do let format = takeWhile (/='+') writer -- strip off "+lhs" if present let fname = "templates" format <.> "template" E.try $ readDataFile user fname data TemplateState = TemplateState Int [(String,String)] adjustPosition :: String -> GenParser 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 = primHtml instance TemplateTarget Doc where toTarget = text -- | 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 :: GenParser Char TemplateState [String] parseTemplate = many $ (plaintext <|> escapedDollar <|> conditional <|> for <|> variable) >>= adjustPosition plaintext :: GenParser Char TemplateState String plaintext = many1 $ noneOf "$" escapedDollar :: GenParser Char TemplateState String escapedDollar = try $ string "$$" >> return "$" skipEndline :: GenParser Char st () skipEndline = try $ skipMany (oneOf " \t") >> newline >> return () conditional :: GenParser 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 :: GenParser Char TemplateState String for = try $ do TemplateState pos vars <- getState string "$for(" id' <- ident string ")$" -- if newline after the "if", then a newline after "endif" 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 :: GenParser Char TemplateState String ident = do first <- letter rest <- many (alphaNum <|> oneOf "_-") let id' = first : rest if id' `elem` reservedWords then pzero else return id' variable :: GenParser 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 -> ""