dash-haskell-1.0.0.1: src/Pipes/FileSystem.hs
{-# LANGUAGE OverloadedStrings #-}
module Pipes.FileSystem where
import Control.Applicative
import Control.Monad
import Control.Monad.M
import qualified Data.ByteString as BS
import Data.ByteString.Char8 (pack)
import qualified Data.List as L
import Data.String.Util
import qualified Data.Text as T
import Data.Text.Encoding
import Database.SQLite.Simple
import Filesystem as F
import Filesystem.Path.CurrentOS ((</>))
import qualified Filesystem.Path.CurrentOS as P
import qualified Module as Ghc
import Package.Conf
import Pipes
import System.Directory ( doesDirectoryExist, getDirectoryContents )
import qualified System.Directory as D
import Text.HTML.TagSoup
import Text.HTML.TagSoup.Match
import Haddock.Artifact
import Haddock.Sqlite
-- | See:
-- https://developer.apple.com/library/ios/documentation/general/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
data Plist = Plist
{ cfBundleIdentifier :: String
, cfBundleName :: String
, docSetPlatformFamily :: String
} deriving Show
-- TODO/FIXME the utility of some of these fields are still unclear to me,
-- at the moment they are filled simply to satisfy the docset spec.
plist :: Ghc.PackageId -> BS.ByteString
plist p = Data.ByteString.Char8.pack . unlines $
[ "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
, "<!DOCTYPE plist PUBLIC \"-//Apple//DTD PLIST 1.0//EN\" \"http://www.apple.com/DTDs/PropertyList-1.0.dtd\">"
, "<plist version=\"1.0\">"
, "<dict>"
, "<key>CFBundleIdentifier</key>"
, "<string>" ++ Ghc.packageIdString p ++ "</string>"
, "<key>CFBundleName</key>"
, "<string>docset for Haskell package " ++ Ghc.packageIdString p ++ "</string>"
, "<key>DocSetPlatformFamily</key>"
, "<string>haskell</string>"
, "<key>isDashDocset</key>"
, "<true/>"
, "<key>dashIndexFilePath</key>"
, "<string>index.html</string>"
, "</dict>"
, "</plist>"
]
docsetDir :: Ghc.PackageId -> P.FilePath
docsetDir p = P.decodeString $ Ghc.packageIdString p ++ ".docset"
leafs :: (P.FilePath -> Bool) -> P.FilePath -> ProducerM P.FilePath ()
leafs incPred p = do
names <- liftIO . getDirectoryContents $ P.encodeString p
forM_ (filter (`notElem` [".", ".."]) names) $ \name' -> do
let path = p </> P.decodeString name'
dir <- liftIO . doesDirectoryExist $ P.encodeString path
(if dir then
leafs incPred
else if not . incPred $ path then const (return ()) else yield)
path
type Tag' = Tag T.Text
remoteUrl :: T.Text -> Bool
remoteUrl url = any (T.isPrefixOf url) ["http://", "https://"]
toStripped :: P.FilePath -> P.FilePath -> Either String P.FilePath
toStripped pfx original =
-- I don't understand why System.FilePath.CurrentOS necessitates
-- additional checking after the prefix has already been determined.
case P.stripPrefix pfx original of
Nothing ->
Left $
"attempted to strip prefix: \n "
++ P.encodeString pfx ++ " from: \n " ++ P.encodeString original
Just remainder ->
Right remainder
toRelativePath :: P.FilePath -> P.FilePath -> Either String P.FilePath
toRelativePath base path = do
let sharedPfx = P.commonPrefix [base, path]
relative <- relativePath sharedPfx
(</>) relative <$> toStripped sharedPfx path
where
relativePath :: P.FilePath -> Either String P.FilePath
relativePath pfx =
P.concat
. flip replicate ".."
. length
. P.splitDirectories <$> toStripped pfx base
relativize :: Ghc.PackageId -> P.FilePath -> Either String T.Text
relativize package p =
let filename = P.filename p
packageSubpath = P.decodeString $ Ghc.packageIdString package
matches = filter (packageSubpath ==) . reverse $ P.splitDirectories (P.parent p)
in
T.pack . P.encodeString <$>
if L.null matches then
return p -- preserve the path so that it still can be used
else -- assume as a package doc file and make relative
toRelativePath packageSubpath $ L.head matches </> filename
convertUrl :: Ghc.PackageId -> T.Text -> Either String T.Text
convertUrl p urlExp
| T.null urlExp = Right T.empty
| otherwise =
if T.isPrefixOf "file:///" urlExp then
relativize p (P.fromText . T.drop 7 $ urlExp)
else if T.isPrefixOf "/" urlExp then
relativize p $ P.fromText urlExp
else
Right urlExp
attributes :: P.FilePath -> Tag T.Text -> Either String [Attribute T.Text]
attributes _ (TagOpen _ list) =
Right list
attributes src other =
Left $
"failed to retrieve expected attributes from tag:\n "
++ show other ++ "\n in: \n" ++ P.encodeString src
-- | Convert local package-compiled haddock links to local relative.
convertLink :: Ghc.PackageId -> P.FilePath -> Tag' -> Either String Tag'
convertLink package src tag =
-- We're only interested in processing links
if not $ tagOpenLit "a" (anyAttrNameLit "href") tag then
Right tag
else do
preserved <- filter (\(n,_) -> n /= "href") <$> attributes src tag
let url = fromAttrib "href" tag
if remoteUrl url
then
Right tag -- ignore remote links
else do
url' <- convertUrl package url
Right . TagOpen "a" $ ("href", url') : preserved
pipe_htmlConvert :: Ghc.PackageId -> PipeM P.FilePath (P.FilePath, Maybe BS.ByteString) ()
pipe_htmlConvert p =
forever $ do
src <- await
if P.extension src /= Just "html"
then
yield (src, Nothing)
else do
buf <- liftIO $ F.readTextFile src
-- Link conversion errors are non-fatal.
case mapM (convertLink p src) . parseTags $ buf of
Left e -> do
lift . warning $
preposition "failed to convert links" "for" "file" (P.encodeString src) [e]
yield (src, Nothing)
Right tags ->
yield (src, Just . encodeUtf8 . renderTags $ tags)
-- | This consumes a doc file and copies it to a path in 'dstRoot'.
-- By pre-condition:
-- path has src_root as an ancestor
-- By post-condition:
-- written dst is the difference of path and src_root,
-- with by the concatenation of dst_root as it's parent.
cons_writeFile :: P.FilePath -> P.FilePath -> ConsumerM (P.FilePath, Maybe BS.ByteString) ()
cons_writeFile src_root dst_root = forever $ do
(path, buf) <- await
case P.stripPrefix src_root path of
Nothing -> lift . err $
"filepath error when attempting to find common prefix between src: \n"
++ P.encodeString path ++ "\n and: \n" ++ P.encodeString src_root
Just dst_relative_path ->
-- Yes, this could be shorter, but I try not to unnecessarily obfuscate
liftIO $ do
let dst_path = dst_root </> dst_relative_path
-- create requisite parent directories for the file at the destination
F.createTree $ P.parent dst_path
case buf of
Nothing -> F.copyFile path dst_path
Just buf' -> F.writeFile dst_path buf'
cons_writeFiles :: P.FilePath -> ConsumerM Conf ()
cons_writeFiles docsets_root = forever $ do
conf <- await
lift . msg $ "processing: " ++ (Ghc.packageIdString . pkg $ conf)
let docset_folder = docsetDir (pkg conf)
dst_root = docsets_root </> docset_folder
dst_doc_root = dst_root </> P.decodeString "Contents/Resources/Documents/"
liftIO . F.createTree $ dst_doc_root
-- Copy all files and convert if necessary
lift . indentM 2 $ msg "writing files.."
lift . runEffect $
cons_writeFile (htmlDir conf) dst_doc_root
<-< pipe_htmlConvert (pkg conf)
<-< leafs (\p -> P.extension p /= Just "haddock") (htmlDir conf)
-- FIXME/TODO Since the haddock index is already produced in source docs
-- with latest packaging systems, this is likely unnecessary
-- liftIO $ do
-- putStrLn "running haddock indexes"
-- runHaddockIndex (interfaceFile conf) dst_doc_root
lift . indentM 2 $ msg "writing plist.."
liftIO . F.writeFile (dst_root </> "Contents/Info.plist") $ plist (pkg conf)
lift . indentM 2 $ msg "populating database.."
let db_path = dst_root </> P.decodeString "Contents/Resources/docSet.dsidx"
liftIO $ do
db_exists <- D.doesFileExist . P.encodeString $ db_path
when db_exists $ F.removeFile db_path
-- Initialize the SQlite Db
c' <- liftIO $ do
c <- open . P.encodeString $ db_path
createTable c
return c
-- Populate the SQlite Db
liftIO $ execute_ c' "BEGIN;"
artifacts <- lift $ toArtifacts (pkg conf) (interfaceFile conf)
lift $ mapM_ (fromArtifact (pkg conf) c') artifacts
liftIO $ execute_ c' "COMMIT;"
liftIO . close $ c'
lift . indentM 2 $ msg "finished populating sqlite database.."
lift $ msg "\n"