209 lines
7.4 KiB
Haskell
209 lines
7.4 KiB
Haskell
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
|
|
{-
|
|
Copyright (C) 2009-2010 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-2010 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.Blaze (preEscapedString, 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 _ "json" = return $ Right ""
|
|
getDefaultTemplate _ "docx" = return $ Right ""
|
|
getDefaultTemplate user "odt" = getDefaultTemplate user "opendocument"
|
|
getDefaultTemplate user "epub" = getDefaultTemplate user "html"
|
|
getDefaultTemplate user writer = do
|
|
let format = takeWhile (/='+') writer -- strip off "+lhs" if present
|
|
let fname = "templates" </> "default" <.> format
|
|
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 = 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 :: 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 "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 :: 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 -> ""
|