yaml-marked-0.2.0.1: src/Data/Yaml/Marked/Replace.hs
module Data.Yaml.Marked.Replace
( Replace
, newReplace
, replaceMarked
, ReplaceException (..)
, runReplaces
, runReplacesOnOverlapping
) where
import Prelude
import Control.Monad (void, when)
import Control.Monad.Trans.Resource (MonadThrow (..))
import Data.Bifunctor (second)
import Data.ByteString (ByteString)
import qualified Data.ByteString.Char8 as BS8
import Data.List (sortOn)
import Data.Yaml.Marked
import Numeric.Natural
import UnliftIO.Exception (Exception (..))
data Replace = Replace
{ replaceIndex :: Natural
, replacedLength :: Natural
, replacedBy :: ByteString
}
deriving stock (Eq, Show)
-- | Create a 'Replace' directly at given index and length
--
-- NB. This function is unsafe in that it can be used with negative literals and
-- will fail at runtime. Prefer 'replaceMarked'.
newReplace :: Natural -> Natural -> ByteString -> Replace
newReplace idx len bs =
Replace
{ replaceIndex = idx
, replacedLength = len
, replacedBy = bs
}
-- | Create a 'Replace' for something 'Marked'
replaceMarked :: Marked a -> ByteString -> Replace
replaceMarked Marked {..} = newReplace idx len
where
idx = locationIndex markedLocationStart
end = locationIndex markedLocationEnd
len
| end >= idx = end - idx
| otherwise = 0
data ReplaceException
= ReplaceOutOfBounds Replace Natural
| OverlappingReplace Replace
deriving stock (Eq, Show)
instance Exception ReplaceException where
displayException = \case
ReplaceOutOfBounds r bLen ->
"The replacement "
<> show r
<> " is trying to replace more characters than remain in the ByteString ("
<> show bLen
<> ")"
OverlappingReplace r ->
"The replacement "
<> show r
<> " is where an earlier replacement has already been made"
runReplaces :: MonadThrow m => [Replace] -> ByteString -> m ByteString
runReplaces = runReplacesOnOverlapping $ throwM . OverlappingReplace
runReplacesOnOverlapping
:: MonadThrow m
=> (Replace -> m a)
-- ^ What to do with the first overlapping 'Replace' if encountered
--
-- NB. the overlapping replace(s) will be ignored, but this allows you to log
-- it as a warning, or use 'throwM' to halt (which 'runReplaces' does).
-> [Replace]
-> ByteString
-> m ByteString
runReplacesOnOverlapping f rs bs = do
rs' <- filterOverlapping f $ sortOn replaceIndex rs
runReplaces' 0 "" rs' bs
runReplaces'
:: MonadThrow m
=> Natural
-> ByteString
-> [Replace]
-> ByteString
-> m ByteString
runReplaces' _ acc [] bs = pure $ acc <> bs
runReplaces' offset acc (r : rs) bs = do
(before, after) <- breakAtOffsetReplace offset r bs
let newOffset = offset + fromIntegral (BS8.length before) + replacedLength r
runReplaces' newOffset (acc <> before <> replacedBy r) rs after
filterOverlapping
:: Monad m => (Replace -> m a) -> [Replace] -> m [Replace]
filterOverlapping onOverlap = go []
where
go acc [] = pure acc
go acc (r : rs)
| any (r `precedesEndOf`) acc = do
void $ onOverlap r
go acc rs
| otherwise = go (acc <> [r]) rs
precedesEndOf :: Replace -> Replace -> Bool
precedesEndOf a b = replaceIndex a < replaceIndex b + replacedLength b
-- | Break a 'ByteString' into the content before/after a replacement
--
-- Will throw 'ReplaceException' if the 'Replace' is not valid for the given
-- input.
breakAtOffsetReplace
:: MonadThrow m
=> Natural
-- ^ An amount to shift the 'replaceIndex' by
--
-- Since this function is called recursively to incrementally replace within
-- an overall 'ByteString', to which the 'replaceIndex' is relative, we need
-- to track how much to shift it as we recur.
-> Replace
-> ByteString
-> m (ByteString, ByteString)
breakAtOffsetReplace offset r bs = do
when (rLen > bLen) $ throwM $ ReplaceOutOfBounds r bLen
pure $
second (BS8.drop $ fromIntegral rLen) $
BS8.splitAt (fromIntegral sIdx) bs
where
sIdx
| offset > replaceIndex r = error "TODO"
| otherwise = replaceIndex r - offset
rLen = replacedLength r
bLen = fromIntegral $ BS8.length bs