module Main where import Control.Applicative import Control.Monad import Control.Concurrent import Data.Maybe import System.IO import System.Exit import System.Directory import System.FilePath import Data.Time.LocalTime import qualified Text.Pandoc as Pandoc import qualified MB.Config as Config import MB.Templates ( loadTemplate , renderTemplate ) import MB.Processing import MB.Util ( copyTree , toLocalTime , rssModificationTime , loadPostIndex , anyChanges , serializePostIndex , summarizeChanges ) import MB.Types import qualified MB.Files as Files import Paths_mathblog ( getDataFileName ) import MB.Startup ( StartupConfig(..) , dataDirectory , listenMode , startupConfigFromEnv , forceRegeneration , initDataDirectory ) import MB.Gnuplot import MB.Tikz import MB.Mathjax import MB.Gladtex defaultConfigFilename :: String defaultConfigFilename = "blog.cfg" skelDir :: IO FilePath skelDir = getDataFileName "skel" commonTemplateAttrs :: Blog -> [(String, String)] commonTemplateAttrs blog = [ ( "baseUrl", baseUrl blog ) , ( "title", title blog ) , ( "authorName", authorName blog ) , ( "authorEmail", authorEmail blog ) , ( "extraPageHead", extraPageHead blog ) ] extraPageHead :: Blog -> String extraPageHead b = concat $ catMaybes $ pageHead <$> processors b fillTemplate :: Blog -> Template -> [(String, String)] -> String fillTemplate blog t attrs = renderTemplate attrs' t where attrs' = commonTemplateAttrs blog ++ attrs writeTemplate :: Blog -> Handle -> Template -> [(String, String)] -> IO () writeTemplate blog h t attrs = hPutStr h $ fillTemplate blog t attrs writePost :: Blog -> Handle -> Post -> IO () writePost blog h post = do let writerOpts = getWriterOptions blog Pandoc.defaultWriterOptions created <- postModificationString post hPutStr h $ "

" ++ getPostTitle blog post BlogPost ++ "

" hPutStr h $ "Posted " ++ created ++ "" hPutStr h $ Pandoc.writeHtmlString writerOpts (postAst post) buildLinks :: Blog -> Maybe Post -> Maybe Post -> String buildLinks _blog prev next = "
" ++ link "next-link" "older »" next ++ link "prev-link" "« newer" prev ++ "
" where link cls name Nothing = "" ++ name ++ "" link cls name (Just p) = "" ++ name ++ "" jsInfo :: Post -> String jsInfo post = "\n" buildPage :: Handle -> Blog -> String -> Maybe String -> IO () buildPage h blog content extraTitle = do eTmpl <- loadTemplate $ Files.pageTemplatePath blog case eTmpl of Left msg -> putStrLn msg >> exitFailure Right tmpl -> do let attrs = [ ("content", content) ] ++ maybe [] (\t -> [("extraTitle", t)]) extraTitle writeTemplate blog h tmpl attrs hClose h buildPost :: Handle -> Blog -> Post -> (Maybe Post, Maybe Post) -> IO () buildPost h blog post prevNext = do eTmpl <- loadTemplate $ Files.postTemplatePath blog case eTmpl of Left msg -> putStrLn msg >> exitFailure Right tmpl -> do html <- readFile $ Files.postIntermediateHtml blog post let attrs = [ ("post", html) , ("nextPrevLinks", uncurry (buildLinks blog) prevNext) , ("jsInfo", jsInfo post) ] let out = (fillTemplate blog tmpl attrs) buildPage h blog out $ Just $ getRawPostTitle blog post generatePost :: Blog -> Post -> ChangeSummary -> IO () generatePost blog post summary = do let finalHtml = Files.postIntermediateHtml blog post generate = (postFilename post `elem` (postsChanged summary)) || configChanged summary when generate $ do putStrLn $ "Rendering " ++ Files.postBaseName post newPost <- applyPreProcessors blog post h <- openFile finalHtml WriteMode writePost blog h newPost hClose h applyPostProcessors blog finalHtml BlogPost generatePosts :: Blog -> ChangeSummary -> IO () generatePosts blog summary = do let numRegenerated = if configChanged summary then length $ blogPosts blog else length $ postsChanged summary when (numRegenerated > 0) $ putStrLn $ "Rendering " ++ (show numRegenerated) ++ " post(s)..." let n = length posts posts = blogPosts blog forM_ (zip posts [0..]) $ \(p, i) -> do let prevPost = if i == 0 then Nothing else Just (posts !! (i - 1)) nextPost = if i == n - 1 then Nothing else Just (posts !! (i + 1)) generatePost blog p summary h <- openFile (Files.postFinalHtml blog p) WriteMode buildPost h blog p (prevPost, nextPost) hClose h buildIndexPage :: Blog -> IO () buildIndexPage blog = do let src = Files.postFinalHtml blog post index = Files.indexHtml blog post = head $ blogPosts blog copyFile src index postModificationString :: Post -> IO String postModificationString p = do tz <- getCurrentTimeZone localTime <- toLocalTime $ postModificationTime p return $ show localTime ++ " " ++ timeZoneName tz generatePostList :: Blog -> IO () generatePostList blog = do -- For each post in the order they were given, extract the -- unrendered title and construct an htex document. Then render it -- to the listing location. entries <- forM (blogPosts blog) $ \p -> do created <- postModificationString p return $ concat [ "
" , "" , getPostTitle blog p Index , "Posted " , created , "
\n" ] let content = "
" ++ concat entries ++ "
" h <- openFile (Files.listHtml blog) WriteMode buildPage h blog content Nothing hClose h applyPostProcessors blog (Files.listHtml blog) Index rssItem :: Blog -> Post -> String rssItem blog p = concat [ "" , "" ++ getRawPostTitle blog p ++ "\n" , "" ++ baseUrl blog ++ Files.postUrl p ++ "\n" , "" ++ rssModificationTime p ++ "\n" , "" ++ baseUrl blog ++ Files.postUrl p ++ "\n" , "\n" ] generateRssFeed :: Blog -> IO () generateRssFeed blog = do h <- openFile (Files.rssXml blog) WriteMode eTmpl <- loadTemplate $ Files.rssTemplatePath blog case eTmpl of Left msg -> putStrLn msg >> exitFailure Right tmpl -> do let items = map (rssItem blog) $ blogPosts blog itemStr = concat items attrs = [ ("items", itemStr) ] writeTemplate blog h tmpl attrs hClose h initializeDataDir :: FilePath -> IO () initializeDataDir dir = do existsBase <- doesDirectoryExist dir existsConfig <- doesFileExist $ dir defaultConfigFilename dataDir <- skelDir when (not existsBase || not existsConfig) $ do putStrLn $ "Setting up data directory using skeleton: " ++ dataDir when (not existsBase) $ createDirectory dir copyTree dataDir dir when existsConfig $ putStrLn $ "Data directory already initialized; found " ++ (dir defaultConfigFilename) ensureDirs :: Blog -> IO () ensureDirs blog = do let dirs = [ postSourceDir , htmlDir , assetDir , postHtmlDir , postIntermediateDir , imageDir , templateDir , htmlTempDir , eqPreamblesDir ] forM_ (dirs <*> pure blog) $ \d -> do exists <- doesDirectoryExist d when (not exists) $ createDirectory d scanForChanges :: IO Bool -> IO () scanForChanges act = do scan where scan = do didWork <- act when didWork $ putStrLn "" threadDelay $ 1 * 1000 * 1000 scan mathBackends :: [(String, Processor)] mathBackends = [ ("gladtex", gladtexProcessor) , ("mathjax", mathjaxProcessor) ] eqBackends :: [(String, Processor)] eqBackends = [ ("gnuplot", gnuplotProcessor) , ("tikz", tikzProcessor) ] mkBlog :: StartupConfig -> IO Blog mkBlog conf = do let base = dataDirectory conf configFile = base (configFilePath conf) e <- doesFileExist configFile when (not e) $ do putStrLn $ "Configuration file " ++ configFile ++ " not found" exitFailure let requiredValues = [ "baseUrl" , "title" , "authorName" , "authorEmail" ] cfg <- Config.readConfig configFile requiredValues let Just cfg_baseUrl = lookup "baseUrl" cfg Just cfg_title = lookup "title" cfg Just cfg_authorName = lookup "authorName" cfg Just cfg_authorEmail = lookup "authorEmail" cfg -- Load blog posts from disk let postSrcDir = base "posts" allPosts <- loadPostIndex postSrcDir let requestedMathBackend = lookup "mathBackend" cfg mathBackend = case requestedMathBackend of Nothing -> mathjaxProcessor Just b -> case lookup b mathBackends of Nothing -> error $ "Unsupported math backend " ++ show b ++ "; valid choices are " ++ (show $ fst <$> mathBackends) Just proc -> proc requestedEqBackend = lookup "eqBackend" cfg eqBackend = case requestedEqBackend of Nothing -> gnuplotProcessor Just b -> case lookup b eqBackends of Nothing -> error $ "Unsupported equation backend " ++ show b ++ "; valid choices are " ++ (show $ fst <$> eqBackends) Just proc -> proc procs = [eqBackend, mathBackend] let html = htmlOutputDirectory conf b = Blog { baseDir = base , postSourceDir = postSrcDir , htmlDir = html , assetDir = base "assets" , postHtmlDir = html "posts" , postIntermediateDir = base "generated" , imageDir = html "generated-images" , templateDir = base "templates" , htmlTempDir = base "tmp" , baseUrl = cfg_baseUrl , title = cfg_title , authorName = cfg_authorName , authorEmail = cfg_authorEmail , eqPreamblesDir = base "eq-preambles" , configPath = configFile , blogPosts = allPosts , processors = procs } ensureDirs b return b installAssets :: Blog -> IO () installAssets blog = do let ad = assetDir blog -- For each file and directory in assets/, copy it to the output -- directory. entries <- filter (not . flip elem [".", ".."]) <$> getDirectoryContents ad dirs <- filterM doesDirectoryExist $ map (ad ) entries files <- filterM doesFileExist $ map (ad ) entries forM_ dirs $ \d -> copyTree d (htmlDir blog (takeBaseName d)) forM_ files $ \f -> copyFile f (htmlDir blog) regenerateContent :: StartupConfig -> IO Bool regenerateContent conf = do blog <- mkBlog conf summary <- summarizeChanges blog (forceRegeneration conf) case anyChanges summary of False -> return False True -> do putStrLn $ "Blog directory: " ++ baseDir blog putStrLn $ "Config file: " ++ configFilePath conf when (configChanged summary) $ putStrLn "Configuration file changed; regenerating all content." when (templatesChanged summary) $ putStrLn "Templates changed; regenerating accordingly." when (not $ null $ postsChanged summary) $ do putStrLn "Posts changed:" forM_ (postsChanged summary) $ \n -> putStrLn $ " " ++ n when (postIndexChanged summary) $ putStrLn "Post index changed; regenerating next/previous links." when (assetsChanged summary) $ do putStrLn "Assets changed; reinstalling." installAssets blog generatePosts blog summary buildIndexPage blog generatePostList blog generateRssFeed blog writeFile (Files.postIndex blog) $ serializePostIndex $ blogPosts blog putStrLn "Done." return True main :: IO () main = do conf <- startupConfigFromEnv let dir = dataDirectory conf when (initDataDirectory conf) $ initializeDataDir dir case listenMode conf of False -> do didWork <- regenerateContent conf when (not didWork) $ putStrLn "No changes found!" True -> do putStrLn $ "Waiting for changes in " ++ (dataDirectory conf) ++ " ..." scanForChanges (regenerateContent $ conf { forceRegeneration = False })