packages feed

hslinks-0.5: src/hslinks.hs

{-# 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<!-- Unknown: " ++ ident ++ " " ++ e ++ "-->\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