atomo-0.4: src/Atomo/Kernel/Regexp.hs
{-# LANGUAGE QuasiQuotes #-}
module Atomo.Kernel.Regexp where
import Control.Arrow
import Data.Char (isDigit)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Text.Regex.PCRE
import qualified Data.Array as A
import qualified Data.ByteString as BS
import qualified Data.Text as T
import Atomo
byteString :: BS.ByteString -> Value
byteString = String . decodeUtf8
data MkRegex a = RegexOK a | Failed String
instance Monad MkRegex where
return = RegexOK
fail = Failed
Failed x >>= _ = Failed x
RegexOK a >>= f = f a
data REReplace
= REChunk T.Text
| REReference Int
| RENamed String
load :: VM ()
load = do
([$p|RegexpBindings|] =::) =<< eval [$e|Object clone|]
([$p|RegexpMatch|] =::) =<< eval [$e|Object clone|]
[$p|Regexp new: (s: String) &flags: ""|] =: do
s <- getString [$e|s|]
fs <- getString [$e|flags|]
case regex s fs of
RegexOK re ->
return (Regexp re s fs (namedCaptures s))
Failed x ->
raise ["regexp-failed"] [string x]
[$p|(r: Regexp) =~ (s: String)|] =::: [$e|r matches?: s|]
[$p|(s: String) =~ (r: Regexp)|] =::: [$e|r matches?: s|]
[$p|(r: Regexp) matches?: (s: String)|] =: do
Regexp { rCompiled = re } <- here "r" >>= findRegexp
t <- getText [$e|s|]
return (Boolean (match re (encodeUtf8 t)))
[$p|(r: Regexp) match: (s: String)|] =: do
Regexp { rCompiled = re, rNamed = ns } <- here "r" >>= findRegexp
t <- getText [$e|s|]
let mr = match re (encodeUtf8 t) :: MatchResult BS.ByteString
if BS.null (mrMatch mr)
then return (particle "none")
else do
bs <- mkBindings (mrMatch mr:mrSubList mr) ns
rm <- [$e|RegexpMatch|] `newWith`
[ ("before", byteString (mrBefore mr))
, ("match", byteString (mrMatch mr))
, ("after", byteString (mrAfter mr))
, ("captures", list (map byteString (mrSubList mr)))
, ("bindings", bs)
]
return (keyParticleN ["ok"] [rm])
[$p|(s: String) replace: (r: Regexp) with: (callback: Block)|] =: do
Regexp { rCompiled = re, rNamed = ns } <- here "r" >>= findRegexp
callback <- here "callback"
t <- getText [$e|s|]
doReplace re t $ \cs -> do
bs <- mkBindings cs ns
dispatch (keyword ["join"] [bs, callback])
>>= liftM fromString . findString
[$p|(s: String) replace: (r: Regexp) with: (format: String)|] =: do
Regexp { rCompiled = re, rNamed = ns } <- here "r" >>= findRegexp
format <- getText [$e|format|]
t <- getText [$e|s|]
doReplace re t (reReplace (replacements format) ns)
[$p|(s: String) replace-all: (r: Regexp) with: (callback: Block)|] =: do
Regexp { rCompiled = re, rNamed = ns } <- here "r" >>= findRegexp
callback <- here "callback"
t <- getText [$e|s|]
doReplaceAll re t $ \cs -> do
bs <- mkBindings cs ns
dispatch (keyword ["join"] [bs, callback])
>>= liftM fromString . findString
[$p|(s: String) replace-all: (r: Regexp) with: (format: String)|] =: do
Regexp { rCompiled = re, rNamed = ns } <- here "r" >>= findRegexp
format <- getText [$e|format|]
t <- getText [$e|s|]
doReplaceAll re t (reReplace (replacements format) ns)
doReplace :: Regex -> T.Text -> ([BS.ByteString] -> VM T.Text) -> VM Value
doReplace re t f =
case ms of
[] -> return (String t)
((s, (o, l)):cs) -> do
r <- f (s:map fst cs)
return . String . T.concat $
[ T.take o t
, r
, T.drop (o + l) t
]
where
ms = getAllTextSubmatches (match re (encodeUtf8 t))
doReplaceAll :: Regex -> T.Text -> ([BS.ByteString] -> VM T.Text) -> VM Value
doReplaceAll re t f = do
rs <- forM (map (map fst) ms) f
return (String (fuse cs rs))
where
marr :: AllTextMatches (A.Array Int) (MatchText BS.ByteString)
marr = match re (encodeUtf8 t)
ms :: [[(BS.ByteString, (MatchOffset, MatchLength))]]
ms = map A.elems . A.elems $ getAllTextMatches marr
cs = chunks t (map (snd . head) ms)
chunks :: T.Text -> [(MatchOffset, MatchLength)] -> [T.Text]
chunks t [] = [t]
chunks t ((o, l):xs) = c : chunks rest updated
where
c = T.take o t
rest = T.drop (o + l) t
updated = map (first (flip (-) (o + l))) xs
fuse :: [T.Text] -> [T.Text] -> T.Text
fuse (x:xs) (r:rs) = T.concat [x, r, fuse xs rs]
fuse xs ys = T.concat (xs ++ ys)
replacements :: T.Text -> [REReplace]
replacements t
| T.null t = []
| T.head t == '$' && t `T.index` 1 == '(' =
RENamed name : replacements afterName
| T.head t == '$' =
REReference (read num) : replacements afterNum
| otherwise =
REChunk chunk : replacements rest
where
name = T.unpack (T.takeWhile (/= ')') (T.drop 2 t))
afterName = T.tail (T.dropWhile (/= ')') (T.drop 2 t))
num = T.unpack (T.takeWhile isDigit (T.tail t))
afterNum = T.dropWhile isDigit (T.tail t)
(chunk, rest) = T.span (/= '$') t
reReplace :: [REReplace] -> [(String, Int)] -> [BS.ByteString] -> VM T.Text
reReplace [] _ _ = return T.empty
reReplace (REChunk c:rs) ns bs = liftM (c `T.append`) (reReplace rs ns bs)
reReplace (REReference n:rs) ns bs =
liftM (decodeUtf8 (bs !! n) `T.append`) (reReplace rs ns bs)
reReplace (RENamed n:rs) ns bs =
case lookup n ns of
Nothing -> raise ["unknown-regexp-reference"] [string n]
Just i -> do
rest <- reReplace rs ns bs
return (decodeUtf8 (bs !! i) `T.append` rest)
mkBindings :: [BS.ByteString] -> [(String, Int)] -> VM Value
mkBindings subs names =
[$e|RegexpBindings|] `newWith` concat
[ zipWith (\n m -> ("\\" ++ show n, byteString m))
[0 :: Int ..]
subs
, map (\(n, o) -> ("\\" ++ n, byteString (subs !! o))) names
]