-- | A collection of trimming and unindenting utilities.
module Trimdent (
  trim,
  trimdent,
  unindent,
) where

import Data.Char (isSpace)
import Data.List (sort)
import Data.Maybe (listToMaybe)

-- | Trims whitespace characters from both ends of the text.
--
-- >>> trim " hello "
-- "hello"
trim :: String -> String
trim :: String -> String
trim = (Char -> Bool) -> String -> String
dropWhileRev Char -> Bool
isSpace (String -> String) -> (String -> String) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
dropWhile Char -> Bool
isSpace

dropWhileRev :: (Char -> Bool) -> String -> String
dropWhileRev :: (Char -> Bool) -> String -> String
dropWhileRev Char -> Bool
p = String -> String
forall a. [a] -> [a]
reverse (String -> String) -> (String -> String) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
dropWhile Char -> Bool
p (String -> String) -> (String -> String) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> String
forall a. [a] -> [a]
reverse

-- | Smartly unindents a multiline text.
--
-- >>> unindent "\n  def f():\n    return 1\n"
-- "\ndef f():\n  return 1\n"
unindent :: String -> String
unindent :: String -> String
unindent String
s =
  case String -> [String]
lines String
s of
    String
sHead : [String]
sTail ->
      let unindentedsHead :: String
unindentedsHead = (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
dropWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
' ') String
sHead
          minimumsTailIndent :: Maybe Int
minimumsTailIndent = String -> Maybe Int
minimumIndent (String -> Maybe Int)
-> ([String] -> String) -> [String] -> Maybe Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [String] -> String
unlines ([String] -> Maybe Int) -> [String] -> Maybe Int
forall a b. (a -> b) -> a -> b
$ [String]
sTail
          unindentedsTail :: [String]
unindentedsTail = case Maybe Int
minimumsTailIndent of
            Just Int
indent -> (String -> String) -> [String] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map (Int -> String -> String
forall a. Int -> [a] -> [a]
drop Int
indent) [String]
sTail
            Maybe Int
Nothing -> [String]
sTail
       in [String] -> String
unlines ([String] -> String) -> [String] -> String
forall a b. (a -> b) -> a -> b
$ String
unindentedsHead String -> [String] -> [String]
forall a. a -> [a] -> [a]
: [String]
unindentedsTail
    [] -> []
 where
  minimumIndent :: [Char] -> Maybe Int
  minimumIndent :: String -> Maybe Int
minimumIndent =
    [Int] -> Maybe Int
forall a. [a] -> Maybe a
listToMaybe ([Int] -> Maybe Int) -> (String -> [Int]) -> String -> Maybe Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [Int] -> [Int]
forall a. Ord a => [a] -> [a]
sort ([Int] -> [Int]) -> (String -> [Int]) -> String -> [Int]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (String -> Int) -> [String] -> [Int]
forall a b. (a -> b) -> [a] -> [b]
map String -> Int
lineIndent
      ([String] -> [Int]) -> (String -> [String]) -> String -> [Int]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (String -> Bool) -> [String] -> [String]
forall a. (a -> Bool) -> [a] -> [a]
filter (Bool -> Bool
not (Bool -> Bool) -> (String -> Bool) -> String -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> Bool
forall (t :: * -> *) a. Foldable t => t a -> Bool
null (String -> Bool) -> (String -> String) -> String -> Bool
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
dropWhile Char -> Bool
isSpace)
      ([String] -> [String])
-> (String -> [String]) -> String -> [String]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> [String]
lines

-- | Returns the amount of spaces on the first line.
lineIndent :: [Char] -> Int
lineIndent :: String -> Int
lineIndent = String -> Int
forall (t :: * -> *) a. Foldable t => t a -> Int
length (String -> Int) -> (String -> String) -> String -> Int
forall b c a. (b -> c) -> (a -> b) -> a -> c
. (Char -> Bool) -> String -> String
forall a. (a -> Bool) -> [a] -> [a]
takeWhile (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== Char
' ')

-- | Smartly unindents and trims a multiline text.
--
-- >>> trimdent "\n  def f():\n    return 1\n"
-- "def f():\n  return 1"
trimdent :: String -> String
trimdent :: String -> String
trimdent = String -> String
trim (String -> String) -> (String -> String) -> String -> String
forall b c a. (b -> c) -> (a -> b) -> a -> c
. String -> String
unindent