packages feed

mathblog-0.6: src/MB/Util.hs

module MB.Util
    ( copyContents
    , toUtcTime
    , loadPostIndex
    , getModificationTime
    , allPostFilenames
    , dirFilenames
    , dirFilenamesRecursive
    , serializePostIndex
    , ensureDir
    , getInlineStr
    , fromInlines
    )
where
import Control.Applicative
import System.Directory
    ( doesDirectoryExist
    , doesFileExist
    , createDirectory
    , getDirectoryContents
    , copyFile
    )
import Data.Default
    ( def
    )
import System.FilePath
    ( (</>)
    , takeFileName
    , takeBaseName
    )
import Control.Monad.Trans (liftIO)
import Control.Monad.Trans.Either
import Control.Monad
    ( when
    , forM_
    , forM
    , filterM
    )
import System.Posix.Files
    ( getFileStatus
    , modificationTime
    , accessTime
    , setFileTimes
    )
import System.Posix.Types
    ( EpochTime
    )
import Data.List
    ( isSuffixOf
    , isPrefixOf
    )
import Data.Time.LocalTime
import Data.Time.Clock
import Data.Time.Format
    ( parseTime
    )
import Data.Time.Format
    ( defaultTimeLocale
    )
import System.IO
    ( IOMode(ReadMode)
    , openFile
    , hClose
    , hGetContents
    )
import Data.List
    ( sortBy
    )
import Data.Maybe
    ( fromJust
    , catMaybes
    )
import qualified Text.Pandoc as Pandoc
import MB.Types
import MB.TeXMacros
import Control.DeepSeq

-- |Given a source path SRC and a destination path DST, the SRC and
-- DST paths MUST exist and must both be directories.  All contents of
-- SRC/ will be copied, recursively, to DST/.
copyContents :: FilePath -> FilePath -> IO ()
copyContents src dst = do
  -- For each file in src, copy it to dest.
  entries <- filter (not . flip elem [".", ".."]) <$> getDirectoryContents src

  dirs <- filterM doesDirectoryExist $ map (src </>) entries
  files <- filterM doesFileExist $ map (src </>) entries

  -- For each directory in src, create it in dest, then descend into
  -- that directory in both src and dest.
  forM_ files $ \f -> do
    let destPath = dst </> takeFileName f
    st <- getFileStatus f
    copyFile f destPath
    setFileTimes destPath (accessTime st) (modificationTime st)

  forM_ dirs $ \dir ->
      do
        let dstDir = dst </> dirName
            dirName = takeFileName dir

        e <- doesDirectoryExist dstDir
        when (not e) $ do
          createDirectory dstDir
          st <- getFileStatus $ src </> dirName
          setFileTimes dstDir (accessTime st) (modificationTime st)

        copyContents (src </> dirName) dstDir

toUtcTime :: EpochTime -> UTCTime
toUtcTime t = fromJust $ parseTime defaultTimeLocale "%s" $ show t

loadPost :: FilePath -> EitherT String IO Post
loadPost fullPath = do
  fileContent <- liftIO $ readFile fullPath
  t <- liftIO $ getModificationTime fullPath
  (Pandoc.Pandoc m blocks) <- hoistEither $ case Pandoc.readMarkdown def fileContent of
      Left pandocErr -> Left $ show pandocErr
      Right v -> Right v

  -- Extract defined TeX macros in the post and store them in
  -- the post data structure to make them available to other
  -- parts of the page generation process (see Mathjax and TikZ
  -- for examples.)
  let (newBlocks, macros) = extractTeXMacros blocks

      pas = case Pandoc.docAuthors m of
              [] -> []
              as -> fromInlines <$> as

      pd = case Pandoc.docDate m of
             [] -> Nothing
             d -> Just $ fromInlines d

  tz <- liftIO getCurrentTimeZone
  localTime <- liftIO $ toLocalTime t
  let modStr = show localTime ++ "  " ++ timeZoneName tz
      bn = takeBaseName $ takeFileName fullPath
      htmlFilename =  bn ++ ".html"

  return $ Post { postTitle = Pandoc.docTitle m
                , postPath = fullPath
                , postFilename = takeFileName fullPath
                , postModificationTime = t
                , postAst = Pandoc.Pandoc m newBlocks
                , postTeXMacros = macros
                , postAuthors = pas
                , postDate = pd
                , postModificationString = modStr
                , postBaseName = bn
                , postUrl = "/posts/" ++ htmlFilename
                , postHtmlFilename = htmlFilename
                }

dirFilenames :: FilePath -> IO [FilePath]
dirFilenames dir = do
  allFiles <- getDirectoryContents dir
  return [ dir </> f | f <- allFiles
         , not $ "." `isPrefixOf` f
         ]

dirFilenamesRecursive :: FilePath -> IO [FilePath]
dirFilenamesRecursive dir = do
  entries <- filter (not . flip elem [".", ".."]) <$> getDirectoryContents dir
  dirs <- filterM doesDirectoryExist $ map (dir </>) entries
  files <- filterM doesFileExist $ map (dir </>) entries

  rest <- concat <$> forM dirs dirFilenamesRecursive
  return $ files ++ rest

allPostFilenames :: FilePath -> IO [FilePath]
allPostFilenames postSrcDir = do
  allFiles <- dirFilenames postSrcDir
  return [ f | f <- allFiles
         , ".txt" `isSuffixOf` f
         ]

getModificationTime :: FilePath -> IO UTCTime
getModificationTime fullPath = do
  info <- getFileStatus fullPath
  return $ toUtcTime $ modificationTime info

loadPostIndex :: BlogInputFS -> EitherT String IO [Post]
loadPostIndex ifs = do
  let indexFilename = ifsPostIndexPath ifs
  e <- liftIO $ doesFileExist indexFilename

  indexNames <- liftIO $ case e of
                  False -> return []
                  True -> do
                         h <- openFile indexFilename ReadMode
                         s <- hGetContents h
                         s `deepseq` return ()
                         let idx = unserializePostIndex s
                         hClose h
                         return idx

  -- Now that we have a postIndex to deal with, load posts from disk
  -- and insert them into the post index in the proper order
  postFiles <- liftIO $ allPostFilenames $ ifsPostSourceDir ifs
  posts <- mapM loadPost postFiles

  -- There are two types of posts to put into the index: the ones that
  -- are not already in the index, and the ones that are (and in a
  -- specific order).

  let pairs = [ (postFilename p, p) | p <- posts ]
      newPosts = [ p | p <- posts, not $ postFilename p `elem` indexNames ]
      preexistingPosts = catMaybes [ lookup n pairs | n <- indexNames ]
      ps = sortPosts newPosts ++ preexistingPosts

  return ps

serializePostIndex :: [Post] -> String
serializePostIndex ps = unlines $ map postFilename ps

unserializePostIndex :: String -> [String]
unserializePostIndex = lines

sortPosts :: [Post] -> [Post]
sortPosts = sortBy (\a b -> postModificationTime b `compare`
                            postModificationTime a)

ensureDir :: FilePath -> IO ()
ensureDir d = do
  exists <- doesDirectoryExist d
  when (not exists) $ createDirectory d

getInlineStr :: Pandoc.Inline -> String
getInlineStr (Pandoc.Str s) = s
getInlineStr (Pandoc.Math _ s) = s
getInlineStr Pandoc.Space = " "
getInlineStr i = error $ "Unexpected inline in document title, got " ++ (show i)

fromInlines :: [Pandoc.Inline] -> String
fromInlines = concat . (getInlineStr <$>)


toLocalTime :: UTCTime -> IO LocalTime
toLocalTime u = do
  tz <- getCurrentTimeZone
  return $ utcToLocalTime tz u