packages feed

regex-parsec-0.90: Text/Regex/Parsec/RegexParsecState.hs

{-# OPTIONS -funbox-strict-fields #-}
-- | The Parsec parser needs to keep track of various state
-- information while matching a regular expression.  This includes the
-- number of accepted characters and the in-progross and completed
-- substring matches.  This module defines that state and some
-- convenience functions.
--
-- A user defined state can be maintained via newState, getUserState,
-- setUserState, and updateUserState.
--
-- The 'FullState' type is opaque, allowing for better abstraction.
-- Note that calling stopSub when startSub has not been called will
-- trigger an 'error', and that initState/finalState call startSub
-- 0/stopSub 0 respectively.
module Text.Regex.Parsec.RegexParsecState 
    (
 -- ** Create the full state of the parsec parser
     initState,finalState,newState,initStateP,finalStateP
 -- ** Manipulate the user state of the parser
    ,getUserState,setUserState,updateUserState
 -- ** Manupulate the accepted character counter
    ,incState,plusState,lookupAccepted
-- ** Compare Matched Strings
    ,eqSubs
 -- ** Manipulate the captured substrings, PCRE style
    ,startSub,stopSub,lookupSub,lookupSubs
 -- ** Manipulate the captured substrings, Posix style
    ,startSubP,stopSubP,lookupSubP,lookupSubsP
    ) where

{- By Chris Kuklewicz, 2006. BSD License, see the LICENSE file. -}

import Text.Regex.Parsec.Common(RegexParser, MatchedStrings, FullState(..), Opened(..), Closed(..))
import Text.ParserCombinators.Parsec(updateState, setState, getState, getInput)
import Control.Monad(liftM)
import qualified Data.IntMap as I(fromAscList,toAscList, updateLookupWithKey,insert, empty, lookup)

trace :: a -> b -> b
trace _ b = b

-- | 'initState' forgets all substring matching (in-progress or
-- complete) and starts sub 0 (the whole match).  This operation
-- preserves the user state and accepted character count
initState :: RegexParser userState ()
initState = do -- keep only userState and accepted, reset capture
  updateState $ \state -> state {openSub = I.empty, closedSub = I.empty,posixSub = EndOpened []}
  startSub 0

initStateP :: RegexParser userState ()
initStateP = do -- keep only userState and accepted, reset capture
  updateState $ \state -> state {openSub = I.empty, closedSub = I.empty,posixSub = EndOpened []}
  startSubP 0

-- | 'finalState' stops sub 0 (the whole match) and returns an IntMap
-- of all the completed substring matches.  Any in-progress matches
-- are ignored.
finalState :: RegexParser userState MatchedStrings
finalState = do stopSub 0
                lookupSubs

-- | 'finalState' stops sub 0 (the whole match) and returns an IntMap
-- of all the completed substring matches.  Any in-progress matches
-- are ignored.
finalStateP :: RegexParser userState MatchedStrings
finalStateP = do stopSubP 0
                 lookupSubsP

-- | This takes a value of the user state and returns the full parsec
-- state with zero accepted characters to n and no in-progress or completed
-- substring matches.
newState :: Int -> userState -> FullState userState
newState n user = FullState {userState = user
                            ,accepted = n
                            ,posixSub = EndOpened []
                            ,openSub = I.empty
                            ,closedSub = I.empty}

-- | This returns the value of the user state
getUserState :: RegexParser userState userState
getUserState = liftM userState getState

-- | The replaces the user state with the provided value
setUserState :: userState -> RegexParser userState ()
setUserState user = do state <- getState
                       setState $ state {userState = user}

-- | This applies the given function to the current value of the user state
updateUserState :: (userState -> userState) -> RegexParser userState ()
updateUserState f = updateState (\state@FullState{userState=user}->state{userState=f user})

-- | This adds one to the number of accepted characters
incState :: RegexParser userState ()
incState = updateState (\state@(FullState {accepted=pos})->state {accepted=succ pos})

-- | This adds the provided number to the count of accepted characters
plusState :: Int -> RegexParser userState ()
plusState n = updateState (\state@(FullState {accepted=pos})->state {accepted=n + pos})


-- | 'lookupAccepted' return the number of accepted input characters.
-- This is typicaly counted from the call to initState.
lookupAccepted :: RegexParser userState Int
lookupAccepted = liftM accepted getState

-- | Called with PatternIndex i, this makes the i'th substring match
-- inprogress and marks the start of the i'th substring match at the
-- current position.  This does affect any value for the completed
-- i'th substring match.
startSub :: Int -> RegexParser userState ()
startSub i = do 
  state@(FullState {accepted=n,openSub=open}) <- getState
  here <- getInput
  let open' = I.insert i (here,n) open
      state' = state {openSub = open'}
  setState state'

-- | Called with PatternIndex i, this captures the input from point
-- where startSub i was called until the current location.  It removes
-- the in-progress i'th substring and sets the i'th completed
-- substring to the new value, overwriting any previous value.
--
-- The completed value is available from lookupSub i or in the map
-- provided by lookupSubs.
--
-- If the i'th substring is not open when this is called then stopSub
-- will call 'error'.
stopSub :: Int -> RegexParser userState ()
stopSub i = do
  state@(FullState {accepted=n,openSub=open,closedSub=closed}) <- getState
  let (mEntry,open') = I.updateLookupWithKey del i open
      del _ _ = Nothing
  case mEntry of
    Nothing -> error ("RegexParsecState: Could not closeSub "++show i)
    Just (here,pos) -> let len = n-pos
                           sub = take len here
                           closed' = I.insert i (sub,(pos,len)) closed
                           state' = state {openSub=open',closedSub=closed'}
                       in setState state'

-- | 'lookupSub' i returns Just the completed captured between 'startSub'
-- i and 'stopSub' i or Nothing if there has been no capture.
lookupSub :: Int -> RegexParser userState (Maybe String)
lookupSub i = do msol <- liftM ((I.lookup i).closedSub) getState
                 case msol of
                   Nothing -> return Nothing
                   Just (sub,_) -> return (Just sub)

-- | 'lookupSubs' returns an IntMap of all the completed substring captures.
lookupSubs :: RegexParser userState MatchedStrings
lookupSubs = liftM closedSub getState

eqSubs :: MatchedStrings -> MatchedStrings -> Bool
eqSubs a b = let a' = map (\ (k,(_,ol)) -> (k,ol)) . I.toAscList $ a
                 b' = map (\ (k,(_,ol)) -> (k,ol)) . I.toAscList $ b
             in a' == b'

startSubP :: Int -> RegexParser userState ()
startSubP i = trace ("startSub: "++show i) $ do
  state@(FullState {accepted=pos,posixSub=sub}) <- getState
  trace (">startSubP "++show i++" : "++show sub) $ return ()  
  here <- getInput
  let sub' = 
        case sub of
          Opened closed j dat next ->
            case break (\(Closed _ k _)->i==k) closed of
              (_,[]) -> Opened [] i (here,pos) sub
              (_,_:closed') -> Opened [] i (here,pos) (Opened closed' j dat next)
          EndOpened closed ->
            case break (\(Closed _ k _)->i==k) closed of
              (_,[]) -> Opened [] i (here,pos) sub
              (_,_:closed') -> Opened [] i (here,pos) (EndOpened closed')
  setState (state {posixSub=sub'})
  trace ("<startSubP "++show i++" : "++show sub') $ return ()  

stopSubP :: Int -> RegexParser userstate ()
stopSubP i = trace ("stopSub: "++show i) $ do
  state@(FullState {accepted=pos',posixSub=sub}) <- getState
  trace (">stopSubP "++show i++" : "++show sub) $ return ()  
  let sub' =
        case sub of
          Opened closed j (here,pos) next | j==i ->
            let len = pos'-pos
                capture = Closed closed i (take len here,(pos,len))
            in case next of
                 Opened closed' j' dat' next' ->
                   Opened (capture:closed') j' dat' next'
                 EndOpened closed' ->
                   EndOpened (capture:closed')
                                          | otherwise ->
            error ("Malformed nesting when stopSubP group #"++show i++" when Opened group was #"++show j)
          EndOpened _ -> error ("Malformed nesting when stopSubP group #"++show i++" when Opened group was EndOpened")
  setState (state {posixSub=sub'})
  trace ("<stopSubP "++show i++" : "++show sub') $ return ()  

lookupSubP :: Int -> RegexParser userstate (Maybe String)
lookupSubP i =
  let fetchOpen (EndOpened closed) = fetchClosed closed
      fetchOpen (Opened greater j (string,_) lesser) =
        case compare i j of
          GT -> fetchClosed greater
          EQ -> Just string
          LT -> fetchOpen lesser
      fetchClosed [] = Nothing
      fetchClosed (Closed greater j (string,_) : lesser) =
        case compare i j of
          GT -> fetchClosed greater
          EQ -> Just string
          LT -> fetchClosed lesser
  in trace ("lookupSub: "++show i) $ liftM (fetchOpen . posixSub) getState

lookupSubsP :: RegexParser userstate MatchedStrings
lookupSubsP = 
  let traverseOpen acc (EndOpened closed) = traverseClosed acc closed
      traverseOpen acc (Opened greater _ _ lesser) =
        let acc' = traverseClosed acc greater
        in traverseOpen acc' lesser
      traverseClosed acc [] = acc
      traverseClosed acc (Closed greater j dat : lesser) =
        let acc' = (j,dat) : traverseClosed acc greater
        in traverseClosed acc' lesser
  in do (FullState {posixSub=sub}) <- getState
        let value = I.fromAscList . (traverseOpen []) $ sub 
        trace ("lookupSubs " ++ show sub ++ "\n" ++ show value) $ return value