replace-attoparsec-1.0.0.0: src/Replace/Attoparsec/ByteString.hs
-- |
-- Module : Replace.Attoparsec.ByteString
--
-- __Replace.Attoparsec__ is for finding text patterns, and also editing and
-- replacing the found patterns.
-- This activity is traditionally done with regular expressions,
-- but __Replace.Attoparsec__ uses "Data.Attoparsec" parsers instead for
-- the pattern matching.
--
-- __Replace.Attoparsec__ can be used in the same sort of “pattern capture”
-- or “find all” situations in which one would use Python
-- <https://docs.python.org/3/library/re.html#re.findall re.findall>,
-- or Perl
-- <https://perldoc.perl.org/functions/m.html m//>,
-- or Unix
-- <https://www.gnu.org/software/grep/ grep>.
--
-- __Replace.Attoparsec__ can be used in the same sort of “stream editing”
-- or “search-and-replace” situations in which one would use Python
-- <https://docs.python.org/3/library/re.html#re.sub re.sub>,
-- or Perl
-- <https://perldoc.perl.org/functions/s.html s///>,
-- or Unix
-- <https://www.gnu.org/software/sed/manual/html_node/The-_0022s_0022-Command.html sed>,
-- or
-- <https://www.gnu.org/software/gawk/manual/gawk.html awk>.
--
-- See the __[replace-attoparsec](https://hackage.haskell.org/package/replace-attoparsec)__ package README for usage examples.
{-# LANGUAGE LambdaCase #-}
module Replace.Attoparsec.ByteString
(
-- * Parser combinator
sepCap
, findAll
, findAllCap
-- * Running parser
, streamEdit
, streamEditT
-- * Parser
, getOffset
)
where
-- import Control.Exception (SomeException, throw)
import Data.Functor.Identity
import Data.Bifunctor
import Control.Applicative
import Control.Monad
import Data.Attoparsec.ByteString
import qualified Data.ByteString as B
import GHC.Word
import qualified Data.Attoparsec.Internal.Types as AT
-- |
-- == Separate and capture
--
-- Parser combinator to find all of the non-overlapping ocurrences
-- of the pattern @sep@ in a text stream. Separate the stream into sections:
--
-- * sections which can parsed by the pattern @sep@ will be captured as
-- matching sections in 'Right'
-- * non-matching sections of the stream will be captured in 'Left'.
--
-- This parser will always consume its entire input and can never fail.
-- If there are no pattern matches, then the entire input stream will be
-- returned as a non-matching 'Left' section.
--
-- The pattern matching parser @sep@ will not be allowed to succeed without
-- consuming any input. If we allow the parser to match a zero-width pattern,
-- then it can match the same zero-width pattern again at the same position
-- on the next iteration, which would result in an infinite number of
-- overlapping pattern matches. So, for example, the
-- pattern @many digit@, which can match zero occurences of a digit,
-- will be treated by @sepCap@ as @many1 digit@, and required to match
-- at least one digit.
--
-- This @sepCap@ parser combinator is the basis for all of the other
-- features of this module. It is similar to the @sep*@ family of functions
-- found in
-- <http://hackage.haskell.org/package/parser-combinators/docs/Control-Monad-Combinators.html parser-combinators>
-- and
-- <http://hackage.haskell.org/package/parsers/docs/Text-Parser-Combinators.html parsers>
-- but, importantly, it returns the parsed result of the @sep@ parser instead
-- of throwing it away.
--
{-# INLINABLE sepCap #-}
sepCap
:: Parser a -- ^ The pattern matching parser @sep@
-> Parser [Either B.ByteString a]
sepCap sep = (fmap.fmap) (first B.pack)
$ fmap sequenceLeft
$ many $ fmap Right (consumeSome sep) <|> fmap Left anyWord8
-- TODO We might consider accumulating a Builder for Left instead
-- of returning a list of Word8.
-- Would expect faster for sparse patterns, slower for dense
-- patterns.
where
sequenceLeft :: [Either Word8 r] -> [Either [Word8] r]
sequenceLeft = foldr consLeft []
where
consLeft :: Either l r -> [Either [l] r] -> [Either [l] r]
consLeft (Left l) ((Left ls):xs) = (Left (l:ls)):xs
consLeft (Left l) xs = (Left [l]):xs
consLeft (Right r) xs = (Right r):xs
-- If sep succeeds and consumes 0 input tokens, we must force it to fail,
-- otherwise infinite loop
consumeSome p = do
offset1 <- getOffset
x <- p
offset2 <- getOffset
when (offset1 >= offset2) empty
return x
-- |
-- == Find all occurences, parse and capture pattern matches
--
-- Parser combinator for finding all occurences of a pattern in a stream.
--
-- Will call 'sepCap' with the 'Data.Attoparsec.ByteString.match' combinator so that
-- the text which matched the pattern parser @sep@ will be returned in
-- the 'Right' sections, along with the result of the parse of @sep@.
--
-- Definition:
--
-- @
-- findAllCap sep = 'sepCap' ('Data.Attoparsec.ByteString.match' sep)
-- @
{-# INLINABLE findAllCap #-}
findAllCap
:: Parser a -- ^ The pattern matching parser @sep@
-> Parser [Either B.ByteString (B.ByteString, a)]
findAllCap sep = sepCap (match sep)
-- |
-- == Find all occurences
--
-- Parser combinator for finding all occurences of a pattern in a stream.
--
-- Will call 'sepCap' with the 'Data.Attoparsec.ByteString.match' combinator and
-- return the text which matched the pattern parser @sep@ in
-- the 'Right' sections.
--
-- Definition:
--
-- @
-- findAll sep = (fmap.fmap) ('Data.Bifunctor.second' fst) $ 'sepCap' ('Data.Attoparsec.ByteString.match' sep)
-- @
{-# INLINABLE findAll #-}
findAll
:: Parser a -- ^ The pattern matching parser @sep@
-> Parser [Either B.ByteString B.ByteString]
findAll sep = (fmap.fmap) (second fst) $ sepCap (match sep)
-- |
-- == Stream editor
--
-- Also known as “find-and-replace”, or “match-and-substitute”. Finds all
-- of the sections of the stream which match the pattern @sep@, and replaces
-- them with the result of the @editor@ function.
--
-- This function is not a “parser combinator,” it is
-- a “way to run a parser”, like 'Data.Attoparsec.ByteString.parse'
-- or 'Data.Attoparsec.ByteString.parseOnly'.
--
-- === Access the matched section of text in the @editor@
--
-- If you want access to the matched string in the @editor@ function,
-- then combine the pattern parser @sep@
-- with 'Data.Attoparsec.ByteString.match'. This will effectively change
-- the type of the @editor@ function to @(s,a) -> s@.
--
-- This allows us to write an @editor@ function which can choose to not
-- edit the match and just leave it as it is. If the @editor@ function
-- always returns the first item in the tuple, then @streamEdit@ changes
-- nothing.
--
-- So, for all @sep@:
--
-- @
-- streamEdit ('Data.Attoparsec.ByteString.match' sep) 'Data.Tuple.fst' ≡ 'Data.Function.id'
-- @
{-# INLINABLE streamEdit #-}
streamEdit
-- :: forall s a. (Stream s, Monoid s, Tokens s ~ s, Show s, Show (Token s), Typeable s)
:: Parser a
-- ^ The parser @sep@ for the pattern of interest.
-> (a -> B.ByteString)
-- ^ The @editor@ function. Takes a parsed result of @sep@
-- and returns a new stream section for the replacement.
-> B.ByteString
-- ^ The input stream of text to be edited.
-> B.ByteString
streamEdit sep editor = runIdentity . streamEditT sep (Identity . editor)
-- |
-- == Stream editor transformer
--
-- Monad transformer version of 'streamEdit'.
--
-- The @editor@ function will run in the underlying monad context.
--
-- If you want to do 'IO' operations in the @editor@ function then
-- run this in 'IO'.
--
-- If you want the @editor@ function to remember some state,
-- then run this in a stateful monad.
{-# INLINABLE streamEditT #-}
streamEditT
:: (Monad m)
=> Parser a
-- ^ The parser @sep@ for the pattern of interest.
-> (a -> m B.ByteString)
-- ^ The @editor@ function. Takes a parsed result of @sep@
-- and returns a new stream section for the replacement.
-> B.ByteString
-- ^ The input stream of text to be edited.
-> m B.ByteString
streamEditT sep editor input = do
case parseOnly (sepCap sep) input of
(Left err) -> error err
-- this function should never error, because it only errors
-- when the 'sepCap' parser fails, and the 'sepCap' parser
-- can never fail. If this function ever throws an error, please
-- report that as a bug.
-- (We don't use MonadFail because Identity is not a MonadFail.)
(Right r) -> fmap mconcat $ traverse (either return editor) r
-- | Get the 'Data.Attoparsec.ByteString.Parser' ’s current offset position in the stream.
--
-- [“… you know you're in an uncomfortable state of sin :-)” — bos](https://github.com/bos/attoparsec/issues/101)
getOffset :: Parser Int
getOffset = AT.Parser $ \t pos more _ succ' -> succ' t pos more (AT.fromPos pos)