packages feed

parochial-0.1.0.0: src/Parochial/HaddockGenerator.hs

module Parochial.HaddockGenerator (
    createSymLinkFarm
  , installedHaddocks
  , installedHaddocks'
  ) where


import           Protolude hiding (packageName)

import qualified Data.List as L

import           Distribution.Types.Version
import           Distribution.Types.PackageId
import           Distribution.Types.PackageName
import           Distribution.Types.LocalBuildInfo

import           Distribution.Simple.PackageIndex
import           Distribution.InstalledPackageInfo

import           System.FilePath
import           System.Directory
import           System.PosixCompat.Files

import           Text.Blaze.Html.Renderer.Pretty ( renderHtml )

import           Text.Blaze.Html5 ((!))
import qualified Text.Blaze.Html5 as H
import qualified Text.Blaze.Html5.Attributes as A

import           Parochial.Types


-- | Predefined list of blacklisted packages. This is dirty nasty hack because Hoogle
--   dies when it parses certain files it shouldn't, ironically it dies parsing it's
--   own package! See https://github.com/ndmitchell/hoogle/issues/362 for more details.
--
--   The downside of this approach is that it won't build the documentation for these
--   packages
blackListedPagkages :: [Text]
blackListedPagkages = ["hoogle"]


-- | Construct a project specific symlink farm which links the installed haddocks to the
--   project and wraps it in a (very) primitive html page.
createSymLinkFarm :: Target -> [Pkg] -> IO ()
createSymLinkFarm t i = removeSymLinks t >> (generateSymLinks t i >>= createIndex t)


-- | Builds a symlink farm from the package database so that when the documentation is served
--   up by a web server the links are correct.
generateSymLinks :: Target -> [Pkg] -> IO [Pkg]
generateSymLinks target = traverse generateSymLink
  where
    generateSymLink (pId, p) = createSymbolicLink p (path pId) >> pure (pId, p)
    path = (target </>) . toS . packageName


-- | Remove all symlinks to ensure there are no dangling links or links pointing to the wrong
--   versions.
removeSymLinks :: Target -> IO ()
removeSymLinks path = listDirectoryAbs >>= filterM pathIsSymbolicLink >>= traverse_ removeFile
  where
    listDirectoryAbs = listDirectory path >>= traverse (pure . (path </>))


-- | Contrive a @Pkg@ for the locally installed documentation
projectPkg :: LocalBuildInfo -> Pkg
projectPkg lbi = (projectIdentifier lbi, takeDirectory (buildDir lbi) </> "doc" </> "html" </> name </> name)
  where
    name = toS $ packageName (projectIdentifier lbi)


-- | Extract the @PackageIdentifier@ for the project.
projectIdentifier :: LocalBuildInfo -> PackageIdentifier
projectIdentifier = localPackage


-- | Find all project dependencies and filter out any promlematic ones. This isn't
--   particularly efficeient but given it's performing IO it won't make much difference.
installedHaddocks :: LocalBuildInfo -> IO [Pkg]
installedHaddocks = filterM hasIndexHtml . extract . filterPackages . allPackages . installedPkgs
  where
    -- FIXME mconcat not correct and needs to be done properly. I'm not sure what properly is though!
    extract :: [InstalledPackageInfo] -> [Pkg]
    extract = map (\i -> (sourcePackageId i, mconcat $ haddockHTMLs i))

    filterPackages :: [InstalledPackageInfo] -> [InstalledPackageInfo]
    filterPackages = L.filter (liftM2 (&&) isBlacklisted isNotEmpty)

    isBlacklisted :: InstalledPackageInfo -> Bool
    isBlacklisted = not . flip elem blackListedPagkages . packageName . sourcePackageId

    isNotEmpty :: InstalledPackageInfo -> Bool
    isNotEmpty = not . null . haddockHTMLs

    hasIndexHtml :: Pkg -> IO Bool
    hasIndexHtml = fileExist . indexPath . snd


-- | The same as @installedHaddocks@ but included the project documentation.
installedHaddocks' :: LocalBuildInfo -> IO [Pkg]
installedHaddocks' i = installedHaddocks i >>= \h -> return (projectPkg i : h)


-------------------------------------------------------
-- HTML
-------------------------------------------------------

-- | Write the project specific index.html file.
createIndex :: Target -> [Pkg] -> IO ()
createIndex t ps = withFile (indexPath t) WriteMode write
  where
    write = flip hPutStr (renderHtml generateHtml)

    generateHtml = H.docTypeHtml $ do
      H.head $ H.title (H.text $ toS t)
      H.body $ H.ul $ forM_ ps (H.li . pkgLink)

    pkgLink :: Pkg -> H.Html
    pkgLink (pId, _) = H.a ! A.href (H.toValue (indexPath (toS $ packageName pId))) $
                               H.text (libName pId)


indexPath :: FilePath -> FilePath
indexPath = (</> "index.html")


-------------------------------------------------------
-- Utilities
-------------------------------------------------------

libName :: PackageIdentifier -> Text
libName p = packageName p <> " — " <> fmtVersion p
  where
    fmtVersion = toS . L.intercalate "." . map show . versionNumbers . pkgVersion


packageName :: PackageIdentifier -> Text
packageName = toS . unPackageName . pkgName