packages feed

dash-haskell-1.1.0.1: src/Pipe/FileSystem.hs

-- TODO repl test file path methods.

{-# LANGUAGE OverloadedStrings #-}
module Pipe.FileSystem where
import           Control.Monad
import           Control.Monad.M
import qualified Data.List as L
import           Data.String.Util
import qualified Data.Text as T
import           Database.SQLite.Simple
import           System.FilePath
import           PackageConf
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
import qualified Module as Ghc

-- TODO the utility of some of these fields is still unclear to me,
-- at the moment they are filled simply to satisfy the docset spec.
plist :: String -> String 
plist str = 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>" ++ str ++ "</string>"
  , "<key>CFBundleName</key>"
  , "<string>docset for Haskell package " ++ str ++ "</string>"
  , "<key>DocSetPlatformFamily</key>"
  , "<string>haskell</string>" 
  , "<key>isDashDocset</key>"
  , "<true/>"
  , "<key>dashIndexFilePath</key>"
  , "<string>index.html</string>"
  , "</dict>"
  , "</plist>"
  ]

docsetDir :: Ghc.PackageKey -> FilePath
docsetDir k = Ghc.packageKeyString k ++ ".docset" 

leafs :: (FilePath -> Bool) -> FilePath -> ProducerM FilePath ()
leafs incPred p = do
  names <- liftIO $ getDirectoryContents p
  forM_ (filter (`notElem` [".", ".."]) names) $ \name' -> do
    let path = p </> name'
    dir <- liftIO . doesDirectoryExist $ 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 :: FilePath -> FilePath -> Either String FilePath
-- toStripped pfx original =
         
common :: FilePath -> FilePath -> FilePath
common l r = fst . unzip . takeWhile (uncurry (==)) $ zip (normalise l) (normalise r)

parent :: FilePath -> FilePath
parent = takeDirectory  
       
stripPrefix :: FilePath -> FilePath -> Either String FilePath 
stripPrefix prefix path = 
  if L.isPrefixOf prefix path then
    Right $ L.drop (L.length prefix + 1) path
  else 
    Left $ "prefix: " ++ prefix ++ " doesn't agree with:\n  " ++ path

toRelativePath :: FilePath -> FilePath -> Either String FilePath
toRelativePath base path = do
  let sharedPfx = common base path
  relative <- relativePath sharedPfx
  (</>) relative <$> stripPrefix sharedPfx path 
  where 
    relativePath :: FilePath -> Either String FilePath
    relativePath pfx = 
      joinPath 
       . flip replicate ".." 
       . length 
       . splitPath <$> stripPrefix pfx base 

relativize :: Ghc.PackageKey -> FilePath -> Either String T.Text 
relativize package p = 
  let filename'      = takeFileName p
      packageSubpath = Ghc.packageKeyString package
      matches        = 
        filter (packageSubpath ==) . reverse $ splitPath (parent p)
  in 
    T.pack <$> 
      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.PackageKey -> T.Text -> Either String T.Text 
convertUrl p urlExp 
  | T.null urlExp = Right T.empty
  | otherwise     =  
    if T.isPrefixOf "file:///" urlExp then 
      relativize p (T.unpack . T.drop 7 $ urlExp)
    else if T.isPrefixOf "/" urlExp then 
      relativize p $ T.unpack urlExp
    else
      Right urlExp
      
attributes :: 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" ++ src 
    
-- | Convert local package-compiled haddock links to local relative. 
convertLink :: Ghc.PackageKey -> 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.PackageKey -> PipeM FilePath (FilePath, Maybe String) ()
pipe_htmlConvert p = 
  forever $ do
    src <- await
    if Just (takeExtension src) /= Just "html" 
      then  
        yield (src, Nothing)
      else do 
        buf <- liftIO . readFile $ src
        -- Link conversion errors are non-fatal.
        case mapM (convertLink p src) . parseTags $ T.pack buf of
          Left e -> do 
            lift . warning $ 
              preposition "failed to convert links" "for" "file" src [e]
            yield (src, Nothing) 
          Right tags ->
            yield (src, Just . T.unpack . 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 :: FilePath -> FilePath -> ConsumerM (FilePath, Maybe String) () 
cons_writeFile src_root dst_root = forever $ do 
  (path, buf) <- await
  dst_relative_path <- lift . fromE $ stripPrefix src_root path 

  -- liftIO . putStrLn $ "src_root: " ++ show src_root
  -- liftIO . putStrLn $ "relative path: " ++ show dst_relative_path
  -- liftIO . putStrLn $ "path: " ++ show path

  liftIO $ do 
    let dst_path = dst_root </> dst_relative_path
    --liftIO . putStrLn $ "dst path: " ++ dst_path
    -- create requisite parent directories for the file at the destination
    D.createDirectoryIfMissing True $ parent dst_path 
    case buf of 
      Nothing   -> D.copyFile path dst_path 
      Just buf' -> writeFile dst_path buf'
  
cons_writeFiles :: FilePath -> ConsumerM PackageConf ()
cons_writeFiles docsets_root = forever $ do
  conf <- await
  
  lift . msg $ "processing: " ++ (Ghc.packageKeyString . pkg $ conf)
  let docset_folder = docsetDir (pkg conf) 
      dst_root      = docsets_root </> docset_folder 
      dst_doc_root  = dst_root </> "Contents/Resources/Documents/"

  liftIO . D.createDirectoryIfMissing True $ 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 -> Just (takeExtension p) /= Just "haddock") (htmlDir conf)
  
  -- 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 . writeFile (dst_root </> "Contents/Info.plist") . plist . 
    Ghc.packageKeyString . pkg $ conf 

  let db_path = dst_root </> "Contents/Resources/docSet.dsidx" 

  liftIO $ do
    db_exists <- D.doesFileExist db_path 
    when db_exists $ D.removeFile db_path

  -- Initialize the SQlite Db
  c' <- liftIO $ do 
        c <- open db_path 
        createTable c
        return c

  lift . indentM 2 $ msg "populating database.."

  -- 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"