packages feed

hako-1.0.0: Text/Hako/Parsing.hs

{-# LANGUAGE TemplateHaskell, FlexibleContexts #-}
module Text.Hako.Parsing
( parseTemplateFromString
) where

import Control.Monad.Trans.Class
import Data.Monoid
import Language.Haskell.TH
import Language.Haskell.Meta.Parse
import Text.Parsec
import Text.Parsec.String
import Text.Hako.Html

-- | Hako's main parser, suitable as a quoteExpr.
parseTemplateFromString :: String -> Q Exp
parseTemplateFromString s = do
  exp <- runParserT template () "Hako" s
  return $ either (error . show) id exp

data Template = Template [Dec] [Exp]

tjoin :: Template -> Template -> Template
tjoin (Template dl el) (Template dr er) =
    Template (dl ++ dr) (el ++ er)

tpack :: Template -> Exp
tpack (Template defs exps) =
    let body = if null exps
                then emptyLiteralExp
                else foldl1 expJoin exps
    in if null defs
        then body
        else LetE defs body

template :: ParsecT String () Q Exp
template = do
    tfs <- many templateFragment
    return $ tpack $ foldl1 tjoin tfs

templateFragment :: ParsecT String () Q Template
templateFragment = try templateDefFragment
                 <|> try templateLoopFragment
                 <|> templateExpFragment
                 <|> templateLitFragment
                 <?> "template fragment"

templateDefFragment :: ParsecT String () Q Template
templateDefFragment = do
    def <- blockDef
    return $ Template [def] []

templateLoopFragment :: ParsecT String () Q Template
templateLoopFragment = do
    exp <- forLoop
    return $ Template [] [exp]

templateExpFragment :: ParsecT String () Q Template
templateExpFragment = do
    exp <- haskellExpr
    return $ Template [] [exp]

templateLitFragment :: ParsecT String () Q Template
templateLitFragment = do
    exp <- literalText
    return $ Template [] [exp]

emptyLiteralExp :: Exp
emptyLiteralExp = AppE (ConE 'Html) $ LitE $ StringL ""

expJoin :: Exp -> Exp -> Exp
expJoin a b = AppE (AppE (VarE '(<>)) a) b

expWrap :: Exp -> Exp
expWrap a = AppE (VarE 'toHtml) a

-- We might have a {def const = some fixed template}
-- Or we might have {def f x y = some dynamic template using {x} and {y}}
-- We could also have patterns {def f (x:_) = first {x}}
blockDef :: ParsecT String () Q Dec
blockDef = do
    string "{def"
    space
    leader <- manyTill anyChar $ char '='
    inner <- template
    string "}"
    case parseExp ("let " ++ leader ++ " = 42 in 42") of
      Right (LetE [ValD p _ _] _) -> return $ ValD p (NormalB inner) []
      Right (LetE [FunD n [Clause ps _ _]] _) -> return $ FunD n [Clause ps (NormalB inner) []]
      Right _ -> error "the definition leader did not parse into one of the expected constructs"
      Left err -> error err

-- Turn {for x in list: <a>{x}</a>} into: mconcat . map (\x -> TEMPLATE_EXPR) list
forLoop :: ParsecT String () Q Exp
forLoop = do
    string "{for"
    space
    var <- manyTill anyChar $ string " in "
    lst <- manyTill anyChar $ char ':'
    inner <- template
    string "}"
    let varPat = either error id $ parsePat var
        funExpr = LamE [varPat] inner
        lstExpr = parseExp lst
    case lstExpr of
      Right e -> return $ VarE 'mconcat `AppE` (VarE 'map `AppE` funExpr `AppE` e)
      Left err -> error err

literalText :: ParsecT String () Q Exp
literalText = do
    str <- many1 $ noneOf "{}"
    return $ AppE (ConE 'Html) $ LitE $ StringL str

haskellExpr :: ParsecT String () Q Exp
haskellExpr = do
    e <- haskellExpr'
    return $ expWrap e

haskellExpr' :: ParsecT String () Q Exp
haskellExpr' = do
    _ <- char '{'
    src <- haskellText
    _ <- char '}'
    either fail return $ parseExp src

haskellText :: ParsecT String () Q String
haskellText = do
    parts <- many1 haskellPart
    return $ concat parts

bracedText :: ParsecT String () Q String
bracedText = do
    char '{'
    inner <- haskellText
    char '}'
    return $ "{" ++ inner ++ "}"

haskellPart :: ParsecT String () Q String
haskellPart = quotedChar
            <|> quotedEscapedChar
            <|> quotedString
            <|> bracedText
            <|> haskellOther

haskellOther :: ParsecT String () Q String
haskellOther = many1 $ noneOf "\"'{}"

quotedChar :: ParsecT String () Q String
quotedChar = do
    char '\''
    c <- noneOf "\\"
    char '\''
    return ['\'', c, '\'']

quotedEscapedChar :: ParsecT String () Q String
quotedEscapedChar = do
    char '\''
    char '\\'
    c <- anyChar
    char '\''
    return ['\'', '\\', c, '\'']

quotedString :: ParsecT String () Q String
quotedString = do
    char '"'
    strs <- many quotedStringPart
    char '"'
    let str = concat strs
    return $ "\"" ++ str ++ "\""
    where quotedStringPart = singleChar <|> escapedChar
          singleChar = do { c <- noneOf "\"\\"; return [c] }
          escapedChar = do { char '\\'; c <- anyChar; return ['\\',c] }