summaryrefslogtreecommitdiff
path: root/src/Text/Pandoc/Templates.hs
diff options
context:
space:
mode:
Diffstat (limited to 'src/Text/Pandoc/Templates.hs')
-rw-r--r--src/Text/Pandoc/Templates.hs211
1 files changed, 211 insertions, 0 deletions
diff --git a/src/Text/Pandoc/Templates.hs b/src/Text/Pandoc/Templates.hs
new file mode 100644
index 000000000..c30af0bfc
--- /dev/null
+++ b/src/Text/Pandoc/Templates.hs
@@ -0,0 +1,211 @@
+{-# LANGUAGE TypeSynonymInstances #-}
+{-
+Copyright (C) 2009 John MacFarlane <jgm@berkeley.edu>
+
+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 John MacFarlane
+ 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.
+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 -> ""