packages feed

custom-interpolation-0.1.0.0: src/CustomInterpolation/Parser.hs

module CustomInterpolation.Parser where

import Control.Monad.State (evalStateT, get, lift, modify, unless)
import CustomInterpolation.Config (Brackets (..), Interpolator (..))
import Data.Char (isDigit, isLetter)
import Data.Foldable (asum)
import Data.Functor (($>))
import Language.Haskell.Meta (parseExp)
import Language.Haskell.TH (Exp, Q)
import Text.Parsec (
  ParseError,
  State (statePos),
  anyChar,
  between,
  char,
  eof,
  getInput,
  incSourceColumn,
  lookAhead,
  manyTill,
  noneOf,
  parse,
  setInput,
  string,
  try,
  updateParserState,
  (<|>),
 )
import Text.Parsec.String (Parser)

-- | The raw segments the parser will cut the quasi-quoted string into
data StringPart a = Lit String | Esc Char | Anti (Interpolator a) (Q Exp)

data HsChompState = HsChompState
  { quoteState :: QuoteState,
    braceCt :: Int,
    consumed :: String,
    prevCharWasIdentChar :: Bool
  }
  deriving (Show)

data QuoteState = None | Single EscapeState | Double EscapeState deriving (Eq, Ord, Show)

data EscapeState = Escaped | Unescaped deriving (Bounded, Enum, Eq, Ord, Show)

parseInterpolations :: [Interpolator a] -> String -> Either ParseError [StringPart a]
parseInterpolations interps = parse (pInterp interps) ""

pInterp :: [Interpolator a] -> Parser [StringPart a]
pInterp interps = manyTill (pStringPart interps) eof

pStringPart :: [Interpolator a] -> Parser (StringPart a)
pStringPart interps = asum (map pAnti interps) <|> pEsc <|> pLit interps

pAnti :: Interpolator a -> Parser (StringPart a)
pAnti interp =
  Anti interp . parseExpQ
    <$> between
      (try (pAntiOpen interp))
      (pAntiClose interp)
      (pUntilUnbalancedCloseBracket interp)

-- | 'parseExp' but in the 'Q' Monad ('fail's on parsing errors).
parseExpQ :: String -> Q Exp
parseExpQ = either (fail . ("Error while parsing Haskell expression:\n" <>)) pure . parseExp

pAntiOpen :: Interpolator a -> Parser String
pAntiOpen Interpolator {prefix, brackets} = string (prefix ++ [opening brackets])

pAntiClose :: Interpolator a -> Parser String
pAntiClose Interpolator {brackets} = string [closing brackets]

pUntilUnbalancedCloseBracket :: Interpolator a -> Parser String
pUntilUnbalancedCloseBracket Interpolator {brackets = Brackets {opening, closing}} =
  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 ->
          if
              | c == opening -> incBraceCt 1 *> next
              | c == closing ->
                if braceCt > 0
                  then incBraceCt (-1) *> next
                  else stepBack $> reverse (tail consumed)
              | c == '\'' ->
                unless prevCharWasIdentChar (setQuoteState $ Single Unescaped)
                  *> next
              | c == '"' -> setQuoteState (Double Unescaped) *> next
              | otherwise -> next
        Single Unescaped -> do
          case c of
            '\\' -> setQuoteState (Single Escaped)
            '\'' -> setQuoteState None
            _ -> pure ()
          next
        Single Escaped -> setQuoteState (Single Unescaped) *> next
        Double Unescaped -> do
          case c of
            '\\' -> setQuoteState (Double Escaped)
            '"' -> setQuoteState None
            _ -> pure ()
          next
        Double Escaped -> setQuoteState (Double Unescaped) *> next
    stepBack =
      lift $
        updateParserState
          (\s -> s {statePos = incSourceColumn (statePos s) (-1)})
          *> getInput
          >>= setInput . (closing :)
    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 == '\'']
        }

pEsc :: Parser (StringPart a)
pEsc = Esc <$> (char '\\' *> anyChar)

pLit :: [Interpolator a] -> Parser (StringPart a)
pLit prefixes =
  Lit
    <$> ( try (litCharTill $ try $ asum (map (lookAhead . pAntiOpen) prefixes) <|> lookAhead (string "\\"))
            <|> litCharTill eof
        )
  where
    litCharTill = manyTill $ noneOf ['\\']