packages feed

haddocset-0.4.2: Documentation/Haddocset.hs

{-# LANGUAGE NamedFieldPuns     #-}
{-# LANGUAGE OverloadedStrings  #-}
{-# LANGUAGE Rank2Types         #-}
{-# LANGUAGE TupleSections      #-}
{-# LANGUAGE ViewPatterns       #-}
{-# LANGUAGE CPP                #-}
{-# LANGUAGE DeriveDataTypeable #-}

module Documentation.Haddocset
  ( globalPackageDirectories
  , packageConfs

  , DocInfo(..)
  , readDocInfoFile

  , ResolutionStrategy(..)
  , addSinglePackage

  , docsetDir
  , haddockIndex
  ) where

#if __GLASGOW_HASKELL__ < 709
import           Control.Applicative
#endif

import           Control.Monad.Catch
import           Control.Monad
import           Control.Monad.Trans.Resource
import           Control.Monad.IO.Class
import           Data.Typeable                     (Typeable)

import           System.FilePath
import           System.Directory                  hiding (listDirectory)

import           System.IO
import           System.IO.Error(mkIOError, alreadyExistsErrorType, isDoesNotExistError)
import           System.Process

import           Network.HTTP.Types.URI (urlEncode)

import           Data.Char
import           Data.List
import           Data.Maybe
import qualified Data.Text                         as T
import qualified Data.Text.IO                      as T
import qualified Data.Text.Encoding                as T
import           Text.HTML.TagSoup                 as Ts
import           Text.HTML.TagSoup.Match           as Ts

import           Distribution.Compat.ReadP
import           Distribution.InstalledPackageInfo
import           Distribution.Package
import           Distribution.Text                 (display, parse)
import           Documentation.Haddock
import qualified Module                            as Ghc
import qualified Name                              as Ghc

import           Data.Conduit
import           Data.Conduit.Filesystem (sourceDirectoryDeep)

import Documentation.Haddocset.Index

collapse :: FilePath -> FilePath
collapse = joinPath . reverse . go [] . splitDirectories
  where
    go cs []        = cs
    go cs (".": ps) = go cs ps
    go cs ("..":ps) = go (tail cs) ps
    go cs (p:   ps) = go (p:cs) ps

parent :: FilePath -> FilePath
parent = takeDirectory . dropTrailingPathSeparator

stripPrefixPath :: FilePath -> FilePath -> Maybe FilePath
stripPrefixPath a0 b0 = joinPath <$> go (splitPath a0) (splitPath b0)
  where
    go _      []  = Nothing
    go []     bs  = Just bs
    go (a:as) (b:bs)
      | a == b    = go as bs
      | otherwise = Nothing

docsetDir :: FilePath -> FilePath
docsetDir = flip replaceExtension "docset"

listDirectory :: FilePath -> IO [FilePath]
listDirectory p = map (p </>) . filter (`notElem` [".", ".."]) <$> getDirectoryContents p

globalPackageDirectories :: FilePath -> IO [FilePath]
globalPackageDirectories hcPkg = do
    ds <- map init . filter isPkgDBLine . lines <$>
        readProcess hcPkg ["list", "--global"] ""
    forM ds $ \d -> doesDirectoryExist d >>= \isDir -> return $
        if isDir
        then d
        else (takeDirectory d)
  where
    isPkgDBLine ""      = False
    isPkgDBLine (' ':_) = False
    isPkgDBLine _       = True

packageConfs :: FilePath -> IO [FilePath]
packageConfs dir =
    filter (("package.cache" /=) . takeFileName) <$> listDirectory dir

data DocInfo = DocInfo
    { diPackageId  :: PackageId
    , diInterfaces :: [FilePath]
    , diHTMLs      :: [FilePath]
    , diExposed    :: Bool
    } deriving Show

readDocInfoFile :: FilePath -> IO (Maybe DocInfo)
readDocInfoFile pifile = doesDirectoryExist pifile >>= \isDir ->
    if isDir
    then filter ((== ".haddock") . takeExtension) <$> listDirectory pifile >>= \hdc -> case hdc of
        []       -> return Nothing
        hs@(h:_) -> readInterfaceFile freshNameCache h >>= \ei -> case ei of
            Left _     -> return Nothing
            Right (InterfaceFile _ (intf:_)) -> do
#if __GLASGOW_HASKELL__ >= 800
                let rPkg = readP_to_S parse . Ghc.unitIdString . Ghc.moduleUnitId $ instMod intf :: [(PackageId, String)]
#elif __GLASGOW_HASKELL__ >= 710
                let rPkg = readP_to_S parse . Ghc.packageKeyString . Ghc.modulePackageKey $ instMod intf :: [(PackageId, String)]
#else
                let rPkg = readP_to_S parse . Ghc.packageIdString . Ghc.modulePackageId $ instMod intf :: [(PackageId, String)]
#endif
                case rPkg of
                    []  -> return Nothing
                    pkg -> do
                        return . Just $ DocInfo (fst $ last pkg) hs [collapse pifile] True
            Right _ -> return Nothing
    else do
        result <- parseInstalledPackageInfo <$> readFile pifile
        return $ case result of
            ParseFailed _ -> Nothing
            ParseOk [] a
                | null (haddockHTMLs a)      -> Nothing
                | null (haddockInterfaces a) -> Nothing
                | otherwise -> Just $
                    DocInfo
                        (sourcePackageId a)
                        (map expandPkgRoot (haddockInterfaces a))
                        (map expandPkgRoot (haddockHTMLs a))
                        (exposed a)

            ParseOk _  _  -> Nothing
  where
    -- drop the package.conf directory: pkgroot/package.conf.d/foo.conf -> pkgroot
    pkgroot = takeDirectory . takeDirectory $ pifile
    expandPkgRoot path = case splitPath path of
        "${pkgroot}/":rest -> joinPath (pkgroot : rest)
        _ -> path

copyHtml :: DocFile -> FilePath -> IO ()
copyHtml doc dst = do
    tags <- Ts.parseTags <$> T.readFile (docAbsolute doc)
    T.writeFile dst . Ts.renderTags $ concatMap (addAnchor . modifyUrl) tags
  where
    modifyUrl tag
        | Ts.tagOpenLit "a" (Ts.anyAttrNameLit "href") tag =
            let absp p = collapse $ docBaseDir doc </> p
                attr   = filter (\(n,_) -> n /= "href") (getAttr tag)
            in case T.unpack $ Ts.fromAttrib "href" tag of
                url | "http://"  `isPrefixOf` url -> tag
                    | "https://" `isPrefixOf` url -> tag
                    | "file:///" `isPrefixOf` url -> Ts.TagOpen "a" $ ("href", T.pack . rebase $ drop 7 url) : attr
                    | "#"        `isPrefixOf` url -> Ts.TagOpen "a" $ ("href", T.pack . rebase $ dst ++ url) : attr
                    | otherwise                   -> Ts.TagOpen "a" $ ("href", T.pack . rebase $ absp  url) : attr
        | Ts.tagOpenLit "a" (Ts.anyAttrNameLit "name") tag =
            let Ts.TagOpen _ attr = tag
                hash = '#' : T.unpack (Ts.fromAttrib "name" tag)
            in Ts.TagOpen "a" $ ("href", T.pack . rebase $ dst ++ hash) : attr
        | otherwise = tag

    addAnchor tag@(Ts.TagOpen "a" as)
        | Ts.anyAttrNameLit "name" as && hasClass "def" as =
            maybe id (\(t, n) -> (++)
                [ Ts.TagOpen  "a"
                    [ ("class", "dashAnchor")
                    , ("name", T.concat ["//apple_ref/cpp/", t, "/", T.decodeUtf8 . urlEncode True $ T.encodeUtf8 n])
                    ]
                , Ts.TagClose "a"
                ]) (anchorName =<< lookup "name" as) $
            [tag]
      | otherwise = [tag]
    addAnchor tag = [tag]

    unescape [] = Just []
    unescape ('-':n) = case reads n of
        [(c, '-':o)] -> (toEnum c :) <$> unescape o
        _            -> Nothing
    unescape (c:n) = (c:) <$> unescape n

    anchorName s = fmap T.pack <$> case T.unpack s of
      ('t':_:t)     -> ("Type",) <$> unescape t
      ('v':_:':':c) -> ("Constructor",) <$> unescape (':':c)
      ('v':_:a:as)
        | isUpper a -> ("Constructor",) <$> unescape (a:as)
        | otherwise -> ("Function",) <$> unescape (a:as)
      _ -> Nothing

    hasClass c = maybe False (any (c ==) . T.words) . lookup "class"

    getAttr (TagOpen _ a) = a
    getAttr _             = error "copyHtml: call attr to !TagOpen."

    rebase :: FilePath -> FilePath
    rebase p =
        let file    = takeFileName p
            isSrc   = "src" `elem` splitDirectories (parent p)
            srcNize = if isSrc then ("src" </>) else id
            pkgs    = filter packageLike . reverse $ splitDirectories (parent p)
        in case pkgs of
            []    -> file
            pkg:_ -> ".." </> pkg </> srcNize file

    packageLike :: FilePath -> Bool
    packageLike p =
        let t = T.pack $ dropTrailingPathSeparator p
            (pkg, version) = T.breakOnEnd "-" t
        in not (T.null pkg) && T.all (`elem` ("0123456789." :: String)) version

data DocFile = DocFile
    { docPackage     :: PackageId
    , docBaseDir     :: FilePath
    , docRationalDir :: FilePath
    } deriving Show

docAbsolute :: DocFile -> FilePath
docAbsolute doc = docBaseDir doc </> docRationalDir doc

copyDocument :: (MonadThrow m, MonadIO m) => FilePath
             -> Consumer DocFile m ()
copyDocument docdir = awaitForever $ \doc -> do
    let full = docAbsolute doc
        dst = docdir
              </> display (docPackage doc)
              </> docRationalDir doc
    liftIO $ createDirectoryIfMissing True (takeDirectory dst)
    ex <- liftIO $ (||) <$> doesFileExist dst <*> doesDirectoryExist dst
    when ex $ throwM $ mkIOError alreadyExistsErrorType "copyDocument" Nothing (Just dst)
    case takeExtension $ docRationalDir doc of
        ".html"    -> liftIO $ copyHtml doc dst
        ".haddock" -> return ()
        _          -> liftIO $ copyFile full dst

docFiles :: (MonadIO m, MonadResource m) => PackageId -> [FilePath] -> Producer m DocFile
docFiles sourcePackageId haddockHTMLs =
    forM_ haddockHTMLs $ \dir ->
        sourceDirectoryDeep False dir
        =$= awaitForever
            (\f -> yield
                $ DocFile sourcePackageId dir
                $ fromMaybe (error $ "Prefix mismatch: " ++ show (dir, f))
                $ stripPrefixPath (addTrailingPathSeparator dir) f)

data Provider
    = Haddock  PackageId FilePath
    | Package  PackageId
    | Module   PackageId Ghc.Module
    | Function PackageId Ghc.Module Ghc.Name

moduleProvider :: MonadIO m => DocInfo -> Producer m Provider
moduleProvider iFile =
    mapM_ sub $ diInterfaces iFile
  where
    sub file = do
        rd <- liftIO $ readInterfaceFile freshNameCache file
        case rd of
            Left _ -> return ()
            Right (ifInstalledIfaces -> iIntrf) -> do
                let pkg = diPackageId iFile
                yield $ Haddock pkg file
                yield $ Package pkg
                forM_ iIntrf $ \i -> do
                    let modn = instMod i
                        fs   = instVisibleExports i
                    when (OptHide `notElem` instOptions i) $ do
                        yield $ Module pkg modn
                        mapM_ (yield . Function pkg modn) fs

moduleNmaeUrl :: String -> String
moduleNmaeUrl = map dot2Dash
  where dot2Dash '.'  = '-'
        dot2Dash c    = c

progress :: MonadIO m => Bool -> Int -> Char -> ConduitM o o m ()
progress cr u c = sub 1
  where
    sub n = await >>= \mbi -> case mbi of
        Nothing -> when cr $ liftIO (hPutChar stderr '\n' >> hFlush stderr)
        Just i  -> do
            when (n `mod` u == 0) $ liftIO (hPutChar stderr c >> hFlush stderr)
            yield i
            sub (succ n)

providerToEntry :: FilePath -> Conduit Provider IO IndexEntry
providerToEntry hdir = awaitForever $ \provider -> case provider of
    Haddock p h -> liftIO $
        copyFile h (hdir </> display p <.> "haddock")
    Package pkg ->
        yield $! IndexEntry
            { entryName = T.pack (display pkg)
            , entryType = PackageEntry
            , entryPath = display pkg ++ "/index.html"
            , entryPackage = pkg
            }
    Module pkg modn ->
        yield $! IndexEntry
            { entryName = T.pack $ Ghc.moduleNameString $ Ghc.moduleName modn
            , entryType = ModuleEntry
            , entryPath = display pkg ++ '/' : moduleNameURL modn ++ ".html"
            , entryPackage = pkg
            }
    Function pkg modn name ->
        let typ = case () of
                    _ | Ghc.isTyConName   name -> TypeEntry
                      | Ghc.isDataConName name -> ConstructorEntry
                      | otherwise              -> FunctionEntry
            prefix = case typ of
                        TypeEntry -> 't'
                        _         -> 'v'
        in yield $! IndexEntry
            { entryName = T.pack $ Ghc.getOccString name
            , entryType = typ
            , entryPath
                = display pkg ++ '/'
                : moduleNameURL modn ++ ".html#"
               ++ prefix : ':' : escapeSpecial (Ghc.getOccString name)
            , entryPackage = pkg
            }

  where
    moduleNameURL = moduleNmaeUrl . Ghc.moduleNameString . Ghc.moduleName

    specialChars  = "!&|+$%(,)*<>-/=#^\\?" :: String
    escapeSpecial = concatMap (\c -> if c `elem` specialChars then '-': show (fromEnum c) ++ "-" else [c])


haddockIndex :: FilePath -> FilePath -> IO ()
haddockIndex haddockdir documentdir = do
    argIs <- map (\h -> "--read-interface="
               ++   (dropExtension $ takeFileName h) ++
               ',': h) <$> listDirectory haddockdir
    haddock $ "--gen-index": "--gen-contents": ("--odir=" ++ documentdir): argIs

-- | What to do if documentation for a package already exists?
data ResolutionStrategy
    = Skip
    | Overwrite
    | Fail
    deriving (Show, Ord, Eq)

addSinglePackage
    :: Bool
    -> ResolutionStrategy
    -> FilePath
    -> FilePath
    -> SearchIndex ReadOnly
    -> DocInfo
    -> IO ()
addSinglePackage quiet resolution docDir haddockDir idx iFile =
    go `catchIOError` handler
  where
    -- Directory to which documentation for this package will be written.
    pkgDocDir = docDir </> display (diPackageId iFile)

    resolveConflict :: PackageId -> IO () -> IO ()
    resolveConflict pkgId io = do
        hasConflict <- doesDirectoryExist pkgDocDir
        if not hasConflict
          then io
          else case resolution of
                 Fail      -> throwM $ AlreadyExists pkgId
                 Overwrite -> do
                    unless quiet $
                        putStrLn $ "    " ++ display pkgId ++
                                   ": Found existing documentation. Deleting."
                    removeDirectoryRecursive pkgDocDir >> io
                 _         ->
                    unless quiet $
                        putStrLn $ "    " ++ display pkgId ++
                                   ": Found existing documentation. Skipping."

    go = resolveConflict (diPackageId iFile) $ do
        -- These operations are run only after resolving a conflict--if
        -- one existed.

        putStr "    "
        putStr (display $ diPackageId iFile)
        putChar ' '
        hFlush stdout

        runResourceT $
          docFiles (diPackageId iFile) (diHTMLs iFile)
            $$ (if quiet then id else (progress False  10 '.' =$))
               (copyDocument docDir)

        withReadWrite idx $ \idx' ->
            moduleProvider iFile
                $= providerToEntry haddockDir
                $$ (if quiet then id else (progress True  100 '*' =$))
                   (sinkEntries idx')

    handler ioe
        | isDoesNotExistError ioe = putStr "Error: " >> print   ioe
        | otherwise               = ioError ioe


data AddException
    = AlreadyExists PackageId
  deriving (Typeable)

instance Exception AddException

instance Show AddException where
    show (AlreadyExists pkg) =
        "Failed to write documentation for " ++ display pkg ++ ". " ++
        "Documentation for it already exists. " ++
        "Use -f/--force to overwrite it or -s/--skip to skip it."