packages feed

mathblog (empty) → 0.1

raw patch · 17 files changed

+893/−0 lines, 17 filesdep +basedep +directorydep +filepathsetup-changedbinary-added

Dependencies added: base, directory, filepath, old-locale, pandoc, process, time, unix

Files

+ LICENSE view
@@ -0,0 +1,30 @@+Copyright (c) 2010, Jonathan Daugherty.+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are+met:++    * Redistributions of source code must retain the above copyright+      notice, this list of conditions and the following disclaimer.++    * Redistributions in binary form must reproduce the above+      copyright notice, this list of conditions and the following+      disclaimer in the documentation and/or other materials provided+      with the distribution.++    * The names of the contributors may not be used to endorse or+      promote products derived from this software without specific+      prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT+HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ README view
@@ -0,0 +1,209 @@++mathblog+--------++mathblog is a Haskell program targeted at people who want to write+statically-generated, mathematically-themed weblogs.++Project vision+--------------++I wrote mathblog with a very specific set of requirements in mind,+motivated by the following principles:++ - a blog should be easy to create, host, and update.++ - a blog should be easy to maintain.++ - I should be able to edit posts in my editor of choice and write+   them in an intelligent textual markup language.++ - It should be easy to embed high-quality mathematical symbols and+   equations in the blog posts.++As a result, mathblog has the following properties:++ - the software is composed of a single executable which will+   automatically take care of creating your blog and regenerating+   pages when your post markup changes.++ - all content is stored in plain text files and is generated+   statically.  No database, web framework, or templating system is+   used.++ - a mathblog can be hosted with a simple static fileserver such as+   thttpd, lighttpd, or apache.++ - blog posts are written in the Markdown format with extensions, as+   supported by the Pandoc document converter.++ - Math is embedded with $...$ and $$...$$.++These properties have some nice advantages; your blog content is+cacheable and can be subjected to revision control.  Posts are easy to+edit and editing doesn't require a web browser.  The static file+representation model means you can compose a blog post on your laptop+and get it just right using a local installation of mathblog, then+push it up to your server to post it to your public blog.++Dependencies+------------++mathblog takes advantage of three primary software components:++ - pandoc, which will be installed for you automatically if you use+   "cabal" to install mathblog.  For best results, use the revision+   control version of Pandoc.++ - gladTeX, a package of tools for rendering LaTeX equations.  At+   present you MUST get gladTeX from revision control, since mathblog+   requires features not yet released.++ - a TeX distribution, such as TeX-live++How to use+----------++mathblog doesn't have any persistent configuration file; the way you+configure it is by setting two environment variables:++  MB_BASE_DIR++    The location of your blog files on the filesystem.  Must be an+    absolute path.++  MB_BASE_URL++    The base URL of your blog as it will be hosted.  This is used to+    generate fully-qualified URLs.  A trailing slash is not expected.+    E.g., "http://localhost:8080".++Once you've set those environment variables, just run "mb".  It will+take care of setting up a new blog data directory for you, complete+with some default pages and a default first blog post:++  $ export MB_BASE_URL="http://localhost:8080"+  $ export MB_BASE_DIR=`pwd`/myBlog+  $ mb+  mb: using base directory "/home/.../myBlog"+  mb: using base url "http://localhost:8080"+  Setting up data directory using skeleton: /home/.../.cabal/share/mathblog-0.1/skel+  Processing: first-post+  Generating all-posts list.+  Done.++Now you have a blog; fire up a web server pointing at myBlog/html and+you'll see a demo post.++Next, you might want to edit or create a new post, or even remove one.+Just edit the appropriate file in myBlog/posts/ accordingly and re-run+mb.  Here's an example of adding a new post:++  $ mb+  mb: using base directory "/home/.../blog/myBlog"+  mb: using base url "http://localhost:8080"+  Processing: second-post+  Generating all-posts list.+  Done.++Post format+-----------++Posts are formatted in Markdown and support the extended Markdown+syntax as implemented by Pandoc.  The only important convention to+note is that the post title goes on the first line of the file+following a percent sign, as follows:++  %My First Post++  First paragraph starts here.++Running mb in "listen" mode+---------------------------++Ordinarily, you'll just run "mb" once in a while to update your+content.  But if you're in the middle of authoring a new post and you+want to see what it looks like as you edit it rather than run "mb" by+hand periodically, you can run "mb" in "listen" mode; this means that+mb will run forever, peroidically scanning the filesystem for changes+to your blog posts and other data files and regenerate them when it+detects a change.  Running mathblog in listen mode is simple:++  $ mb -l+  mb: using base directory "/home/.../myBlog"+  mb: using base url "http://localhost:8080"++  Changes detected:+    /home/.../myBlog/posts/second-post.txt+  Processing: second-post+  Generating all-posts list.+  Done.++Above I ran "mb" in listen mode and then modified one of the posts,+second-post.txt, and "mb" detected the change and ran its typical+regeneration routine.++Customizing your blog+---------------------++It's likely that you'll want to customize the look and feel of your+blog.  To this end, mathblog generates the pages of your blog by+assembling various pieces of the page to create the final result.  The+biggest piece of a generated page is the blog post itself, but the+surrounding elements are read from various files that are created by+mathblog when it creates your blog data directory.  These files are+stored in the "templates/" subdirectory of your blog data directory+and are as follows:++  templates/rssPreamble.xml+  templates/rssPostamble.xml++    These files make up the leading and trailing RSS XML that will be+    used to wrap the individual elements of your blog's RSS feed.+    You'll probably want to change the rssPreamble.xml file, since it+    contains information about the blog author.++  templates/pagePreamble.html+  templates/pagePostamble.html++    These files make up the outermost HTML content of the blog post+    pages, including the index page and the post list page.++  templates/postPreamble.html+  templates/postPostamble.html++    These files make up the HTML content surrounding the post body+    itself.++In addition, subdirectories of the "html/" directory contain things+you might want to customize, such as a CSS stylesheet.++Other features+--------------++Since mathblog doesn't provide many moving parts, it's up to you to+outsource various web site features, such as comments.  I've+successfully integrated mathblog with the Disqus comments service.  To+do this, some javascript needs to be embedded in the blog pages.+Disqus works best when you supply it with a page identifier so it can+guarantee that comments are post-specific rather than URL-specific.+The way mathblog makes this possible is by exposing a JavaScript+variable to other scripts in your page:++  Blog.pageName = "foobar-baz";++This variable name can be used by scripts you embed, such as with+Disqus comment forms:++  var disqus_identifier = Blog.pageName;++Afterword+---------++I personally use this software package but I'll be pleased if others+find it useful.  In addition, I'm open to accepting contributions on+the project if they're consistent with what I've outlined above.+Happy blogging!++Jonathan Daugherty+drcygnus@gmail.com
+ Setup.hs view
@@ -0,0 +1,2 @@+import Distribution.Simple+main = defaultMain
+ mathblog.cabal view
@@ -0,0 +1,54 @@+Name:                mathblog+Version:             0.1+Synopsis:            A program for creating and managing a+                     static, mathematically-inclined weblog+Description:         This package provides a program for creating and+                     managing a statically-generated, VCS-friendly,+                     mathematically-inclined weblog.  If you're+                     interested in managing a blog with few moving+                     parts and support for embedded LaTeX math and the+                     UNIX editor of your choice, then this is the+                     blogging platform for you!  For detailed+                     information, please see the README.+Author:              Jonathan Daugherty <drcygnus@gmail.com>+Maintainer:          Jonathan Daugherty <drcygnus@gmail.com>+Build-Type:          Simple+Cabal-version:       >= 1.2+License:             BSD3+License-file:        LICENSE+Category:            Web+Homepage:            http://repos.codevine.org/mathblog/++Data-Files:+  README+  skel/html/images/older.png+  skel/html/images/newer-subdued.png+  skel/html/images/newer.png+  skel/html/images/older-subdued.png+  skel/html/stylesheets/stylesheet.css+  skel/templates/postPostamble.html+  skel/templates/postPreamble.html+  skel/templates/pagePreamble.html+  skel/templates/pagePostamble.html+  skel/templates/rssPreamble.xml+  skel/templates/rssPostamble.xml+  skel/posts/first-post.txt++Executable mb+  Build-depends:+    base >= 3 && < 5,+    directory >= 1.0 && < 1.1,+    filepath >= 1.1 && < 1.2,+    pandoc >= 1.6 && < 1.7,+    unix >= 2.3 && < 2.5,+    process >= 1.0 && < 1.1,+    time >= 1.1 && < 1.2,+    old-locale >= 1.0 && < 1.1++  Other-modules:+    Paths_mathblog++  GHC-Options: -Wall++  Hs-Source-Dirs:  src+  Main-is:         Main.hs
+ skel/html/images/newer-subdued.png view

binary file changed (absent → 1198 bytes)

+ skel/html/images/newer.png view

binary file changed (absent → 1198 bytes)

+ skel/html/images/older-subdued.png view

binary file changed (absent → 1190 bytes)

+ skel/html/images/older.png view

binary file changed (absent → 1190 bytes)

+ skel/html/stylesheets/stylesheet.css view
@@ -0,0 +1,110 @@+html {+  background: #aaa;+}+img {+  border: none;+}+body {+  font-size: 16px;+  margin-left: 3em;+  margin-right: 3em;+  background: #fff;+  margin-top: 0px;+  min-height: 900px;+}+#page {+  padding-top: 1em;+  padding-left: 1em;+  padding-right: 1em;+  margin-left: auto;+  margin-right: auto;+  max-width: 700px;+}+#header {+  font-size: 26px;+}+h1 {+  font-size: 32px;+  font-weight: normal;+}+h3 {+  font-weight: normal;+}+#header a {+  color: #00f;+  text-decoration: none;+}+a#listing {+  float: right;+  font-size: 14px;+  margin-top: 0.25em;+}+div#all-posts {+  margin-top: 1.5em;+}+div.listing-entry {+  margin-top: 0.5em;+  padding-bottom: 0.5em;+}+span.post-title {+  font-size: 20px;+  display: block;+}+span.post-title a {+  text-decoration: none;+  color: blue;+}+span.post-created {+  font-size: 15px;+  display: block;+  margin-left: 1em;+  color: #555;+}+div#prev-next-links {+  margin-top: 1em;+}+.prev-link {+  color: blue;+  text-decoration: none;+  background-image: url('/images/newer.png');+  background-repeat: no-repeat;+  background-position: 0px 5px;+  padding-left: 30px;+}+.prev-link-subdued {+  color: #aaa;+  background-image: url('/images/newer-subdued.png');+  background-repeat: no-repeat;+  background-position: 0px 5px;+  padding-left: 30px;+}+.next-link {+  float: right;+  color: blue;+  text-decoration: none;+  background-image: url('/images/older.png');+  background-repeat: no-repeat;+  background-position: right 5px;+  padding-right: 25px;+}+.next-link-subdued {+  color: #aaa;+  float: right;+  background-image: url('/images/older-subdued.png');+  background-repeat: no-repeat;+  background-position: right 5px;+  padding-right: 25px;+}+#footer {+  border-top: 1px solid #ddd;+  color: #555;+  margin-top: 1em;+  padding-top: 0.5em;+  font-size: smaller;+}+#post h1 {+  margin-bottom: 0px;+}+#post span.post-created {+  color: #999;+}
+ skel/posts/first-post.txt view
@@ -0,0 +1,44 @@+%A first post $p = 0$++You've successfully set up your shiny new math blog!++This initial post is here to show how the software works.  Posts are+written in the Markdown format using <a+href="http://johnmacfarlane.net/pandoc/">Pandoc</a>.  They employ an+extended Markdown syntax as described in the <a+href="http://johnmacfarlane.net/pandoc/README.html#pandocs-markdown-vs.standard-markdown">Pandoc+User Guide</a>.  You can embed LaTeX math in your posts; it will be+processed with <a+href="http://ans.hsh.no/home/mgg/gladtex/">gladTeX</a>.++Inline math is introduced with `$...$` and looks like $x \in Y$.  Math+can be embedded in post titles, as well!  To insert a block-level+equation, use `$$...$$` as a block-level Markdown element.  This+causes the math to be typeset in the `displaymath` environment.  The+results look like this:++$$\int^n_0{\ldots} = \sum^n_{i=1}{b_0 G(u^s)}+\Longrightarrow \frac{\tan x}{\lim_{h \to \infty}{\Phi}}$$++as opposed to this:++$\int^n_0{\ldots} = \sum^n_{i=1}{b_0 G(u^s)}+\Longrightarrow \frac{\tan x}{\lim_{h \to \infty}{\Phi}}$++The content of this post can be found in the `posts` subdirectory of+your blog data directory; the rest of the content of this page can be+found in "preamble" and "postamble" files in the `templates`+subdirectory.  You can modify those files and re-run `mb` to+regenerate all of your blog content.  Note that some defaults, such as+the blog title and footer text, are probably bogus and need to be+changed.++Note that `mb` will not bother to regenerate the actual content of+your blog posts unless the corresponding source file in `posts` has+changed.++Once you have the hang of creating posts, you'll probably want to+delete this one; just delete it from the `posts` subdirectory and run+`mb`.++Enjoy!
+ skel/templates/pagePostamble.html view
@@ -0,0 +1,6 @@+      <div id="footer">+        Copyright &copy; 2010 Your Name Here+      </div>+    </div>+  </body>+</html>
+ skel/templates/pagePreamble.html view
@@ -0,0 +1,12 @@+<html>+  <head>+    <title>A Blog</title>+    <link rel="stylesheet" type="text/css" href="/stylesheets/stylesheet.css"/>+    <link rel="alternate" type="application/rss+xml" href="/feed.xml"/>+  </head>+  <body>+    <div id="page">+      <div id="header">+        <a id="listing" href="/posts/">all posts</a>+        <a href="/">A Blog</a>+      </div>
+ skel/templates/postPostamble.html view
@@ -0,0 +1,1 @@+</div>
+ skel/templates/postPreamble.html view
@@ -0,0 +1,1 @@+<div id="post">
+ skel/templates/rssPostamble.xml view
@@ -0,0 +1,2 @@+</channel>+</rss>
+ skel/templates/rssPreamble.xml view
@@ -0,0 +1,8 @@+<rss version="2.0">+  <channel>+    <title>A Blog</title>+    <link>http://localhost:8080/</link>+    <description>A Blog.</description>+    <language>en-us</language>+    <managingEditor>your@email.com</managingEditor>+    <webMaster>your@email.com</webMaster>
+ src/Main.hs view
@@ -0,0 +1,414 @@+module Main where++import Control.Applicative+    ( (<*>)+    , pure+    )+import Control.Monad+    ( when+    , forM_+    )+import Control.Concurrent+    ( threadDelay+    )+import System.IO+    ( IOMode(WriteMode)+    , Handle+    , openFile+    , hPutStr+    , hClose+    )+import System.Exit+    ( exitFailure+    )+import System.Environment+    ( getEnvironment+    , getArgs+    )+import System.Directory+    ( doesDirectoryExist+    , doesFileExist+    , getDirectoryContents+    , removeFile+    , copyFile+    , createDirectory+    )+import System.FilePath+    ( (</>)+    )+import System.Posix.Files+    ( getFileStatus+    , modificationTime+    , createSymbolicLink+    )+import Data.List+    ( sortBy+    , isSuffixOf+    )+import Data.Maybe+    ( isNothing+    )+import Data.Time.LocalTime+    ( TimeZone(timeZoneName)+    , getCurrentTimeZone+    )+import Data.Time.Clock+    ( UTCTime+    , getCurrentTime+    )+import qualified Text.Pandoc as Pandoc+import MB.Util+    ( copyTree+    , toUtcTime+    , toLocalTime+    , pandocTitle+    , pandocTitleRaw+    , rssModificationTime+    )+import MB.Gladtex+    ( gladTex+    , checkForGladtex+    )+import MB.Types+import qualified MB.Files as Files+import Paths_mathblog+    ( getDataFileName+    )++skelDir :: IO FilePath+skelDir = getDataFileName "skel"++baseDirEnvName :: String+baseDirEnvName = "MB_BASE_DIR"++baseUrlEnvName :: String+baseUrlEnvName = "MB_BASE_URL"++allPostFilenames :: Config -> IO [FilePath]+allPostFilenames config = do+  allFiles <- getDirectoryContents $ postSourceDir config+  return [ postSourceDir config </> f | f <- allFiles+         , ".txt" `isSuffixOf` f+         ]++getModificationTime :: FilePath -> IO UTCTime+getModificationTime fullPath = do+  info <- getFileStatus fullPath+  return $ toUtcTime $ modificationTime info++loadPost :: FilePath -> IO Post+loadPost fullPath = do+  fileContent <- readFile fullPath+  t <- getModificationTime fullPath+  let doc = Pandoc.readMarkdown Pandoc.defaultParserState fileContent++  return $ Post { postTitle = pandocTitle doc+                , postTitleRaw = pandocTitleRaw doc+                , postFilename = fullPath+                , postModificationTime = t+                , postAst = doc+                }++allPosts :: Config -> IO [Post]+allPosts config = do+  postFiles <- allPostFilenames config++  -- For each file, construct a Post from it.+  posts <- mapM loadPost postFiles++  -- Return posts sorted by modification time, descending+  return $ sortBy (\a b -> postModificationTime b `compare`+                           postModificationTime a) posts++pandocWriterOptions :: Pandoc.WriterOptions+pandocWriterOptions =+    Pandoc.defaultWriterOptions { Pandoc.writerHTMLMathMethod = Pandoc.GladTeX+                                }++writePost :: Handle -> Post -> IO ()+writePost h post = do+  created <- postModificationString post+  hPutStr h $ "<h1>" ++ postTitle post 175 ++ "</h1>"+  hPutStr h $ "<span class=\"post-created\">Posted " ++ created ++ "</span>"+  hPutStr h $ Pandoc.writeHtmlString pandocWriterOptions (postAst post)++buildLinks :: Maybe Post -> Maybe Post -> String+buildLinks prev next =+    "<div id=\"prev-next-links\">"+      ++ link "next-link" "older" next+      ++ link "prev-link" "newer" prev+      ++ "</div>"+        where+          link cls name Nothing =+              "<span class=\"" ++ cls ++ "-subdued\">" ++ name ++ "</span>"+          link cls name (Just p) =+              "<a class=\"" ++ cls ++ "\" href=\"" ++ Files.postUrl p +++                                "\">" ++ name ++ "</a>"++jsInfo :: Post -> String+jsInfo post =+    "<script type=\"text/javascript\">\n" +++    "Blog = {\n" +++    "  pageName: " ++ show (Files.postBaseName post) +++    "\n" +++    "};\n" +++    "</script>\n"++buildPost :: Handle -> Config -> Post -> (Maybe Post, Maybe Post) -> IO ()+buildPost h config post prevNext = do+  hPutStr h =<< (readFile $ Files.pagePreamble config)+  hPutStr h $ jsInfo post+  hPutStr h $ uncurry buildLinks prevNext+  hPutStr h =<< (readFile $ Files.postPreamble config)+  hPutStr h =<< (readFile $ Files.postIntermediateHtml config post)+  hPutStr h =<< (readFile $ Files.postPostamble config)+  hPutStr h =<< (readFile $ Files.pagePostamble config)++generatePost :: Config -> Post -> IO ()+generatePost config post = do+  let tempHtml = htmlTempDir config </> Files.postBaseName post ++ ".html"+      finalHtml = Files.postIntermediateHtml config post++  htmlExists <- doesFileExist finalHtml+  skip <- case htmlExists of+            False -> return False+            True -> do+              info <- getFileStatus finalHtml+              return $ (toUtcTime $ modificationTime info) > postModificationTime post++  when (not skip) $ do+    putStrLn $ "Processing: " ++ Files.postBaseName post++    h <- openFile (Files.postHtex config post) WriteMode+    writePost h post+    hClose h++    -- Run gladtex on the temp file to generate the final file.+    gladTex config (Files.postHtex config post) "000000"++    -- Gladtex generates the HTML in the same directory as the source+    -- file, so we need to copy that to the final location.+    copyFile tempHtml finalHtml++    -- Remove the temporary file.+    removeFile $ Files.postHtex config post+    removeFile tempHtml++generatePosts :: Config -> [Post] -> IO ()+generatePosts config posts = do+  let n = length posts+  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 config p+        h <- openFile (Files.postFinalHtml config p) WriteMode+        buildPost h config p (prevPost, nextPost)+        hClose h++generateIndex :: Config -> Post -> IO ()+generateIndex config post = do+  let dest = Files.postFinalHtml config post+      index = Files.indexHtml config++  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++generateList :: Config -> [Post] -> IO ()+generateList config posts = do+  putStrLn "Generating all-posts list."++  h <- openFile (Files.listHtex config) WriteMode++  hPutStr h =<< (readFile $ Files.pagePreamble config)+  hPutStr h "<div id=\"all-posts\">"++  -- 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.+  forM_ posts $ \p -> do+    created <- postModificationString p+    hPutStr h $ concat [ "<div class=\"listing-entry\"><span class=\"post-title\">"+                       , "<a href=\"" ++ Files.postUrl p ++ "\">"+                       , postTitle p 110+                       , "</a></span><span class=\"post-created\">Posted "+                       , created+                       , "</span></div>\n"+                       ]++  hPutStr h "</div>"+  hPutStr h =<< (readFile $ Files.pagePostamble config)+  hClose h++  gladTex config (Files.listHtex config) "0000FF"++  -- Gladtex generates the HTML in the same directory as the source+  -- file, so we need to copy that to the final location.+  copyFile (Files.listTmpHtml config) (Files.listHtml config)++  -- Remove the temporary file.+  removeFile $ Files.listHtex config+  removeFile $ Files.listTmpHtml config++fullPostUrl :: Config -> Post -> String+fullPostUrl config p = baseUrl config ++ Files.postUrl p++rssItem :: Config -> Post -> String+rssItem config p =+    concat [ "<item>"+           , "<title>" ++ postTitleRaw p ++ "</title>\n"+           , "<link>" ++ fullPostUrl config p ++ "</link>\n"+           , "<pubDate>" ++ rssModificationTime p ++ "</pubDate>\n"+           , "<guid>" ++ fullPostUrl config p ++ "</guid>\n"+           , "</item>\n"+           ]++generateRssFeed :: Config -> [Post] -> IO ()+generateRssFeed config posts = do+  h <- openFile (Files.rssXml config) WriteMode+  hPutStr h =<< (readFile $ Files.rssPreamble config)++  forM_ posts (hPutStr h . rssItem config)++  hPutStr h =<< (readFile $ Files.rssPostamble config)+  hClose h++setup :: Config -> IO ()+setup config = do+  exists <- doesDirectoryExist $ baseDir config+  dataDir <- skelDir++  when (not exists) $ do+          putStrLn $ "Setting up data directory using skeleton: " ++ dataDir+          copyTree dataDir $ baseDir config++  ensureDirs config++ensureDirs :: Config -> IO ()+ensureDirs config = do+  let dirs = [ postSourceDir+             , htmlDir+             , stylesheetDir+             , postHtmlDir+             , postIntermediateDir+             , imageDir+             , templateDir+             , htmlTempDir+             ]++  forM_ (dirs <*> pure config) $ \d ->+      do+        exists <- doesDirectoryExist d+        when (not exists) $ createDirectory d++-- The files we look at to decide whether to regenerate the blog.+-- We'll always look at the post input files, but we also want to look+-- at other files to trigger a regeneration.+changedFiles :: Config -> [FilePath]+changedFiles config = [ Files.rssPreamble+                      , Files.rssPostamble+                      , Files.pagePreamble+                      , Files.pagePostamble+                      , Files.postPreamble+                      , Files.postPostamble+                      ] <*> pure config++preserveM :: (Monad m) => (a -> m b) -> a -> m (a, b)+preserveM act val = act val >>= \r -> return (val, r)++scanForChanges :: Config -> IO () -> IO ()+scanForChanges config act = do+  t <- getCurrentTime+  scan t+      where+        scan t = do+          posts <- allPostFilenames config+          let filesToInspect = posts ++ changedFiles config+          allTimes <- mapM (preserveM getModificationTime) filesToInspect++          let modified = filter ((> t) . snd) allTimes+              nextTime = if null modified+                         then t+                         else maximum $ map snd modified++          when (not $ null modified) $+               do+                 putStrLn ""+                 putStrLn "Changes detected:"+                 forM_ modified $ \(fp, _) -> do+                              putStrLn $ "  " ++ fp+                 act+          threadDelay $ 1 * 1000 * 1000+          scan nextTime++mkConfig :: FilePath -> String -> Config+mkConfig base url = Config { baseDir = base+                           , postSourceDir = base </> "posts"+                           , 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 = url+                           }++usage :: IO ()+usage = do+  putStrLn "Usage: mb [-l]\n"+  putStrLn "mb is a tool for creating and managing a mathematically-inclined"+  putStrLn "weblog.  To use mb, you must set a few environment variables:"+  putStrLn ""+  putStrLn $ "  " ++ baseDirEnvName ++ ": path where blog files will be stored"+  putStrLn $ "  " ++ baseUrlEnvName ++ ": base URL where blog will be hosted"+  putStrLn ""+  putStrLn " -l: make mb poll periodically and regenerate your blog content"+  putStrLn "     when something changes.  This is useful if you want to run a"+  putStrLn "     local web server to view your posts as you're writing them."++main :: IO ()+main = do+  env <- getEnvironment+  args <- getArgs++  checkForGladtex++  let mBase = lookup baseDirEnvName env+      mBaseUrl = lookup baseUrlEnvName env++  when (isNothing (mBase >> mBaseUrl)) $ usage >> exitFailure++  let Just dir = mBase+      Just url = mBaseUrl++  when (head dir /= '/') $ do+         putStrLn $ baseDirEnvName ++ " must contain an absolute path"+         exitFailure++  putStrLn $ "mb: using base directory " ++ (show dir)+  putStrLn $ "mb: using base url " ++ (show url)+  let config = mkConfig dir url+  setup config++  let work = do+         posts <- allPosts config+         generatePosts config posts+         generateIndex config $ head posts+         generateList config posts+         generateRssFeed config posts+         putStrLn "Done."++  case args of+    [] -> work+    ["-l"] -> scanForChanges config work+    _ -> usage >> exitFailure