packages feed

pgrep-0.1.0.0: src/Scrappy/Grep/DSL/Interpreter.hs

{-# LANGUAGE FlexibleContexts #-}

module Scrappy.Grep.DSL.Interpreter
  ( interpret
  , interpretWithRefs
  , InterpreterError(..)
  ) where

import Scrappy.Grep.DSL
import Scrappy.Scrape (ScraperT)

import Text.Parsec hiding ((<|>))
import Data.Map (Map)
import qualified Data.Map as Map
import Control.Applicative ((<|>))

data InterpreterError
  = UnknownRef String
  deriving (Show, Eq)

-- | Interpret AST without named references (refs will fail)
interpret :: ParserExpr -> Either InterpreterError (ScraperT String)
interpret = interpretWithRefs Map.empty

-- | Interpret AST with named parser references
-- The Map contains pre-interpreted parsers for each ref name
interpretWithRefs :: Map String (ScraperT String) -> ParserExpr -> Either InterpreterError (ScraperT String)
interpretWithRefs refs = go
  where
    go :: ParserExpr -> Either InterpreterError (ScraperT String)
    go expr = case expr of
      -- Primitives (return matched text as String)
      PChar c     -> Right $ (:[]) <$> char c
      PString s   -> Right $ string s
      PAnyChar    -> Right $ (:[]) <$> anyChar
      PDigit      -> Right $ (:[]) <$> digit
      PLetter     -> Right $ (:[]) <$> letter
      PAlphaNum   -> Right $ (:[]) <$> alphaNum
      PSpace      -> Right $ (:[]) <$> space
      PSpaces     -> Right $ many space
      PNewline    -> Right $ (:[]) <$> newline
      POneOf cs   -> Right $ (:[]) <$> oneOf cs
      PNoneOf cs  -> Right $ (:[]) <$> noneOf cs

      -- Sequence: run p1 then p2, return p2's result
      PSeq p1 p2 -> do
        parser1 <- go p1
        parser2 <- go p2
        Right $ parser1 *> parser2

      -- Sequence with concatenation: run both, concatenate results
      PSeqConcat p1 p2 -> do
        parser1 <- go p1
        parser2 <- go p2
        Right $ (++) <$> parser1 <*> parser2

      -- Alternative: try p1, if fails try p2
      PAlt p1 p2 -> do
        parser1 <- go p1
        parser2 <- go p2
        Right $ try parser1 <|> parser2

      -- Many: zero or more
      PMany p -> do
        parser <- go p
        Right $ concat <$> many (try parser)

      -- Some: one or more
      PSome p -> do
        parser <- go p
        Right $ concat <$> many1 (try parser)

      -- Optional: zero or one
      POptional p -> do
        parser <- go p
        Right $ option "" (try parser)

      -- Try: backtracking
      PTry p -> do
        parser <- go p
        Right $ try parser

      -- Between: parse between delimiters
      PBetween open close p -> do
        parser <- go p
        Right $ do
          o <- char open
          result <- parser
          c <- char close
          pure $ [o] ++ result ++ [c]

      -- Count: exactly n repetitions
      PCount n p -> do
        parser <- go p
        Right $ concat <$> count n parser

      -- ManyTill: non-greedy match until end
      PManyTill p end -> do
        parserP <- go p
        parserEnd <- go end
        Right $ manyTillConcat parserP parserEnd

      -- Reference: lookup in refs map
      PRef name -> case Map.lookup name refs of
        Nothing -> Left $ UnknownRef name
        Just parser -> Right parser

-- | manyTill that concatenates all matched strings including the end
manyTillConcat :: ScraperT String -> ScraperT String -> ScraperT String
manyTillConcat p end = go
  where
    go = (try end) <|> do
      x <- p
      xs <- go
      pure (x ++ xs)