{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE CPP #-}
module Data.String.Interpolate
( i, __i, iii )
where
import Prelude hiding ( fail )
import Data.Char ( isSpace )
import Data.Proxy
import Data.Function ( on, (&) )
import Data.Semigroup ( Min(..) )
import Data.List
import Data.List.Split
import Control.Monad.Fail
import qualified Language.Haskell.Exts.Extension as Ext
import Language.Haskell.Exts.Parser
( ParseMode(..), ParseResult(..), defaultParseMode, parseExpWithMode )
import Language.Haskell.Meta ( ToExp(..) )
import Language.Haskell.TH
import Language.Haskell.TH.Quote ( QuasiQuoter(..) )
import Data.String.Interpolate.Conversion ( build, finalize, interpolate, ofString )
import Data.String.Interpolate.Parse ( InterpSegment(..), dosToUnix, parseInterpSegments )
i :: QuasiQuoter
i = QuasiQuoter
{ quoteExp = toExp . parseInterpSegments . dosToUnix
, quotePat = const $ errQQType "i" "pattern"
, quoteType = const $ errQQType "i" "type"
, quoteDec = const $ errQQType "i" "declaration"
}
where toExp :: Either String [InterpSegment] -> Q Exp
toExp parseResult = case parseResult of
Left msg -> errQQ "i" msg
Right segs -> interpToExp segs
__i :: QuasiQuoter
__i = QuasiQuoter
{ quoteExp = toExp . parseInterpSegments . dosToUnix
, quotePat = const $ errQQType "__i" "pattern"
, quoteType = const $ errQQType "__i" "type"
, quoteDec = const $ errQQType "__i" "declaration"
}
where toExp :: Either String [InterpSegment] -> Q Exp
toExp parseResult = case parseResult of
Left msg -> errQQ "__i" msg
Right segs -> unindent segs >>= interpToExp
unindent :: [InterpSegment] -> Q [InterpSegment]
unindent segs =
let lines = interpLines segs
mindent = mindentation lines
in warnMixedIndent mindent lines >>
(pure $! (interpUnlines . removeBlanksAround . reduceIndents mindent) lines)
iii :: QuasiQuoter
iii = QuasiQuoter
{ quoteExp = toExp . parseInterpSegments . dosToUnix
, quotePat = const $ errQQType "iii" "pattern"
, quoteType = const $ errQQType "iii" "type"
, quoteDec = const $ errQQType "iii" "declaration"
}
where toExp :: Either String [InterpSegment] -> Q Exp
toExp parseResult = case parseResult of
Left msg -> errQQ "iii" msg
Right segs -> collapse segs
collapse :: [InterpSegment] -> Q Exp
collapse segs = renderOutput segs
& collapseStrings
& fmap outputCollapseWS
& removeWSAround
& outputToExp
outputCollapseWS :: OutputSegment -> OutputSegment
outputCollapseWS (OfString str) = OfString $ collapseWhitespace str
outputCollapseWS other = other
interpLines :: [InterpSegment] -> [[InterpSegment]]
interpLines = split $ dropDelims $ whenElt (== Newline)
interpUnlines :: [[InterpSegment]] -> [InterpSegment]
interpUnlines = intercalate [Newline]
data Mindent = UsesSpaces Int | UsesTabs Int
mindentation :: [[InterpSegment]] -> Mindent
mindentation lines =
let nonblank = filter (not . blankLine) 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
warnMixedIndent :: Mindent -> [[InterpSegment]] -> Q ()
warnMixedIndent mindent = go 1 . removeBlanksAround
where go :: Int -> [[InterpSegment]] -> Q ()
go _lineno [] = pure ()
go lineno (line:lines) = do
let ind = indentation line
case (mindent, any isSpaces ind, any isTabs ind) of
(UsesSpaces _, _, True) ->
reportWarning $
"splice line " ++ show lineno ++ ": found TAB character in indentation"
(UsesTabs _, True, _) ->
reportWarning $
"splice line " ++ show lineno ++ ": found SPACE character in indentation"
_ -> pure ()
go (lineno+1) 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
reduceIndents :: Mindent -> [[InterpSegment]] -> [[InterpSegment]]
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
removeBlanksAround :: [[InterpSegment]] -> [[InterpSegment]]
removeBlanksAround =
reverse
. dropWhile blankLine
. reverse
. dropWhile blankLine
blankLine :: [InterpSegment] -> Bool
blankLine [] = True
blankLine (Expression _ : _) = False
blankLine (Newline : rest) = blankLine rest
blankLine (Spaces _ : rest) = blankLine rest
blankLine (Tabs _ : rest) = blankLine rest
blankLine (Verbatim str:rest) = blank str && blankLine rest
where blank :: String -> Bool
blank = all (\c -> elem c [' ', '\t'])
byWhitespace :: String -> [String]
byWhitespace = split $ condense $ whenElt isSpace
collapseWhitespace :: String -> String
collapseWhitespace =
foldMap (\s -> if all isSpace s && not (null s) then " " else s)
. byWhitespace
removeWSAround :: [OutputSegment] -> [OutputSegment]
removeWSAround =
transformLeading (dropWhile isSpace)
. reverse
. transformLeading (reverse . dropWhile isSpace . reverse)
. reverse
where transformLeading :: (String -> String) -> [OutputSegment] -> [OutputSegment]
transformLeading _ [] = []
transformLeading f (OfString str:rest) = OfString (f str) : rest
transformLeading _ other = other
interpToExp :: [InterpSegment] -> Q Exp
interpToExp = outputToExp . collapseStrings . renderOutput
outputToExp :: [OutputSegment] -> Q Exp
outputToExp segs = [|finalize Proxy $(go segs)|]
where renderExp :: OutputSegment -> Q Exp
renderExp (OfString str) = [|ofString Proxy str|]
renderExp (Interpolate expr) = [|interpolate Proxy $(reifyExpression expr)|]
go :: [OutputSegment] -> Q Exp
go = foldr
(\seg qexp -> [|build Proxy $(renderExp seg) $(qexp)|])
[|ofString Proxy ""|]
data OutputSegment
= OfString String
| Interpolate String
collapseStrings :: [OutputSegment] -> [OutputSegment]
collapseStrings [] = []
collapseStrings (OfString s1 : OfString s2 : rest) =
collapseStrings ((OfString $ s1 ++ s2) : rest)
collapseStrings (other : rest) = other : collapseStrings rest
renderOutput :: [InterpSegment] -> [OutputSegment]
renderOutput = fmap renderSegment
where renderSegment :: InterpSegment -> OutputSegment
renderSegment (Verbatim str) = OfString str
renderSegment Newline = OfString "\n"
renderSegment (Spaces n) = OfString (replicate n ' ')
renderSegment (Tabs n) = OfString (replicate n '\t')
renderSegment (Expression str) = Interpolate str
errQQ :: MonadFail m => String -> String -> m a
errQQ qqName msg =
fail ("Data.String.Interpolate." ++ qqName ++ ": " ++ msg)
errQQType :: MonadFail m => String -> String -> m a
errQQType qqName = errQQ qqName . ("This QuasiQuoter cannot be used as a " ++)
reifyExpression :: String -> Q Exp
reifyExpression s = do
exts <- (fmap . fmap) (Ext.parseExtension . show) extsEnabled
parseMode <- pure (defaultParseMode { extensions = exts })
case parseExpWithMode parseMode s of
ParseFailed _ err -> fail $
"Data.String.Interpolate.i: got error: '" ++ err ++ "' while parsing expression: " ++ s
ParseOk e -> pure (toExp e)