{-# LANGUAGE BangPatterns #-} module Main where import Data.Char import Data.Maybe import Data.MemoTrie import Text.Regex import System.Process import System.IO import System.IO.Unsafe import System.Environment import Language.Haskell.Interpreter import Data.List (sort, sortBy, intercalate, nub) import Distribution.PackageDescription.Parse import Distribution.Verbosity (normal) import Distribution.PackageDescription import Distribution.ModuleName (components) type Identifier = String ------------------------------------------------------------------------------------------ main :: IO () main = do args <- getArgs runStd args runStd :: [FilePath] -> IO () runStd = runFilter stdin stdout runFilter :: Handle -> Handle -> [FilePath] -> IO () runFilter inf outf args = hGetContents inf >>= run args >>= hPutStr outf ------------------------------------------------------------------------------------------ -- | -- Given a list of cabal files, process input by replacing all text on the form -- @[foo] with [`foo`][foo], replacing @@@hslinks@@@ with an index on the form: -- -- [foo]: prefix/Module-With-Foo.html#v:foo -- [Foo]: prefix/Module-With-Foo.html#t:Foo -- -- etc. -- run :: [FilePath] -> String -> IO String run args input = do !modNames <- visibleModsInCabals args -- TODO generate the index let ids = nub $ fmap getId $ allMatches idExpr input let !toLinks = idToLink modNames let links = map toLinks ids let index = intercalate "\n" links return $ subElems $ subIndex index input where idChars = "[^]]+" idExpr = mkRegex $ "@\\[(" ++ idChars ++ ")\\]" indexExpr = mkRegex $ "@@@hslinks@@@" getId = head . snd subElems a = subRegex idExpr a "[`\\1`][\\1]" subIndex i a = subRegex indexExpr a i -- FIXME kPrefix = "/docs/api/" idToLink :: [ModuleName] -> Identifier -> String idToLink sources ident = do let vOrT = if isUpper (head ident) then "t" else "v" case whichModule sources (wrapOp ident) of Left e -> "\n\n" Right modName -> "" ++ "[" ++ ident ++ "]: " ++ kPrefix ++ replace '.' '-' modName ++ ".html#" ++ vOrT ++ ":" ++ handleOp ident ++ "" wrapOp :: Identifier -> Identifier wrapOp [] = [] wrapOp as@(x:_) | isAlphaNum x = as | otherwise = "(" ++ as ++ ")" handleOp :: Identifier -> Identifier handleOp [] = [] handleOp as@(x:_) | isAlphaNum x = as | otherwise = escapeOp as escapeOp = concatMap (\c -> "-" ++ show (ord c) ++ "-") allMatches :: Regex -> String -> [(String, [String])] allMatches reg str = case matchRegexAll reg str of Nothing -> [] Just (before, match, after, subs) -> (match, subs) : allMatches reg after ----------------------------------------------------------------------------------------- -- whichModule, visibleModsInCabals -- Given a set of modules, find the topmost module in which an identifier appears -- A module is considered above another if it has fewer dots in its name. If the number of -- dots are equal, use lexiographic order. whichModule :: [ModuleName] -> Identifier -> Either String ModuleName whichModule modNames ident = eitherMaybe ("No such identifier: " ++ ident) $ fmap (listToMaybe . sortBy bottomMost) modsWithIdent where mods = modsNamed modNames -- modules containing the identifier modsWithIdent = fmap (hasIdent ident) mods modsNamed :: [ModuleName] -> Either String [(ModuleName, [Identifier])] modsNamed modNames = sequence $ fmap modNamed modNames modNamed :: ModuleName -> Either String (ModuleName, [Identifier]) modNamed = modNamed' modNamed' n = case identifiers n of { Left e -> Left e ; Right ids -> Right (n, ids) ; } hasIdent :: Identifier -> [(ModuleName, [Identifier])] -> [ModuleName] hasIdent ident = fmap fst . filter (\(n,ids) -> ident `elem` ids) -- | Get all the identifiers of a module identifiers :: ModuleName -> Either String [Identifier] identifiers = unsafePerformIO . identifiers' identifiers' :: ModuleName -> IO (Either String [Identifier]) identifiers' modName = fmap getElemNames $ runInterpreter $ getModuleExports modName where getElemNames = either (Left . getError) Right . fmap (concatMap getModuleElem) getError = show -- | Get all identifiers in a module element (names, class members, data constructors) getModuleElem :: ModuleElem -> [Identifier] getModuleElem (Fun a) = [a] getModuleElem (Class a as) = a:as getModuleElem (Data a as) = a:as modsInDir :: FilePath -> IO [ModuleName] modsInDir dir = do dirList <- readProcess "find" [dir, "-type", "f", "-name", "*.hs"] "" let dirs = lines dirList let mods = fmap (pathToModName . dropBaseDir) dirs return mods where dropBaseDir = drop (length dir) pathToModName = replace '/' '.' . dropWhile (not . isUpper) . dropLast 3 visibleModsInCabals :: [FilePath] -> IO [ModuleName] visibleModsInCabals = fmap concat . mapM visibleModsInCabal visibleModsInCabal :: FilePath -> IO [ModuleName] visibleModsInCabal path = do packageDesc <- readPackageDescription normal path case condLibrary packageDesc of Nothing -> return [] Just libTree -> return (fmap unModName $ exposedModules $ foldCondTree libTree) where unModName = intercalate "." . components foldCondTree (CondNode x c comp) = x -- TODO subtrees bottomMost :: ModuleName -> ModuleName -> Ordering bottomMost a b = case level a `compare` level b of LT -> GT EQ -> a `compare` b GT -> LT where level = length . filter (== '.') ----------------------------------------------------------------------------------------- eitherMaybe :: e -> Either e (Maybe a) -> Either e a eitherMaybe e' = go where go (Left e) = Left e go (Right Nothing) = Left e' go (Right (Just a)) = Right a -- | @replace x y xs@ replaces all instances of a @x@ in a list @xs@ with @y@. replace :: Eq a => a -> a -> [a] -> [a] replace x y = map $ \z -> if z == x then y else z takeLast :: Int -> [a] -> [a] takeLast n = reverse . take n . reverse dropLast :: Int -> [a] -> [a] dropLast n = reverse . drop n . reverse