{-# LANGUAGE OverloadedStrings #-} -- | Helper functions to manipulate `ByteString`. module HIndent.ByteString ( findPrefix , stripPrefix , addPrefix , unlines' , hasTrailingLine ) where import Data.ByteString (ByteString) import qualified Data.ByteString as S import qualified Data.ByteString.Char8 as S8 import Data.List hiding (stripPrefix) import Data.Maybe -- | Returns the prefix that all the given `ByteString`s except for the ones composed of `\n`s have. -- -- The regex of the prefix is `>?[ \t]*`. findPrefix :: [ByteString] -> ByteString findPrefix = takePrefix . findCommonPrefix . dropNewlines -- | Removes the given prefix from the passed `ByteString`, or raises an error. stripPrefix :: ByteString -> ByteString -> ByteString stripPrefix prefix = fromMaybe (error "Missing expected prefix") . S.stripPrefix prefix -- | Add a prefix to all lines in a `ByteString`. addPrefix :: ByteString -> ByteString -> ByteString addPrefix prefix = unlines' . map (prefix <>) . S8.lines -- | Returns the prefix that all the given `ByteString`s have. findCommonPrefix :: [ByteString] -> ByteString findCommonPrefix [] = "" findCommonPrefix ("":_) = "" findCommonPrefix (p:ps) = if all (startsWithChar first) ps then S8.cons first (findCommonPrefix (S.tail p : map S.tail ps)) else "" where first = S8.head p -- | `unlines'` for `ByteString`. unlines' :: [ByteString] -> ByteString unlines' = S.concat . intersperse "\n" -- | Returns the prefix from the `ByteString` -- -- The regex of the prefix is `>?[ \t]*`. takePrefix :: ByteString -> ByteString takePrefix txt | S8.null txt = "" | S8.head txt == '>' = S8.cons '>' $ takeSpaceOrTab $ S8.tail txt | otherwise = takeSpaceOrTab txt -- | Filters out `ByteString`s composed of only `\n`s. dropNewlines :: [ByteString] -> [ByteString] dropNewlines = filter (not . S.null . S8.dropWhile (== '\n')) -- | `takeWhile` for spaces or tabs takeSpaceOrTab :: ByteString -> ByteString takeSpaceOrTab = S8.takeWhile isSpaceOrTab -- | Does the strict bytestring have a trailing newline? hasTrailingLine :: ByteString -> Bool hasTrailingLine xs = not (S8.null xs) && S8.last xs == '\n' -- | Returns if the `ByteString` starts with the given `Char`. startsWithChar :: Char -> ByteString -> Bool startsWithChar c x = S8.length x > 0 && S8.head x == c -- | Returns if the `Char` is either a space or a tab. isSpaceOrTab :: Char -> Bool isSpaceOrTab = (`elem` [' ', '\t'])