{-# 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 :: [ByteString] -> ByteString
findPrefix = ByteString -> ByteString
takePrefix (ByteString -> ByteString)
-> ([ByteString] -> ByteString) -> [ByteString] -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [ByteString] -> ByteString
findCommonPrefix ([ByteString] -> ByteString)
-> ([ByteString] -> [ByteString]) -> [ByteString] -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [ByteString] -> [ByteString]
dropNewlines

-- | Removes the given prefix from the passed `ByteString`, or raises an error.
stripPrefix :: ByteString -> ByteString -> ByteString
stripPrefix :: ByteString -> ByteString -> ByteString
stripPrefix ByteString
prefix =
  ByteString -> Maybe ByteString -> ByteString
forall a. a -> Maybe a -> a
fromMaybe ([Char] -> ByteString
forall a. HasCallStack => [Char] -> a
error [Char]
"Missing expected prefix") (Maybe ByteString -> ByteString)
-> (ByteString -> Maybe ByteString) -> ByteString -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> ByteString -> Maybe ByteString
S.stripPrefix ByteString
prefix

-- | Add a prefix to all lines in a `ByteString`.
addPrefix :: ByteString -> ByteString -> ByteString
addPrefix :: ByteString -> ByteString -> ByteString
addPrefix ByteString
prefix = [ByteString] -> ByteString
unlines' ([ByteString] -> ByteString)
-> (ByteString -> [ByteString]) -> ByteString -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (ByteString -> ByteString) -> [ByteString] -> [ByteString]
forall a b. (a -> b) -> [a] -> [b]
map (ByteString
prefix ByteString -> ByteString -> ByteString
forall a. Semigroup a => a -> a -> a
<>) ([ByteString] -> [ByteString])
-> (ByteString -> [ByteString]) -> ByteString -> [ByteString]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> [ByteString]
S8.lines

-- | Returns the prefix that all the given `ByteString`s have.
findCommonPrefix :: [ByteString] -> ByteString
findCommonPrefix :: [ByteString] -> ByteString
findCommonPrefix [] = ByteString
""
findCommonPrefix (ByteString
"":[ByteString]
_) = ByteString
""
findCommonPrefix (ByteString
p:[ByteString]
ps) =
  if (ByteString -> Bool) -> [ByteString] -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all (Char -> ByteString -> Bool
startsWithChar Char
first) [ByteString]
ps
    then Char -> ByteString -> ByteString
S8.cons Char
first ([ByteString] -> ByteString
findCommonPrefix (HasCallStack => ByteString -> ByteString
ByteString -> ByteString
S.tail ByteString
p ByteString -> [ByteString] -> [ByteString]
forall a. a -> [a] -> [a]
: (ByteString -> ByteString) -> [ByteString] -> [ByteString]
forall a b. (a -> b) -> [a] -> [b]
map HasCallStack => ByteString -> ByteString
ByteString -> ByteString
S.tail [ByteString]
ps))
    else ByteString
""
  where
    first :: Char
first = ByteString -> Char
S8.head ByteString
p

-- | `unlines'` for `ByteString`.
unlines' :: [ByteString] -> ByteString
unlines' :: [ByteString] -> ByteString
unlines' = [ByteString] -> ByteString
S.concat ([ByteString] -> ByteString)
-> ([ByteString] -> [ByteString]) -> [ByteString] -> ByteString
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> [ByteString] -> [ByteString]
forall a. a -> [a] -> [a]
intersperse ByteString
"\n"

-- | Returns the prefix from the `ByteString`
--
-- The regex of the prefix is `>?[ \t]*`.
takePrefix :: ByteString -> ByteString
takePrefix :: ByteString -> ByteString
takePrefix ByteString
txt
  | ByteString -> Bool
S8.null ByteString
txt = ByteString
""
  | ByteString -> Char
S8.head ByteString
txt Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'>' = Char -> ByteString -> ByteString
S8.cons Char
'>' (ByteString -> ByteString) -> ByteString -> ByteString
forall a b. (a -> b) -> a -> b
$ ByteString -> ByteString
takeSpaceOrTab (ByteString -> ByteString) -> ByteString -> ByteString
forall a b. (a -> b) -> a -> b
$ HasCallStack => ByteString -> ByteString
ByteString -> ByteString
S8.tail ByteString
txt
  | Bool
otherwise = ByteString -> ByteString
takeSpaceOrTab ByteString
txt

-- | Filters out `ByteString`s composed of only `\n`s.
dropNewlines :: [ByteString] -> [ByteString]
dropNewlines :: [ByteString] -> [ByteString]
dropNewlines = (ByteString -> Bool) -> [ByteString] -> [ByteString]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (ByteString -> Bool) -> ByteString -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. ByteString -> Bool
S.null (ByteString -> Bool)
-> (ByteString -> ByteString) -> ByteString -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> ByteString -> ByteString
S8.dropWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'\n'))

-- | `takeWhile` for spaces or tabs
takeSpaceOrTab :: ByteString -> ByteString
takeSpaceOrTab :: ByteString -> ByteString
takeSpaceOrTab = (Char -> Bool) -> ByteString -> ByteString
S8.takeWhile Char -> Bool
isSpaceOrTab

-- | Does the strict bytestring have a trailing newline?
hasTrailingLine :: ByteString -> Bool
hasTrailingLine :: ByteString -> Bool
hasTrailingLine ByteString
xs = Bool -> Bool
not (ByteString -> Bool
S8.null ByteString
xs) Bool -> Bool -> Bool
&& ByteString -> Char
S8.last ByteString
xs Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
'\n'

-- | Returns if the `ByteString` starts with the given `Char`.
startsWithChar :: Char -> ByteString -> Bool
startsWithChar :: Char -> ByteString -> Bool
startsWithChar Char
c ByteString
x = ByteString -> Int
S8.length ByteString
x Int -> Int -> Bool
forall a. Ord a => a -> a -> Bool
> Int
0 Bool -> Bool -> Bool
&& ByteString -> Char
S8.head ByteString
x Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
c

-- | Returns if the `Char` is either a space or a tab.
isSpaceOrTab :: Char -> Bool
isSpaceOrTab :: Char -> Bool
isSpaceOrTab = (Char -> [Char] -> Bool
forall a. Eq a => a -> [a] -> Bool
forall (t :: * -> *) a. (Foldable t, Eq a) => a -> t a -> Bool
`elem` [Char
' ', Char
'\t'])