{-# LANGUAGE OverloadedStrings #-}
{-# OPTIONS_GHC -fno-warn-name-shadowing #-}
module Floskell
(
reformat
, prettyPrint
, Style(..)
, styles
, defaultExtensions
) where
import Data.ByteString ( ByteString )
import qualified Data.ByteString as S
import qualified Data.ByteString.Char8 as S8
import qualified Data.ByteString.Internal as S
import qualified Data.ByteString.Lazy as L
import qualified Data.ByteString.Lazy.Char8 as L8
import qualified Data.ByteString.UTF8 as UTF8
import qualified Data.ByteString.Unsafe as S
import Data.Function ( on )
import Data.List
import qualified Data.Map.Strict as Map
import Data.Maybe
import Data.Monoid
import qualified Floskell.Buffer as Buffer
import Floskell.Comments
import Floskell.Pretty ( pretty, printComment )
import Floskell.Styles ( styles )
import Floskell.Types
import Language.Haskell.Exts
hiding ( Pretty, Style, parse, prettyPrint, style )
import qualified Language.Haskell.Exts as Exts
data CodeBlock = HaskellSource Int ByteString | CPPDirectives ByteString
deriving ( Show, Eq )
reformat :: Style
-> Language
-> [Extension]
-> Maybe FilePath
-> ByteString
-> Either String L.ByteString
reformat style language langextensions mfilepath x = preserveTrailingNewline x
. mconcat . intersperse "\n" <$> mapM processBlock (cppSplitBlocks x)
where
processBlock :: CodeBlock -> Either String L.ByteString
processBlock (CPPDirectives text) = Right $ L.fromStrict text
processBlock (HaskellSource offset text) =
let ls = S8.lines text
prefix = findPrefix ls
code = unlines' (map (stripPrefix prefix) ls)
exts = readExtensions (UTF8.toString code)
mode'' = case exts of
Nothing -> mode'
Just (Nothing, exts') ->
mode' { extensions = exts' ++ extensions mode' }
Just (Just lang, exts') ->
mode' { baseLanguage = lang
, extensions = exts' ++ extensions mode'
}
in
case parseModuleWithComments mode'' (UTF8.toString code) of
ParseOk (m, comments) ->
fmap (addPrefix prefix) (prettyPrint style m comments)
ParseFailed loc e -> Left $
Exts.prettyPrint (loc { srcLine = srcLine loc + offset })
++ ": " ++ e
unlines' = S.concat . intersperse "\n"
unlines'' = L.concat . intersperse "\n"
addPrefix :: ByteString -> L8.ByteString -> L8.ByteString
addPrefix prefix = unlines'' . map (L8.fromStrict prefix <>) . L8.lines
stripPrefix :: ByteString -> ByteString -> ByteString
stripPrefix prefix line = if S.null (S8.dropWhile (== '\n') line)
then line
else fromMaybe (error "Missing expected prefix")
. s8_stripPrefix prefix $ line
findPrefix :: [ByteString] -> ByteString
findPrefix = takePrefix False . findSmallestPrefix . dropNewlines
dropNewlines :: [ByteString] -> [ByteString]
dropNewlines = filter (not . S.null . S8.dropWhile (== '\n'))
takePrefix :: Bool -> ByteString -> ByteString
takePrefix bracketUsed txt = case S8.uncons txt of
Nothing -> ""
Just ('>', txt') ->
if not bracketUsed then S8.cons '>' (takePrefix True txt') else ""
Just (c, txt') -> if c == ' ' || c == '\t'
then S8.cons c (takePrefix bracketUsed txt')
else ""
findSmallestPrefix :: [ByteString] -> ByteString
findSmallestPrefix [] = ""
findSmallestPrefix ("" : _) = ""
findSmallestPrefix (p : ps) =
let first = S8.head p
startsWithChar c x = S8.length x > 0 && S8.head x == c
in
if all (startsWithChar first) ps
then S8.cons first (findSmallestPrefix (S.tail p : map S.tail ps))
else ""
mode' = defaultParseMode { parseFilename = fromMaybe "<stdin>" mfilepath
, baseLanguage = language
, extensions = langextensions
}
preserveTrailingNewline x x' = if not (S8.null x) && S8.last x == '\n'
&& not (L8.null x') && L8.last x' /= '\n'
then x' <> "\n"
else x'
cppSplitBlocks :: ByteString -> [CodeBlock]
cppSplitBlocks = map (classify . unlines') . groupBy ((==) `on` (cppLine . snd))
. zip [ 0 .. ] . S8.lines
where
cppLine :: ByteString -> Bool
cppLine src =
any (`S8.isPrefixOf` src)
[ "#if"
, "#end"
, "#else"
, "#define"
, "#undef"
, "#elif"
, "#include"
, "#error"
, "#warning"
]
unlines' :: [(Int, ByteString)] -> (Int, ByteString)
unlines' [] = (0, "")
unlines' xs@((line, _) : _) = (line, S8.unlines $ map snd xs)
classify :: (Int, ByteString) -> CodeBlock
classify (ofs, text) =
if cppLine text then CPPDirectives text else HaskellSource ofs text
prettyPrint :: Style
-> Module SrcSpanInfo
-> [Comment]
-> Either a L.ByteString
prettyPrint style m comments =
let (cs, ast) =
annotateComments (fromMaybe m $ applyFixities baseFixities m)
comments
csComments = map comInfoComment cs
in
Right (runPrinterStyle style
(do
mapM_ (printComment Nothing)
(reverse csComments)
pretty ast))
runPrinterStyle :: Style -> Printer () -> L.ByteString
runPrinterStyle (Style _name _author _desc st) m =
maybe (error "Printer failed with mzero call.")
(Buffer.toLazyByteString . psBuffer)
(snd <$> execPrinter m
(PrintState Buffer.empty
0
0
Map.empty
st
False
Anything))
defaultExtensions :: [Extension]
defaultExtensions = [ e | e@EnableExtension{} <- knownExtensions ]
\\ map EnableExtension badExtensions
badExtensions :: [KnownExtension]
badExtensions =
[ Arrows
, TransformListComp
, XmlSyntax
, RegularPatterns
, UnboxedTuples
, PatternSynonyms
, RecursiveDo
, DoRec
, TypeApplications
]
s8_stripPrefix :: ByteString -> ByteString -> Maybe ByteString
s8_stripPrefix bs1@(S.PS _ _ l1) bs2
| bs1 `S.isPrefixOf` bs2 = Just (S.unsafeDrop l1 bs2)
| otherwise = Nothing