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 +30/−0
- README +209/−0
- Setup.hs +2/−0
- mathblog.cabal +54/−0
- skel/html/images/newer-subdued.png binary
- skel/html/images/newer.png binary
- skel/html/images/older-subdued.png binary
- skel/html/images/older.png binary
- skel/html/stylesheets/stylesheet.css +110/−0
- skel/posts/first-post.txt +44/−0
- skel/templates/pagePostamble.html +6/−0
- skel/templates/pagePreamble.html +12/−0
- skel/templates/postPostamble.html +1/−0
- skel/templates/postPreamble.html +1/−0
- skel/templates/rssPostamble.xml +2/−0
- skel/templates/rssPreamble.xml +8/−0
- src/Main.hs +414/−0
+ 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 © 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