mathblog-0.6: src/Main.hs
module Main where
import Control.Applicative
import Control.Monad
import Control.Monad.Reader
import Control.Concurrent
import Control.Monad.Trans.Either
import Data.Maybe
import Data.Time.Clock
import Data.Time.Calendar
import Data.List
import System.IO
import System.Exit
import System.Directory
hiding (getModificationTime)
import System.FilePath
import System.FSNotify
import qualified MB.Config as Config
import MB.Server
import MB.Util
import MB.Types
import MB.Startup
import MB.Initialize
import MB.Templates
import MB.Gen.Post
import MB.Gen.PostList
import MB.Gen.Index
import MB.Gen.RSS
import MB.Processors.Tikz
import MB.Processors.Mathjax
import MB.Processors.Base
ensureDirs :: BlogInputFS -> BlogOutputFS -> IO ()
ensureDirs ifs ofs = do
let ifsDirs = [ ifsPostSourceDir
, ifsAssetDir
, ifsTemplateDir
]
ofsDirs = [ ofsBaseDir
, ofsPostHtmlDir
, ofsImageDir
, ofsHtmlTempDir
]
forM_ (ifsDirs <*> pure ifs) ensureDir
forM_ (ofsDirs <*> pure ofs) ensureDir
doGeneration :: StartupConfig -> Blog -> (GenEvent -> IO ()) -> IO ()
doGeneration config blog handler = do
ch <- newChan
mv <- newEmptyMVar
let waitForEvents = do
ev <- readChan ch
handler ev
case ev of
Finished -> putMVar mv ()
_ -> waitForEvents
_ <- forkIO waitForEvents
runBlogM blog ch config regenerateContent
-- Signal event handler to shut down
writeChan ch Finished
-- Wait for event handler to shut down
_ <- readMVar mv
return ()
isEventInteresting :: BlogInputFS -> Event -> Bool
isEventInteresting ifs ev =
let fp = case ev of
Added f _ -> f
Modified f _ -> f
Removed f _ -> f
isPost f = (ifsPostSourceDir ifs) `isPrefixOf` f &&
".txt" `isSuffixOf` f &&
not ("." `isPrefixOf` takeFileName f)
isPostIndex f = ifsPostIndexPath ifs == f
isAsset f = (ifsAssetDir ifs) `isPrefixOf` f
isTemplate f = (ifsTemplateDir ifs) `isPrefixOf` f &&
(not ("." `isPrefixOf` takeFileName f)) &&
((".html" `isSuffixOf` f) ||
(".xml" `isSuffixOf` f))
isConfig f = ifsConfigPath ifs == f
in or ([ isPost
, isAsset
, isTemplate
, isConfig
, isPostIndex
] <*> pure fp)
scanForChanges :: StartupConfig
-> (GenEvent -> IO ())
-> (Blog -> Blog)
-> IO ()
-> IO ()
scanForChanges conf h blogTrans signalAct = do
let ifs = blogInputFS conf
forever $ do
withManager $ \m ->
do
let loadBlog =
do
result <- runEitherT (mkBlog conf)
case result of
Left e -> do
putStrLn $ "Error reading blog configuration: " ++ e
threadDelay 1000000
loadBlog
Right blog -> return $ blogTrans blog
blog <- loadBlog
doGeneration conf blog h
signalAct
putStrLn ""
-- Wait for a bit so the filesystem scan doesn't pick up the
-- posts-index change in the source tree
threadDelay $ 500 * 1000
ch <- newChan
watchTreeChan m (dataDirectory conf) (const True) ch
let nextEv = do
ev <- readChan ch
if isEventInteresting ifs ev then return ev else nextEv
evt <- nextEv
case evt of
Added fp _ -> putStrLn $ "File created: " ++ fp
Modified fp _ -> putStrLn $ "File modified: " ++ fp
Removed fp _ -> putStrLn $ "File removed: " ++ fp
mathBackends :: [(String, Processor)]
mathBackends =
[ ("mathjax", mathjaxProcessor)
]
eqBackends :: [(String, Processor)]
eqBackends =
[ ("tikz", tikzProcessor)
]
getIFSState :: BlogInputFS -> BlogOutputFS -> IO BlogInputFSState
getIFSState ifs ofs =
BlogInputFSState <$> indexMod <*> configMod <*> baseline <*> template
where
-- As a fallback timestamp, use a time in the past to ensure
-- things get regenerated if they're missing.
fallback = previousDay <$> getCurrentTime
previousDay t =
t { utctDay = addDays (-1000) (utctDay t) }
indexMod = do
let pth = ifsPostIndexPath ifs
ex <- doesFileExist pth
if ex then getModificationTime pth else fallback
configMod = do
let pth = ifsConfigPath ifs
ex <- doesFileExist pth
if ex then getModificationTime pth else fallback
template = do
contents <- getDirectoryContents $ ifsTemplateDir ifs
let templates = filter (not . flip elem [".", ".."]) contents
templatePath = (ifsTemplateDir ifs </>)
last <$> sort <$>
mapM getModificationTime (templatePath <$> templates)
baseline = do
let indexHtml = ofsIndexHtml ofs
ex <- doesFileExist indexHtml
if ex then getModificationTime indexHtml else fallback
mkBlog :: StartupConfig -> EitherT String IO Blog
mkBlog conf = do
let ifs = blogInputFS conf
ofs = blogOutputFS conf
configPath = ifsConfigPath ifs
e <- liftIO $ doesFileExist configPath
case e of
False -> left $ "Configuration file " ++ configPath ++ " not found"
True -> return ()
let requiredValues = [ "baseUrl"
, "title"
, "authorName"
, "authorEmail"
]
cfg <- Config.readConfig configPath 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
allPosts <- loadPostIndex ifs
let requestedMathBackend = lookup "mathBackend" cfg
isBackendRequested (nam, p) =
let Just opt = lookup nam cfg <|> Just "no"
in if Config.affirmative opt
then Just p
else Nothing
mathBackend <- case requestedMathBackend of
Nothing -> return mathjaxProcessor
Just b -> case lookup b mathBackends of
Nothing -> left $ "Unsupported math backend " ++ show b
++ "; valid choices are "
++ (show $ fst <$> mathBackends)
Just proc -> return proc
let procs = baseProcessor : eqBackendConfig ++ [mathBackend]
eqBackendConfig = catMaybes $ isBackendRequested <$> eqBackends
ifsState <- liftIO $ getIFSState ifs ofs
liftIO $ ensureDirs ifs ofs
return $ Blog { inputFS = ifs
, outputFS = ofs
, inputFSState = ifsState
, baseUrl = fromJust (overrideBaseUrl conf <|> Just cfg_baseUrl)
, title = cfg_title
, authorName = cfg_authorName
, authorEmail = cfg_authorEmail
, blogPosts = allPosts
, processors = procs
}
-- For each configured document processor, run its check routine in
-- case it needs to install data files or do validation.
runProcessorChecks :: BlogM ()
runProcessorChecks = do
ps <- processors <$> theBlog
let checks = catMaybes $ checkDataDir <$> ps
sequence_ checks
doInstallAssets :: BlogM ()
doInstallAssets = do
ps <- processors <$> theBlog
let fs = catMaybes $ installAssets <$> ps
sequence_ fs
regenerateContent :: BlogM ()
regenerateContent = do
blog <- theBlog
let ifs = inputFS blog
ofs = outputFS blog
runProcessorChecks
generatePosts
buildIndexPage
generatePostList
withTemplate (ifsRssTemplatePath ifs) $ \t ->
liftIO $ writeFile (ofsRssXml ofs) $ generateRssFeed blog t
liftIO $ writeFile (ifsPostIndexPath ifs) $
serializePostIndex $ blogPosts blog
doInstallAssets
printHandler :: GenEvent -> IO ()
printHandler (PostRender p cs) =
let cause Config = "config"
cause PostIndex = "post-index"
cause Template = "template"
cause PostModified = "modified"
cause Forced = "forced"
reasons = intercalate ", " (cause <$> cs)
in if cs == [PostModified] || Forced `elem` cs
then putStrLn $ "Rendering post: " ++ (show $ postBaseName p)
else putStrLn $ "Rendering post (" ++ reasons
++ "): " ++ (show $ postBaseName p)
printHandler Finished =
putStrLn "Done."
main :: IO ()
main = do
-- This is only to make it possible to run "mb" during the LaTeX
-- manual build process. I run "mb" to generate output for
-- demonstrations in the manual and output buffering was causing
-- output to not get sent to pdfLaTeX.
hSetBuffering stdout NoBuffering
conf <- startupConfigFromEnv
let dir = dataDirectory conf
newConf <- case initDataDirectory conf of
True -> do
initializeDataDir dir
return $ conf { forceRegeneration = True }
False -> return conf
canonicalConfig <- canonicalizeStartupConfig newConf
result <- runEitherT (mkBlog canonicalConfig)
blog <- case result of
Left e -> (putStrLn $ "Error: " ++ e) >> exitFailure
Right b -> return b
case listenAddr canonicalConfig of
Nothing -> doGeneration canonicalConfig blog printHandler
Just _ -> do
putStrLn $ "Starting up in listen mode..."
let conf' = canonicalConfig { forceRegeneration = False }
withServing conf' $ flip scanForChanges printHandler