mathblog 0.4 → 0.5
raw patch · 9 files changed
+257/−175 lines, 9 filesdep ~HUnitdep ~SHAdep ~test-framework
Dependency ranges changed: HUnit, SHA, test-framework
Files
- README.md +27/−9
- mathblog.cabal +8/−8
- skel/assets/stylesheets/stylesheet.css +114/−0
- skel/html/stylesheets/stylesheet.css +0/−114
- src/MB/Files.hs +0/−3
- src/MB/Startup.hs +27/−0
- src/MB/Types.hs +2/−1
- src/MB/Util.hs +20/−1
- src/Main.hs +59/−39
README.md view
@@ -107,6 +107,7 @@ $ export MB_BASE_DIR=`pwd`/myBlog $ mb -i Blog directory: .../myBlog+Config file: .../myBlog/blog.cfg Setting up data directory using skeleton: /.../skel Configuration file changed; regenerating all content. Templates changed; regenerating accordingly.@@ -168,13 +169,17 @@ Serving the Blog ================ -To serve your blog files you can either make the "html" directory a+By default, `mb` will use `$(MB_BASE_DIR)/html` as its output+directory, but if you want the generated files to be installed+somewhere else, use the `-h` option.++To serve your blog files you can either make the output directory a document root for your web server or virtual host, or you can symlink-the `html` directory to the location of choice. The mathblog+the output directory to the location of choice. The mathblog directory itself is not intended to be in your web server's document tree because it contains many files that shouldn't be served to users. -Start up a web server pointing at the `html` directory and take a+Start up a web server pointing at the output directory and take a look. Now you might want to edit or create a new post, or even remove one.@@ -234,22 +239,27 @@ 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:+to your blog posts and other data files and regenerate them and copy+them to the output directory when it detects a change. Running+mathblog in listen mode is simple: ~~~ $ mb -l-Blog directory: /.../blog+Waiting for changes in .../blog ...+Blog directory: .../blog+Config file: .../blog/blog.cfg Post index changed; regenerating next/previous links. Done. Blog directory: /.../blog+Config file: .../blog/blog.cfg Configuration file changed; regenerating all content. Rendering 1 post(s)... Rendering first-post Done. Blog directory: /Users/cygnus/blog3+Config file: .../blog/blog.cfg Posts changed: first-post.txt Rendering 1 post(s)...@@ -287,9 +297,6 @@ This file makes up the structure of the post portion of the page, for pages which show posts (i.e., not the post index). -In addition, subdirectories of the `html/` directory contain things-you might want to customize, such as a CSS stylesheet.- The templates mentioned above are StringTemplate templates and are processed with the HStringTemplate library. The following template placeholders are supported in each template:@@ -327,6 +334,17 @@ - `$content$` The content of the page to be rendered.++Blog Assets+===========++The default blog directory created by `mb` also includes an `assets`+directory. Whenever `mb` detects changes in your blog post or asset+files (or if you run `mb -f`), everything in `assets/` will be copied+recursively into the output directory. This way, if you have custom+stylesheets or other files which need to be available, they can be+kept in your data directory and published with the rest of the+generated content. Function Graph Embedding ========================
mathblog.cabal view
@@ -1,5 +1,5 @@ Name: mathblog-Version: 0.4+Version: 0.5 Synopsis: A program for creating and managing a static weblog with LaTeX math and function graphs Description: This package provides a program for creating and@@ -11,8 +11,8 @@ of your choice, then this is the blogging platform for you! For detailed information and a full feature list, please see the README.-Author: Jonathan Daugherty <drcygnus@gmail.com>-Maintainer: Jonathan Daugherty <drcygnus@gmail.com>+Author: Jonathan Daugherty <cygnus@foobox.com>+Maintainer: Jonathan Daugherty <cygnus@foobox.com> Build-Type: Simple Cabal-version: >= 1.6 License: BSD3@@ -21,7 +21,7 @@ Data-Files: README.md- skel/html/stylesheets/stylesheet.css+ skel/assets/stylesheets/stylesheet.css skel/templates/postTemplate.html skel/templates/pageTemplate.html skel/templates/rssTemplate.xml@@ -44,7 +44,7 @@ process >= 1.0, time >= 1.1, old-locale >= 1.0,- SHA >= 1.4 && < 1.5,+ SHA >= 1.4 && < 1.6, bytestring >= 0.9 && < 1.0, HStringTemplate >= 0.6 && < 0.7, ConfigFile >= 1.1 && < 1.2@@ -79,12 +79,12 @@ process >= 1.0 && < 1.1, time >= 1.1, old-locale >= 1.0 && < 1.1,- SHA >= 1.4 && < 1.5,+ SHA >= 1.4 && < 1.6, bytestring >= 0.9 && < 1.0, HStringTemplate >= 0.6 && < 0.7, ConfigFile >= 1.1 && < 1.2,- HUnit >= 1.2.2 && < 1.2.3,- test-framework >= 0.3.3 && < 0.4,+ HUnit >= 1.2.2 && < 1.2.5,+ test-framework >= 0.3.3 && < 0.6, test-framework-hunit >= 0.2.6 && < 0.3 GHC-Options: -Wall
+ skel/assets/stylesheets/stylesheet.css view
@@ -0,0 +1,114 @@+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;+ margin-bottom: 1em;+}+div#prev-next-links {+ margin-top: 1em;+}+.prev-link {+ color: blue;+ text-decoration: none;+}+.prev-link-subdued {+ color: #aaa;+}+.next-link {+ float: right;+ color: blue;+}+.next-link-subdued {+ color: #aaa;+ float: right;+}+#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;+}+.eq-left {+ float: left;+ margin-right: 0.5em;+ margin-bottom: 0.5em;+}+.eq-right {+ float: right;+}+.eq-center {+ display: block;+ margin-left: auto;+ margin-right: auto;+}+.displaymath {+ display: block;+ margin-top: 1em !important;+ margin-bottom: 1em !important;+ margin-left: auto !important;+ margin-right: auto !important;+}
− skel/html/stylesheets/stylesheet.css
@@ -1,114 +0,0 @@-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;- margin-bottom: 1em;-}-div#prev-next-links {- margin-top: 1em;-}-.prev-link {- color: blue;- text-decoration: none;-}-.prev-link-subdued {- color: #aaa;-}-.next-link {- float: right;- color: blue;-}-.next-link-subdued {- color: #aaa;- float: right;-}-#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;-}-.eq-left {- float: left;- margin-right: 0.5em;- margin-bottom: 0.5em;-}-.eq-right {- float: right;-}-.eq-center {- display: block;- margin-left: auto;- margin-right: auto;-}-.displaymath {- display: block;- margin-top: 1em !important;- margin-bottom: 1em !important;- margin-left: auto !important;- margin-right: auto !important;-}
src/MB/Files.hs view
@@ -54,6 +54,3 @@ postFinalHtml :: Blog -> Post -> FilePath postFinalHtml config p = postHtmlDir config </> postBaseName p ++ ".html"--stylesheet :: Blog -> FilePath-stylesheet c = stylesheetDir c </> "stylesheet.css"
src/MB/Startup.hs view
@@ -22,12 +22,15 @@ ( getEnvironment , getArgs )+import System.FilePath import System.Console.GetOpt data StartupConfig = StartupConfig { listenMode :: Bool , dataDirectory :: FilePath , initDataDirectory :: Bool , forceRegeneration :: Bool+ , htmlOutputDirectory :: FilePath+ , configFilePath :: FilePath } deriving (Show, Eq) @@ -35,6 +38,8 @@ | DataDir FilePath | InitDataDirectory | ForceRegenerate+ | HtmlOutputDir FilePath+ | ConfigFile FilePath deriving (Eq) getDataDirFlag :: [Flag] -> Maybe FilePath@@ -42,6 +47,16 @@ getDataDirFlag (DataDir p:_) = Just p getDataDirFlag (_:fs) = getDataDirFlag fs +getHtmlOutputDirFlag :: [Flag] -> Maybe FilePath+getHtmlOutputDirFlag [] = Nothing+getHtmlOutputDirFlag (HtmlOutputDir p:_) = Just p+getHtmlOutputDirFlag (_:fs) = getHtmlOutputDirFlag fs++getConfigFilePath :: [Flag] -> Maybe FilePath+getConfigFilePath [] = Nothing+getConfigFilePath (ConfigFile p:_) = Just p+getConfigFilePath (_:fs) = getConfigFilePath fs+ options :: [OptDescr Flag] options = [ Option ['d'] [baseDirParamName] (ReqArg DataDir "PATH") $ "Path where blog files will be stored. If this is\n" ++@@ -57,6 +72,14 @@ "to view your posts as you're writing them." , Option ['f'] ["force"] (NoArg ForceRegenerate) "Force a rebuild of all blog content."+ , Option ['h'] ["html-dir"] (ReqArg HtmlOutputDir "PATH")+ $ "Write generated HTML and images to the specified\n" +++ "directory. The default is the 'html' directory in\n" +++ "the blog data directory."+ , Option ['c'] ["config-file"] (ReqArg ConfigFile "FILENAME")+ $ "Use the specified config file instead of 'blog.cfg'\n" +++ "in the data directory. This path must be relative\n" +++ "to the data directory." ] -- |Inspect the program environment to create a startup configuration.@@ -91,11 +114,15 @@ i = InitDataDirectory `elem` flags f = ForceRegenerate `elem` flags d <- getDataDirFlag flags `mplus` (lookup baseDirEnvName env)+ h <- getHtmlOutputDirFlag flags `mplus` (Just $ d </> "html")+ c <- getConfigFilePath flags `mplus` (Just $ d </> "blog.cfg") return $ StartupConfig { listenMode = lm , dataDirectory = d , initDataDirectory = i , forceRegeneration = f+ , htmlOutputDirectory = h+ , configFilePath = c } baseDirEnvName :: String
src/MB/Types.hs view
@@ -21,7 +21,7 @@ data Blog = Blog { baseDir :: FilePath , postSourceDir :: FilePath , htmlDir :: FilePath- , stylesheetDir :: FilePath+ , assetDir :: FilePath , postHtmlDir :: FilePath , postIntermediateDir :: FilePath , imageDir :: FilePath@@ -69,5 +69,6 @@ , configChanged :: Bool , templatesChanged :: Bool , postIndexChanged :: Bool+ , assetsChanged :: Bool } deriving (Show)
src/MB/Util.hs view
@@ -96,6 +96,7 @@ exitFailure when (not dstDExists) $ createDirectory dstPath+ copyTree' srcPath dstPath where@@ -114,7 +115,8 @@ let dstDir = dst </> dirName dirName = takeFileName dir - createDirectory dstDir+ e <- doesDirectoryExist dstDir+ when (not e) $ createDirectory dstDir copyTree' (src </> dirName) dstDir toUtcTime :: EpochTime -> UTCTime@@ -150,6 +152,15 @@ , not $ "." `isPrefixOf` f ] +dirFilenamesRecursive :: FilePath -> IO [FilePath]+dirFilenamesRecursive dir = do+ entries <- filter (not . flip elem [".", ".."]) <$> getDirectoryContents dir+ dirs <- filterM doesDirectoryExist $ map (dir </>) entries+ files <- filterM doesFileExist $ map (dir </>) entries++ rest <- concat <$> forM dirs dirFilenamesRecursive+ return $ files ++ rest+ allPostFilenames :: FilePath -> IO [FilePath] allPostFilenames postSrcDir = do allFiles <- dirFilenames postSrcDir@@ -211,6 +222,7 @@ , not . null . postsChanged , templatesChanged , postIndexChanged+ , assetsChanged ] summarizeChanges :: Blog -> Bool -> IO ChangeSummary@@ -243,8 +255,15 @@ mtime <- getModificationTime f return $ mtime > baseTime + -- Determine whether any assets changed+ assetFiles <- dirFilenamesRecursive (assetDir config)+ assetChanges <- forM assetFiles $ \f -> do+ mtime <- getModificationTime f+ return $ mtime > baseTime+ return $ ChangeSummary { configChanged = configChanged' || forceAll , postsChanged = map postFilename modifiedPosts , templatesChanged = or (forceAll : templateChanges) , postIndexChanged = postIndexChanged' || forceAll+ , assetsChanged = or (forceAll : assetChanges) }
src/Main.hs view
@@ -8,9 +8,6 @@ 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@@ -34,7 +31,8 @@ ( getDataFileName ) import MB.Startup- ( dataDirectory+ ( StartupConfig(..)+ , dataDirectory , listenMode , startupConfigFromEnv , forceRegeneration@@ -45,12 +43,12 @@ import MB.Mathjax import MB.Gladtex +defaultConfigFilename :: String+defaultConfigFilename = "blog.cfg"+ skelDir :: IO FilePath skelDir = getDataFileName "skel" -configFilename :: String-configFilename = "blog.cfg"- commonTemplateAttrs :: Blog -> [(String, String)] commonTemplateAttrs blog = [ ( "baseUrl", baseUrl blog )@@ -167,16 +165,13 @@ buildPost h blog p (prevPost, nextPost) hClose h -linkIndexPage :: Blog -> IO ()-linkIndexPage blog = do- let dest = Files.postFinalHtml blog post+buildIndexPage :: Blog -> IO ()+buildIndexPage blog = do+ let src = Files.postFinalHtml blog post index = Files.indexHtml blog post = head $ blogPosts blog - exists <- doesFileExist index- when exists $ removeFile index-- createSymbolicLink dest index+ copyFile src index postModificationString :: Post -> IO String postModificationString p = do@@ -239,7 +234,7 @@ initializeDataDir :: FilePath -> IO () initializeDataDir dir = do existsBase <- doesDirectoryExist dir- existsConfig <- doesFileExist $ dir </> configFilename+ existsConfig <- doesFileExist $ dir </> defaultConfigFilename dataDir <- skelDir @@ -250,13 +245,13 @@ when existsConfig $ putStrLn $ "Data directory already initialized; found " ++- (dir </> configFilename)+ (dir </> defaultConfigFilename) ensureDirs :: Blog -> IO () ensureDirs blog = do let dirs = [ postSourceDir , htmlDir- , stylesheetDir+ , assetDir , postHtmlDir , postIntermediateDir , imageDir@@ -270,12 +265,12 @@ exists <- doesDirectoryExist d when (not exists) $ createDirectory d -scanForChanges :: FilePath -> (FilePath -> IO Bool) -> IO ()-scanForChanges dir act = do+scanForChanges :: IO Bool -> IO ()+scanForChanges act = do scan where scan = do- didWork <- act dir+ didWork <- act when didWork $ putStrLn "" threadDelay $ 1 * 1000 * 1000 scan@@ -292,13 +287,14 @@ , ("tikz", tikzProcessor) ] -mkBlog :: FilePath -> IO Blog-mkBlog base = do- let configFilePath = base </> configFilename- e <- doesFileExist configFilePath+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 " ++ configFilePath ++ " not found"+ putStrLn $ "Configuration file " ++ configFile ++ " not found" exitFailure let requiredValues = [ "baseUrl"@@ -307,7 +303,7 @@ , "authorEmail" ] - cfg <- Config.readConfig configFilePath requiredValues+ cfg <- Config.readConfig configFile requiredValues let Just cfg_baseUrl = lookup "baseUrl" cfg Just cfg_title = lookup "title" cfg@@ -338,13 +334,14 @@ procs = [eqBackend, mathBackend] - let b = Blog { baseDir = base+ let html = htmlOutputDirectory conf+ b = Blog { baseDir = base , postSourceDir = postSrcDir- , htmlDir = base </> "html"- , stylesheetDir = base </> "html" </> "stylesheets"- , postHtmlDir = base </> "html" </> "posts"+ , htmlDir = html+ , assetDir = base </> "assets"+ , postHtmlDir = html </> "posts" , postIntermediateDir = base </> "generated"- , imageDir = base </> "html" </> "generated-images"+ , imageDir = html </> "generated-images" , templateDir = base </> "templates" , htmlTempDir = base </> "tmp" , baseUrl = cfg_baseUrl@@ -352,7 +349,7 @@ , authorName = cfg_authorName , authorEmail = cfg_authorEmail , eqPreamblesDir = base </> "eq-preambles"- , configPath = configFilePath+ , configPath = configFile , blogPosts = allPosts , processors = procs }@@ -360,15 +357,30 @@ ensureDirs b return b -regenerateContent :: Bool -> FilePath -> IO Bool-regenerateContent force dir = do- blog <- mkBlog dir- summary <- summarizeChanges blog force+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."@@ -381,9 +393,14 @@ when (postIndexChanged summary) $ putStrLn "Post index changed; regenerating next/previous links." + when (assetsChanged summary) $+ do+ putStrLn "Assets changed; reinstalling."+ installAssets blog+ generatePosts blog summary - linkIndexPage blog+ buildIndexPage blog generatePostList blog generateRssFeed blog @@ -402,6 +419,9 @@ case listenMode conf of False -> do- didWork <- regenerateContent (forceRegeneration conf) dir+ didWork <- regenerateContent conf when (not didWork) $ putStrLn "No changes found!"- True -> scanForChanges dir (regenerateContent False)+ True -> do+ putStrLn $ "Waiting for changes in " ++ (dataDirectory conf) ++ " ..."+ scanForChanges+ (regenerateContent $ conf { forceRegeneration = False })