module Data.String.Here.Interpolated (i, iTrim, template) where
import Control.Applicative hiding ((<|>))
import Control.Monad.State
import Data.Char
import Data.Maybe
import Data.Monoid
import Data.String
import Data.Typeable
import Language.Haskell.Meta
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Text.Parsec
import Text.Parsec.Prim
import Text.Parsec.String
import Data.String.Here.Internal
data StringPart = Lit String | Esc Char | Anti (Q Exp)
data HsChompState = HsChompState { quoteState :: QuoteState
, braceCt :: Int
, consumed :: String
, prevCharWasIdentChar :: Bool
}
data QuoteState = None | Single EscapeState | Double EscapeState
data EscapeState = Escaped | Unescaped
i :: QuasiQuoter
i = QuasiQuoter {quoteExp = quoteInterp}
iTrim :: QuasiQuoter
iTrim = QuasiQuoter {quoteExp = quoteInterp . trim}
template :: QuasiQuoter
template = quoteDependentFile i
quoteInterp :: String -> Q Exp
quoteInterp s = either (handleError s) combineParts (parseInterp s)
handleError :: String -> ParseError -> Q Exp
handleError expStr parseError = error $
"Failed to parse interpolated expression in string: "
++ expStr
++ "\n"
++ show parseError
combineParts :: [StringPart] -> Q Exp
combineParts = combine . map toExpQ
where
toExpQ (Lit s) = stringE s
toExpQ (Esc c) = stringE [c]
toExpQ (Anti expq) = [|toString $expq|]
combine [] = stringE ""
combine parts = foldr1 (\subExpr acc -> [|$subExpr <> $acc|]) parts
toString :: (Show a, Typeable a, Typeable b, IsString b) => a -> b
toString x = fromMaybe (fromString $ show x) (cast x)
parseInterp :: String -> Either ParseError [StringPart]
parseInterp = parse p_interp ""
p_interp :: Parser [StringPart]
p_interp = manyTill p_stringPart eof
p_stringPart :: Parser StringPart
p_stringPart = p_anti <|> p_esc <|> p_lit
p_anti :: Parser StringPart
p_anti = Anti <$> between (try p_antiOpen) p_antiClose p_antiExpr
p_antiOpen :: Parser String
p_antiOpen = string "${"
p_antiClose :: Parser String
p_antiClose = string "}"
p_antiExpr :: Parser (Q Exp)
p_antiExpr = p_untilUnbalancedCloseBrace
>>= either fail (return . return) . parseExp
p_untilUnbalancedCloseBrace :: Parser String
p_untilUnbalancedCloseBrace = evalStateT go $ HsChompState None 0 "" False
where
go = do
c <- lift anyChar
modify $ \st@HsChompState {consumed} -> st {consumed = c:consumed}
HsChompState {..} <- get
let next = setIdentifierCharState c >> go
case quoteState of
None -> case c of
'{' -> incBraceCt 1 >> next
'}' | braceCt > 0 -> incBraceCt (1) >> next
| otherwise -> stepBack >> return (reverse $ tail consumed)
'\'' -> unless prevCharWasIdentChar (setQuoteState $ Single Unescaped)
>> next
'"' -> setQuoteState (Double Unescaped) >> next
_ -> next
Single Unescaped -> do case c of '\\' -> setQuoteState (Single Escaped)
'\'' -> setQuoteState None
_ -> return ()
next
Single Escaped -> setQuoteState (Single Unescaped) >> next
Double Unescaped -> do case c of '\\' -> setQuoteState (Double Escaped)
'"' -> setQuoteState None
_ -> return ()
next
Double Escaped -> setQuoteState (Double Unescaped) >> next
stepBack = lift $
updateParserState
(\s -> s {statePos = incSourceColumn (statePos s) (1)})
>> getInput
>>= setInput . ('}':)
incBraceCt n = modify $ \st@HsChompState {braceCt} ->
st {braceCt = braceCt + n}
setQuoteState qs = modify $ \st -> st {quoteState = qs}
setIdentifierCharState c = modify $ \st ->
st
{prevCharWasIdentChar = or [isLetter c, isDigit c, c == '_', c == '\'']}
p_esc :: Parser StringPart
p_esc = Esc <$> (char '\\' *> anyChar)
p_lit :: Parser StringPart
p_lit = fmap Lit $
try (litCharTil $ try $ lookAhead p_antiOpen <|> lookAhead (string "\\"))
<|> litCharTil eof
where litCharTil = manyTill $ noneOf ['\\']