module Data.String.Interpolate.Util (unindent) where

import           Control.Arrow ((>>>))
import           Data.Char

-- | Remove indentation as much as possible while preserving relative
-- indentation levels.
--
-- `unindent` is useful in combination with `Data.String.Interpolate.i` to remove leading spaces that
-- resulted from code indentation.  That way you can freely indent your string
-- literals without the indentation ending up in the resulting strings.
--
-- Here is an example:
--
-- >>> :set -XQuasiQuotes
-- >>> import Data.String.Interpolate
-- >>> import Data.String.Interpolate.Util
-- >>> :{
--  putStr $ unindent [i|
--      def foo
--        23
--      end
--    |]
-- :}
-- def foo
--   23
-- end
--
-- To allow this, two additional things are being done, apart from removing
-- indentation:
--
-- - One empty line at the beginning will be removed and
-- - if the last newline character (@"\\n"@) is followed by spaces, the spaces are removed.
unindent :: String -> String
unindent :: String -> String
unindent =
      String -> [String]
lines_
  (String -> [String]) -> ([String] -> String) -> String -> String
forall k (cat :: k -> k -> *) (a :: k) (b :: k) (c :: k).
Category cat =>
cat a b -> cat b c -> cat a c
>>> [String] -> [String]
removeLeadingEmptyLine
  ([String] -> [String])
-> ([String] -> String) -> [String] -> String
forall k (cat :: k -> k -> *) (a :: k) (b :: k) (c :: k).
Category cat =>
cat a b -> cat b c -> cat a c
>>> [String] -> [String]
trimLastLine
  ([String] -> [String])
-> ([String] -> String) -> [String] -> String
forall k (cat :: k -> k -> *) (a :: k) (b :: k) (c :: k).
Category cat =>
cat a b -> cat b c -> cat a c
>>> [String] -> [String]
removeIndentation
  ([String] -> [String])
-> ([String] -> String) -> [String] -> String
forall k (cat :: k -> k -> *) (a :: k) (b :: k) (c :: k).
Category cat =>
cat a b -> cat b c -> cat a c
>>> [String] -> String
forall (t :: * -> *) a. Foldable t => t [a] -> [a]
concat
  where
    isEmptyLine :: String -> Bool
    isEmptyLine :: String -> Bool
isEmptyLine = (Char -> Bool) -> String -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all Char -> Bool
isSpace

    lines_ :: String -> [String]
    lines_ :: String -> [String]
lines_ [] = []
    lines_ s :: String
s = case (Char -> Bool) -> String -> (String, String)
forall a. (a -> Bool) -> [a] -> ([a], [a])
span (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
/= '\n') String
s of
      (first :: String
first, '\n' : rest :: String
rest) -> (String
first String -> String -> String
forall a. [a] -> [a] -> [a]
++ "\n") String -> [String] -> [String]
forall a. a -> [a] -> [a]
: String -> [String]
lines_ String
rest
      (first :: String
first, rest :: String
rest) -> String
first String -> [String] -> [String]
forall a. a -> [a] -> [a]
: String -> [String]
lines_ String
rest

    removeLeadingEmptyLine :: [String] -> [String]
    removeLeadingEmptyLine :: [String] -> [String]
removeLeadingEmptyLine xs :: [String]
xs = case [String]
xs of
      y :: String
y:ys :: [String]
ys | String -> Bool
isEmptyLine String
y -> [String]
ys
      _ -> [String]
xs

    trimLastLine :: [String] -> [String]
    trimLastLine :: [String] -> [String]
trimLastLine (a :: String
a : b :: String
b : r :: [String]
r) = String
a String -> [String] -> [String]
forall a. a -> [a] -> [a]
: [String] -> [String]
trimLastLine (String
b String -> [String] -> [String]
forall a. a -> [a] -> [a]
: [String]
r)
    trimLastLine [a :: String
a] = if (Char -> Bool) -> String -> Bool
forall (t :: * -> *) a. Foldable t => (a -> Bool) -> t a -> Bool
all (Char -> Char -> Bool
forall a. Eq a => a -> a -> Bool
== ' ') String
a
      then []
      else [String
a]
    trimLastLine [] = []

    removeIndentation :: [String] -> [String]
    removeIndentation :: [String] -> [String]
removeIndentation ys :: [String]
ys = (String -> String) -> [String] -> [String]
forall a b. (a -> b) -> [a] -> [b]
map (Int -> String -> String
forall t. (Eq t, Num t) => t -> String -> String
dropSpaces Int
indentation) [String]
ys
      where
        dropSpaces :: t -> String -> String
dropSpaces 0 s :: String
s = String
s
        dropSpaces n :: t
n (' ' : r :: String
r) = t -> String -> String
dropSpaces (t
n t -> t -> t
forall a. Num a => a -> a -> a
- 1) String
r
        dropSpaces _ s :: String
s = String
s
        indentation :: Int
indentation = [String] -> Int
minimalIndentation [String]
ys
        minimalIndentation :: [String] -> Int
minimalIndentation =
            Int -> [Int] -> Int
forall a. Ord a => a -> [a] -> a
safeMinimum 0
          ([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
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
== ' '))
          ([String] -> [Int]) -> ([String] -> [String]) -> [String] -> [Int]
forall b c a. (b -> c) -> (a -> b) -> a -> c
. [String] -> [String]
removeEmptyLines
        removeEmptyLines :: [String] -> [String]
removeEmptyLines = (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
isEmptyLine)

        safeMinimum :: Ord a => a -> [a] -> a
        safeMinimum :: a -> [a] -> a
safeMinimum x :: a
x xs :: [a]
xs = case [a]
xs of
          [] -> a
x
          _ -> [a] -> a
forall (t :: * -> *) a. (Foldable t, Ord a) => t a -> a
minimum [a]
xs