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 System.Posix.Files ( createSymbolicLink ) 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 ( dataDirectory , listenMode , startupConfigFromEnv , forceRegeneration , initDataDirectory ) import MB.Gnuplot import MB.Tikz import MB.Mathjax import MB.Gladtex skelDir :: IO FilePath skelDir = getDataFileName "skel" configFilename :: String configFilename = "blog.cfg" 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 linkIndexPage :: Blog -> IO () linkIndexPage blog = do let dest = Files.postFinalHtml blog post index = Files.indexHtml blog post = head $ blogPosts blog exists <- doesFileExist index when exists $ removeFile index createSymbolicLink dest 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 configFilename 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 configFilename) ensureDirs :: Blog -> IO () ensureDirs blog = do let dirs = [ postSourceDir , htmlDir , stylesheetDir , postHtmlDir , postIntermediateDir , imageDir , templateDir , htmlTempDir , eqPreamblesDir ] forM_ (dirs <*> pure blog) $ \d -> do exists <- doesDirectoryExist d when (not exists) $ createDirectory d scanForChanges :: FilePath -> (FilePath -> IO Bool) -> IO () scanForChanges dir act = do scan where scan = do didWork <- act dir 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 :: FilePath -> IO Blog mkBlog base = do let configFilePath = base configFilename e <- doesFileExist configFilePath when (not e) $ do putStrLn $ "Configuration file " ++ configFilePath ++ " not found" exitFailure let requiredValues = [ "baseUrl" , "title" , "authorName" , "authorEmail" ] cfg <- Config.readConfig configFilePath 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 b = Blog { baseDir = base , postSourceDir = postSrcDir , htmlDir = base "html" , stylesheetDir = base "html" "stylesheets" , postHtmlDir = base "html" "posts" , postIntermediateDir = base "generated" , imageDir = base "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 = configFilePath , blogPosts = allPosts , processors = procs } ensureDirs b return b regenerateContent :: Bool -> FilePath -> IO Bool regenerateContent force dir = do blog <- mkBlog dir summary <- summarizeChanges blog force case anyChanges summary of False -> return False True -> do putStrLn $ "Blog directory: " ++ baseDir blog 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." generatePosts blog summary linkIndexPage 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 (forceRegeneration conf) dir when (not didWork) $ putStrLn "No changes found!" True -> scanForChanges dir (regenerateContent False)