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 -- | Parse template into nodes. No other than syntactic checks are performed. parseTemplate :: FilePath -- ^ name of the template -> String -- ^ contents of the template -> Either ParseError (Nodes Var) parseTemplate input contents = either (Left . ParseError . show) Right $ parse (nodesP <* eof) input contents ------------------------------------------------------------------------------- -- Location ------------------------------------------------------------------------------- toLoc :: SourcePos -> Loc toLoc p = Loc (sourceLine p) (sourceColumn p) ------------------------------------------------------------------------------- -- Parser ------------------------------------------------------------------------------- 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 -- parsec counts pos from 1, not zero. 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)