module Zinza.Parser (parseTemplate) where
import Control.Applicative (many, optional, some, (<|>))
import Control.Monad (void)
import Data.Char (isAlphaNum, isLower)
import Data.Maybe (isJust)
import Text.Parsec
(eof, getPosition, lookAhead, notFollowedBy, parse, anyChar, satisfy, try)
import Text.Parsec.Char (char, space, spaces, string)
import Text.Parsec.Pos (SourcePos, sourceColumn, sourceLine)
import Text.Parsec.String (Parser)
import Zinza.Errors
import Zinza.Expr
import Zinza.Node
import Zinza.Pos
import Zinza.Var
parseTemplate
:: FilePath
-> String
-> Either ParseError (Nodes Var)
parseTemplate input contents
= either (Left . ParseError . show) Right
$ parse (nodesP <* eof) input contents
toLoc :: SourcePos -> Loc
toLoc p = Loc (sourceLine p) (sourceColumn p)
varP :: Parser Var
varP = (:) <$> satisfy isLower <*> many (satisfy isVarChar)
located :: Parser a -> Parser (Located a)
located p = do
pos <- getPosition
L (toLoc pos) <$> p
locVarP :: Parser (Located Var)
locVarP = located varP
isVarChar :: Char -> Bool
isVarChar c = isAlphaNum c || c == '_'
nodeP :: Parser (Node Var)
nodeP = commentP <|> directiveP <|> exprNodeP <|> newlineN <|> rawP
nodesP :: Parser (Nodes Var)
nodesP = many nodeP
newlineN :: Parser (Node Var)
newlineN = NRaw . pure <$> char '\n'
rawP :: Parser (Node Var)
rawP = mk <$> some rawCharP <*> optional (char '\n') where
rawCharP = notBrace <|> try (char '{' <* lookAhead notSpecial)
notBrace = satisfy $ \c -> c /= '{' && c /= '\n'
notSpecial = satisfy $ \c -> c /= '{' && c /= '%' && c /= '#'
mk s Nothing = NRaw s
mk s (Just c) = NRaw (s ++ [c])
exprNodeP :: Parser (Node Var)
exprNodeP = do
_ <- try (string "{{")
spaces
expr <- located exprP
spaces
_ <- string "}}"
return (NExpr expr)
exprP :: Parser (Expr Var)
exprP = do
b <- optional (char '!')
v@(L l _) <- locVarP
vs <- many (char '.' *> locVarP)
let expr = foldl (\e f -> EField (L l e) f) (EVar v) vs
return $
if isJust b
then ENot (L l expr)
else expr
commentP :: Parser (Node var)
commentP = do
pos <- getPosition
_ <- try (string "{#")
go pos
where
go pos = do
c <- anyChar
case c of
'#' -> do
c' <- anyChar
case c' of
'}' -> NComment <$ eatNewlineWhen (sourceColumn pos == 1)
_ -> go pos
_ -> go pos
eatNewlineWhen :: Bool -> Parser ()
eatNewlineWhen False = return ()
eatNewlineWhen True = void (optional (char '\n'))
directiveP :: Parser (Node Var)
directiveP = forP <|> ifP
spaces1 :: Parser ()
spaces1 = space *> spaces
open :: String -> Parser Bool
open n = do
pos <- getPosition
_ <- try $ string "{%" *> spaces *> string n *> spaces
return $ sourceColumn pos == 1
close :: String -> Parser ()
close n = do
on0 <- open ("end" ++ n)
close' on0
close' :: Bool -> Parser ()
close' on0 = do
_ <- string "%}"
eatNewlineWhen on0
forP :: Parser (Node Var)
forP = do
on0 <- open "for"
var <- varP
spaces1
_ <- string "in"
notFollowedBy $ satisfy isAlphaNum
spaces1
expr <- located exprP
spaces1
close' on0
ns <- nodesP
close "for"
return $ NFor var expr (abstract1 var <$> ns)
ifP :: Parser (Node Var)
ifP = do
on0 <- open "if"
expr <- located exprP
spaces
close' on0
ns <- nodesP
closing (NIf expr ns)
where
closing mk = closeIf mk <|> elifP mk <|> elseP mk
closeIf mk = do
close "if"
return (mk [])
elseP mk = do
on0 <- open "else"
close' on0
ns <- nodesP
close "if"
return (mk ns)
elifP mk = do
on0 <- open "elif"
expr <- located exprP
spaces
close' on0
ns <- nodesP
closing (mk . pure . NIf expr ns)