packages feed

string-interpolate-0.3.1.0: src/lib/Data/String/Interpolate/Lines.hs

{-# LANGUAGE LambdaCase #-}

module Data.String.Interpolate.Lines where

import Data.Function  ( on )
import Data.List      ( find )
import Data.Semigroup ( Min(..) )

import Data.String.Interpolate.Types

isBlankLine :: [InterpSegment] -> Bool
isBlankLine [] = True
isBlankLine (Expression _ : _) = False
isBlankLine (Spaces _ : rest) = isBlankLine rest
isBlankLine (Tabs _ : rest) = isBlankLine rest
isBlankLine (Verbatim str:rest) = blank str && isBlankLine rest
  where
    blank :: String -> Bool
    blank = all (\c -> elem c [' ', '\t'])

-- |
-- Go the other direction, from a `Line' to the input that produced it.
displayLine :: Line -> String
displayLine = foldMap displaySegment
  where
    displaySegment :: InterpSegment -> String
    displaySegment (Expression expr) = "#{" ++ expr ++ "}"
    displaySegment (Verbatim str)    = str
      -- Above case is technically not correct due to escaped characters,
      -- but for the purposes of pinpointing where in a user's interpolation
      -- a problem is, it's good enough.
    displaySegment (Spaces n)        = replicate n ' '
    displaySegment (Tabs n)          = replicate n '\t'

-- |
-- Remove min indentation from given lines, using the minimum indentation
-- found within the lines. Gives back warnings for mixed indentation if
-- any are found.
handleIndents :: Lines -> ([IndentWarning], Lines)
handleIndents lines =
  let mindent = mindentation lines
  in (findMixedIndents mindent lines, reduceIndents mindent lines)

data Mindent = UsesSpaces Int | UsesTabs Int

data IndentWarning = IndentWarning
  { indentLine :: String
  , indentBase :: Mindent
  }

mindentation :: Lines -> Mindent
mindentation lines =
  let
    nonblank = filter (not . isBlankLine) lines
    withIndent = find (\case { Spaces _ : _ -> True; Tabs _ : _ -> True; _ -> False }) nonblank
  in case withIndent of
      Nothing -> UsesSpaces 0
      Just (Spaces _ : _) ->
        maybe (UsesSpaces 0) UsesSpaces $
          findMinIndent (\case { Spaces n -> Just n; _ -> Nothing }) Nothing nonblank
      Just (Tabs _ : _) ->
        maybe (UsesSpaces 0) UsesTabs $
          findMinIndent (\case { Tabs n -> Just n; _ -> Nothing }) Nothing nonblank
      Just _ -> UsesSpaces 0
  where
    findMinIndent :: (InterpSegment -> Maybe Int) -> Maybe Int -> [[InterpSegment]] -> Maybe Int
    findMinIndent _ found [] = found
    findMinIndent f found ((seg:_):rest) =
      findMinIndent f (getMin <$> on mappend (fmap Min) (f seg) found) rest
    findMinIndent f found ([]:rest) = findMinIndent f found rest

reduceIndents :: Mindent -> Lines -> Lines
reduceIndents _ [] = []
reduceIndents i@(UsesSpaces indent) ((Spaces n:line):rest) =
  (Spaces (n-indent):line) : reduceIndents i rest
reduceIndents i@(UsesTabs indent) ((Tabs n:line):rest) =
  (Tabs (n-indent):line) : reduceIndents i rest
reduceIndents i (line:rest) = line : reduceIndents i rest

findMixedIndents :: Mindent -> Lines -> [IndentWarning]
findMixedIndents mindent = go
  where
    go :: [[InterpSegment]] -> [IndentWarning]
    go [] = []
    go (line:lines) = do
      let
        ind = indentation line
        warn = IndentWarning
          { indentLine = displayLine line, indentBase = mindent }
      case (mindent, any isSpaces ind, any isTabs ind) of
        (UsesSpaces _, _, True) -> warn : go lines
        (UsesTabs _, True, _)   -> warn : go lines
        _                       -> go lines

    indentation :: [InterpSegment] -> [InterpSegment]
    indentation =
      takeWhile (\case { Spaces _ -> True; Tabs _ -> True; _ -> False })

    isSpaces :: InterpSegment -> Bool
    isSpaces (Spaces n) = n > 0
    isSpaces _          = False

    isTabs :: InterpSegment -> Bool
    isTabs (Tabs n) = n > 0
    isTabs _        = False