{-# LANGUAGE OverloadedStrings #-} -- | A library to collect definitions from haskell source and collect them -- into an output source file. module EL.Private.ExtractHs where import qualified Data.Char as Char import qualified Data.Map as Map import qualified Data.Text as Text import qualified Data.Text.IO as Text.IO import qualified System.Directory as Directory import qualified System.Environment import qualified System.Exit import qualified System.FilePath as FilePath import qualified System.IO as IO import Global type Error = Text type Warning = Text process :: [String] -> (Text -> a) -> (FilePath -> Map FilePath a -> ([Warning], Text)) -> IO () process args extract generate = do progName <- System.Environment.getProgName (outFname, inputs) <- case args of outFname : inputs -> return (outFname, inputs) _ -> die $ "usage: " <> txt progName <> " output.hs input1.hs input2.hs ..." extracted <- extractFiles extract inputs case generate outFname extracted of (warnings, output) -> do mapM_ (Text.IO.hPutStrLn IO.stderr) warnings Directory.createDirectoryIfMissing True $ FilePath.takeDirectory outFname Text.IO.writeFile outFname $ header progName <> output header :: String -> Text header program = "-- automatically generated by " <> txt program <> "\n" extractFiles :: (Text -> a) -> [FilePath] -> IO (Map FilePath a) extractFiles extract = fmap Map.fromList . mapM (\fn -> (,) fn . extract <$> Text.IO.readFile fn) -- * extract typeDeclarations :: Text -> [(Int, (Text, Text))] typeDeclarations = mapMaybe parse . zip [1..] . Text.lines where parse (lineno, line) | line == "" || Char.isSpace (Text.head line) = Nothing | otherwise = case Text.words line of name : "::" : rest -> Just (lineno, (name, Text.unwords rest)) _ -> Nothing -- | This will be fooled by a {- or -} inside a string. I don't strip -- -- comments because the extract functions look for left justified text. stripComments :: Text -> Text stripComments = mconcat . go (0 :: Int) where go nesting text | Text.null post = [text] | "{-" `Text.isPrefixOf` post = (if nesting > 0 then id else (pre:)) (go (nesting+1) (Text.drop 2 post)) | otherwise = (if nesting == 0 then (pre <> Text.take 2 post :) else id) (go (max 0 (nesting-1)) (Text.drop 2 post)) where (pre, post) = breakOnFirst "{-" "-}" text -- | Like 'Text.breakOn', but break on two things. breakOnFirst :: Text -> Text -> Text -> (Text, Text) breakOnFirst a b text | Text.length aPre <= Text.length bPre = (aPre, aPost) | otherwise = (bPre, bPost) where (aPre, aPost) = Text.breakOn a text (bPre, bPost) = Text.breakOn b text -- * generate moduleDeclaration :: FilePath -> Text moduleDeclaration fname = "module " <> pathToModule fname <> " where" makeImport :: FilePath -> Text makeImport fname = "import qualified " <> pathToModule fname pathToModule :: FilePath -> Text pathToModule = Text.map dot . txt . FilePath.dropExtension . FilePath.normalise where dot c = if c == '/' then '.' else c -- * util die :: Text -> IO a die msg = do Text.IO.hPutStrLn IO.stderr msg System.Exit.exitFailure