packages feed

mathblog 0.2 → 0.3

raw patch · 23 files changed

+1324/−803 lines, 23 filesdep +HUnitdep +pandoc-typesdep +test-frameworkdep ~ConfigFiledep ~directorydep ~filepathnew-component:exe:mb-tests

Dependencies added: HUnit, pandoc-types, test-framework, test-framework-hunit

Dependency ranges changed: ConfigFile, directory, filepath, old-locale, pandoc, process, time

Files

− README
@@ -1,350 +0,0 @@--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, a document-processing library.  For best results, use the-   revision control version of Pandoc.  If you do not, inline and-   block-level equations will look the same.-- - 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-------------To begin, set the following environment variable:--  MB_BASE_DIR--This is the location of your blog files on the filesystem.  It must be-an absolute path.--Once you've set the environment variable, 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_DIR=`pwd`/myBlog-  $ mb-  Blog directory: .../myBlog-  Setting up data directory using skeleton: /.../skel-  Configuration file changed; regenerating all content.-  Templates changed; regenerating accordingly.-  Posts changed:-    first-post.txt-  Post index changed; regenerating next/previous links.-  Rendering first-post-  Done.--Now you have a blog; fire up a web server pointing at myBlog/html and-you'll see a demo post.--(NOTE: to serve your blog files you can either make the "html"-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 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.)--Next, you'll need to make a few changes to the default configuration,-"blog.cfg".  This file contains information about you which may be-included in the generated pages, such as your name, e-mail address,-and the title of your blog.  Most importantly, this file contains the-base URL of your blog which is used to generate URLs in the blog's-pages.--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 ".../myBlog"-  Processing: second-post-  Generating all-posts list.-  Done.--When to run mb-----------------mb looks at the modification times of your post files in posts/, the-config file, template files, and the post index (see below) when-determining when to regenerate content.  It also looks for new posts-that haven't been rendered in the past.  The rule of thumb is: re-run-mb whenever you make any changes to:-- - templates in templates/- - post files in posts/- - the post index, posts/posts-index- - blog.cfg--Modifying a post will cause that post to be re-rendered, but-modifiying the configuration file will cause ALL posts to be-re-rendered (since the configuration file contains values that will-affect all hyperlinks contained inside each post).  mb will always-rebuild the portions of each page affected by template changes even if-post source files are unchanged.--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-  Blog directory: /.../blog-  Post index changed; regenerating next/previous links.-  Done.--  Blog directory: /.../blog-  Configuration file changed; regenerating all content.-  Rendering 1 post(s)...-  Rendering first-post-  Done.--  Blog directory: /Users/cygnus/blog3-  Posts changed:-    first-post.txt-  Rendering 1 post(s)...-  Rendering first-post-  Done.--Above I ran "mb" in listen mode and then modified the post index, then-the blog config file, then one of the posts.  In each case "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/rssTemplate.xml--    This is the template used to generate your RSS feed.--  templates/pageTemplate.html--    This file makes up the overall structure of every page on the-    blog.--  templates/postTemplate.html--    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:--  $title$-  $baseUrl$-  $authorName$-  $authorEmail$--    These placeholders all correspond directly to fields on the-    blog.cfg configuration file.--These placeholders are supported in the post template:--  $jsInfo$--    A brief javascript used to provide information about the page to-    other javascripts (see "Other features" below for usage).--  $nextPrevLinks$--    HTML displaying the "next" and "previous" links for older/newer-    posts.--  $post$--    The body of the post itself.--These placeholders are supported in the page template:--  $content$--    The content of the page to be rendered.--Gnuplot Graph Embedding--------------------------mathblog supports embedding gnuplot scripts in blog posts.  To do-this, we overload the Pandoc code block syntax.  Here's an example:--~~~ {#eq-basic}-f(x) = x-plot [-5.0:5.0] [-5.0:5.0] f(x) lt rgb "blue"-~~~--This defines a single function and plots it in blue over the specified-intervals.  The most important part is the "#eq-basic" bit: this-specifies the equation preamble to use when generating the output-image.  This string refers to this preamble file in your blog-directory:--  eq-preambles/eq-basic.txt--This file contains the commands responsible for determining the output-image size and axis configuration.  The contents of the code block-that you write get appended to the contents of the preamble file you-specify to result in the full gnuplot script, whose output gets-embedded in the page.  We use these templates to make it easier to-create many graphs that have the same general structure (e.g., axis-configuration, image size) without having to repeat the full gnuplot-script each time.--You can create other gnuplot premables to suit your needs; just-reference them in the code block in the same way.  For example, you-might create eq-preambles/eq-fancy.txt, in which case the code block-syntax to use it is:--~~~ {#eq-fancy}-<your script here>-~~~--Note that the pandoc syntax also allows us to assign CSS class names-to the code block, and mathblog passes these through to the generated-image.  So if you wanted to wrap your text around the generated image,-you could create a CSS class like this:--.eq-right {-  float: right;-}--and then assign it to your equation graph like this:--~~~ {#eq-basic .eq-right}-...-~~~--For more information on the code block syntax, please see:--  http://johnmacfarlane.net/pandoc/README.html#delimited-code-blocks--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;--Controlling Post Order-------------------------Whenever you add a new post, mb automatically updates the "post-index", a file called "posts-index" in the post source directory.-This file lists the filenames of all posts from newest to oldest.  By-default, new posts get added to the beginning of the list, as you-would expect.  Any new posts added to the list are sorted by-modification time so that the newest post on disk appears earlier in-the index.  This feature exists to make it possible for older posts to-be updated without changing their ordering in the overall sequence of-posts.--At any time, you can edit the index to reorder the posts as you see-fit.  mb will preserve ordering of posts already in the index when you-run it in the future.--mb will also take care of removing posts from the index if they've-been removed from the post source directory.--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
+ README.md view
@@ -0,0 +1,470 @@++mathblog+========++mathblog is a Haskell program targeted at people who want to write+statically-generated, mathematically-themed weblogs.  It supports:++ - Extended Markdown input syntax as supported by the Pandoc library++ - inline and block-level TeX math rendered by MathJax or LaTeX++ - Function graphing with Gnuplot or the TikZ / pgfplots LaTeX+   packages++ - Integration of Javascript-based web services such as Disqus++ - Template-based document rendering with support for layout and style+   customization++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 or web framework 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 `$...$` or `\(...\)` for inline math and+   `$$...$$` or `\[...\]` for block-level math.++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, a document-processing library.++ - Math typesetting packages:++   - GladTex and LaTeX for math typesetting if you choose `gladtex`+     for the value of the 'mathBackend' configuration setting.+     GladTex 1.3 is required for best results.  GladTex renders LaTeX+     snippets to images.  This is the recommended backend for math+     typesetting.  See <https://sourceforge.net/projects/gladtex/>++   - MathJax if you choose `mathjax` for the value of the+     'mathBackend' configuration setting.  mathblog uses the MathJax+     CDN for MathJax resources.++ - Function graph plotting packages:++   - The TikZ and pgfplots LaTeX packages if you choose `gnuplot` for+     the value of the 'eqBackend' configuration setting.  This is the+     recommended backend for function graph plotting.++   - GNUplot if you choose `gnuplot` for the value of the 'eqBackend'+     configuration setting.++Creating a blog+===============++To begin, set the following environment variable:++  `MB_BASE_DIR`++This is the location of your blog files on the filesystem.  It must be+an absolute path.  If you would rather use a command-line parameter,+specify `-d` or `--baseDir` when running `mb`.  The command line+directory parameter will always override the environment variable if+it is set.++Once you've set the environment variable, just run `mb -i`.  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_DIR=`pwd`/myBlog+$ mb -i+Blog directory: .../myBlog+Setting up data directory using skeleton: /.../skel+Configuration file changed; regenerating all content.+Templates changed; regenerating accordingly.+Posts changed:+  first-post.txt+Post index changed; regenerating next/previous links.+Rendering first-post+Done.+~~~++Configuring the Blog+====================++The default blog configuration file is called `blog.cfg`.  This file+contains information about you which may be included in the generated+pages.  The configuration file `blog.cfg` must have the following+fields set:++ - `baseUrl`++    The base URL of your blog; this URL will be used to generate some+    links in the blog's pages where absolute URLs matter.  Otherwise,+    relative URLs will be used, with an assumption that your blog is+    hosted at the root of a domain.++ - `title`++    The title of your blog, such as "My math blog".++ - `authorName`++    Your name (for the RSS feed metadata and the page footer).++ - `authorEmail`++    Your e-mail address (for the RSS feed metadata only).++ - `mathBackend`++    The backend used to render TeX math expressions.  Can be either+    `gladtex` or `mathjax`.  If `gladtex`, you will need the latest+    version of the `gladtex` program for best results.  No special+    configuration is necessary for MathJax, as mathblog's default+    templates use CDN resources for MathJax.++ - `eqBackend`++    The backend used to render function graphs.  Can be either `tikz`+    or `gnuplot`.  See the Function Graph Embedding section below for+    details.++Note that if you pick a math or function graphing backend which isn't+supported by your system, `mb` will emit errors when processing your+posts.++All of the above fields can be accessed in templates using the syntax+described in "Customizing your blog" below.++Serving the Blog+================++To serve your blog files you can either make the "html" 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+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+look.++Now you might want to edit or create a new post, or even remove one.+Just edit the appropriate file in `posts/` accordingly and re-run+`mb`.  Here's an example of adding a new post:++~~~+$ mb+mb: using base directory ".../myBlog"+Posts changed:+  first-post.txt+Rendering 1 post(s)...+Rendering first-post+Done.+~~~++When to run `mb`+================++`mb` looks at the modification times of your post files in `posts/`,+the config file, template files, and the post index (see below) when+determining when to regenerate content.  It also looks for new posts+that haven't been rendered in the past.  The rule of thumb is: re-run+mb whenever you make any changes to:++ - templates in `templates/`+ - post files in `posts/`+ - the post index, `posts/posts-index`+ - `blog.cfg`++Modifying a post will cause that post to be re-rendered, but+modifiying the configuration file will cause ALL posts to be+re-rendered (since the configuration file contains values that will+affect all hyperlinks contained inside each post).  `mb` will always+rebuild the portions of each page affected by template changes even if+post source files are unchanged.++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+Blog directory: /.../blog+Post index changed; regenerating next/previous links.+Done.++Blog directory: /.../blog+Configuration file changed; regenerating all content.+Rendering 1 post(s)...+Rendering first-post+Done.++Blog directory: /Users/cygnus/blog3+Posts changed:+  first-post.txt+Rendering 1 post(s)...+Rendering first-post+Done.+~~~++Above I ran `mb` in listen mode and then modified the post index, then+the blog config file, then one of the posts.  In each case `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/rssTemplate.xml`++    This is the template used to generate your RSS feed.++ - `templates/pageTemplate.html`++    This file makes up the overall structure of every page on the+    blog.++ - `templates/postTemplate.html`++    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:++  - `$title$`, `$baseUrl$`, `$authorName$`, `$authorEmail$`++    These placeholders all correspond directly to fields on the+    `blog.cfg` configuration file.++  - `$extraPageHead$`++    Content to be placed in the `<HEAD>` tag of the page, such as+    javascript tags, stylesheets, etc.  You'll need to ensure that+    this is somewhere in your `<HEAD>` tag if you want to use mathblog+    features which may need to load extra resources.++These placeholders are supported in the post template:++ - `$jsInfo$`++    A brief javascript used to provide information about the page to+    other javascripts (see "Other features" below for usage).++ - `$nextPrevLinks$`++    HTML displaying the "next" and "previous" links for older/newer+    posts.++ - `$post$`++    The body of the post itself.++These placeholders are supported in the page template:++ - `$content$`++    The content of the page to be rendered.++Function Graph Embedding+========================++mathblog supports inline scripts for rendering function graphs.  Right+now, mathblog supports Gnuplot and the TikZ / pgfplots LaTeX packages.+Set the 'eqBackend' configuration option (see above) to choose a+backend.++Gnuplot+-------++To specify a Gnuplot function graph in a blog post, we overload the+Pandoc code block syntax.  Here's an example of a Gnuplot graph:++    ~~~ {#eq-basic}+    f(x) = x+    plot [-5.0:5.0] [-5.0:5.0] f(x) lt rgb "blue"+    ~~~++This defines a single function and plots it in blue over the specified+intervals.  The most important part is the "#eq-basic" bit: this+specifies the equation preamble to use when generating the output+image.  This string refers to this preamble file in your blog+directory:++  `eq-preambles/eq-basic.txt`++This file contains the commands responsible for determining the output+image size and axis configuration.  The contents of the code block+that you write get appended to the contents of the preamble file you+specify to result in the full gnuplot script, whose output gets+embedded in the page.  We use these templates to make it easier to+create many graphs that have the same general structure (e.g., axis+configuration, image size) without having to repeat the full gnuplot+script each time.++You can create other gnuplot premables to suit your needs; just+reference them in the code block in the same way.  For example, you+might create eq-preambles/eq-fancy.txt, in which case the code block+syntax to use it is:++    ~~~ {#eq-fancy}+    <your script here>+    ~~~++TikZ / pgfplots+---------------++To specify a TikZ / pgfplots function graph in a blog post, we+overload the Pandoc code block syntax.  Here's an example of a TikZ+figure:++    ~~~ {#tikz}+    \begin{axis}[+            minor tick num=3,+            axis y line=center,+            axis x line=middle,+        ]++    \addplot[smooth,mark=none,blue] plot coordinates {+        (-1,-1)+        (2.95,2.95)+    };+    ~~~++This is a LaTeX fragment which will automatically be embedded in a+`tikzpicture` environment and rendered to an image within the blog+post.  For more information on the powerful TikZ / pgfplots packages,+please see <https://sourceforge.net/projects/pgfplots/>++Styling Function Graphs with CSS+--------------------------------++Note that the Pandoc syntax also allows us to assign CSS class names+to the code block, and mathblog passes these through to the generated+image.  So if you wanted to wrap your text around the generated image,+you could create a CSS class like this:++    .eq-right {+      float: right;+    }++and then assign it to your equation graph like this:++    ~~~ {#eq-basic .eq-right}+    ...+    ~~~++For more information on the code block syntax, please see:++  <http://johnmacfarlane.net/pandoc/README.html#delimited-code-blocks>++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;++Controlling Post Order+======================++Whenever you add a new post, mb automatically updates the "post+index", a file called `posts-index` in the post source directory.+This file lists the filenames of all posts from newest to oldest.  By+default, new posts get added to the beginning of the list, as you+would expect.  Any new posts added to the list are sorted by+modification time so that the newest post on disk appears earlier in+the index.  This feature exists to make it possible for older posts to+be updated without changing their ordering in the overall sequence of+posts.++At any time, you can edit the index to reorder the posts as you see+fit.  `mb` will preserve ordering of posts already in the index when+you run it in the future.++`mb` will also take care of removing posts from the index if they've+been removed from the post source directory.++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>)
mathblog.cabal view
@@ -1,35 +1,26 @@ Name:                mathblog-Version:             0.2-Synopsis:            A program for creating and managing a-                     static, mathematically-inclined weblog+Version:             0.3+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                      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 and a full feature list, please see-                     the README.--                     To see a live example of this software, you might-                     consider taking a look at my math-oriented blog-                     at <http://dw.codevine.org/>.+                     parts and support for embedded LaTeX math,+                     embedded function plotting, and the UNIX editor+                     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> Build-Type:          Simple-Cabal-version:       >= 1.2+Cabal-version:       >= 1.6 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+  README.md   skel/html/stylesheets/stylesheet.css   skel/templates/postTemplate.html   skel/templates/pageTemplate.html@@ -38,20 +29,25 @@   skel/posts/first-post.txt   skel/blog.cfg +Source-Repository head+  type:     git+  location: git://github.com/jtdaugherty/mathblog.git+ Executable mb   Build-depends:     base >= 3 && < 5,-    directory >= 1.0 && < 1.1,-    filepath >= 1.1 && < 1.2,-    pandoc >= 1.6 && < 1.7,+    directory >= 1.0 && < 1.1.0.2,+    filepath >= 1.1,+    pandoc >= 1.7 && < 1.9,+    pandoc-types >= 1.7 && < 1.9,     unix >= 2.3 && < 2.5,-    process >= 1.0 && < 1.1,-    time >= 1.1 && < 1.2,-    old-locale >= 1.0 && < 1.1,+    process >= 1.0,+    time >= 1.1,+    old-locale >= 1.0,     SHA >= 1.4 && < 1.5,     bytestring >= 0.9 && < 1.0,     HStringTemplate >= 0.6 && < 0.7,-    ConfigFile >= 1.0 && < 1.1+    ConfigFile >= 1.1 && < 1.2    Other-modules:     Paths_mathblog@@ -60,10 +56,44 @@     MB.Files     MB.Types     MB.Util-    MB.Gladtex     MB.Processing+    MB.Startup+    MB.Gladtex+    MB.Gnuplot+    MB.Mathjax+    MB.Tikz    GHC-Options: -Wall    Hs-Source-Dirs:  src   Main-is:         Main.hs++Executable mb-tests+  Build-depends:+    base >= 3 && < 5,+    directory >= 1.0 && < 1.2,+    filepath >= 1.1,+    pandoc >= 1.8 && < 1.9,+    pandoc-types >= 1.8 && < 1.9,+    unix >= 2.3 && < 2.5,+    process >= 1.0 && < 1.1,+    time >= 1.1,+    old-locale >= 1.0 && < 1.1,+    SHA >= 1.4 && < 1.5,+    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,+    test-framework-hunit >= 0.2.6 && < 0.3++  GHC-Options: -Wall+  Cpp-Options: -DTESTING++  Hs-Source-Dirs:  tests/src,+                   src++  Main-is:         Main.hs+  Other-Modules:+    InitTests+    MB.StartupTests
− skel/html/images/newer-subdued.png

binary file changed (1198 → absent bytes)

− skel/html/images/newer.png

binary file changed (1198 → absent bytes)

− skel/html/images/older-subdued.png

binary file changed (1190 → absent bytes)

− skel/html/images/older.png

binary file changed (1190 → absent bytes)

skel/html/stylesheets/stylesheet.css view
@@ -67,34 +67,17 @@ .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;@@ -121,4 +104,11 @@     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/posts/first-post.txt view
@@ -8,14 +8,12 @@ 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>.+processed with <a href="http://mathjax.com/">MathJax</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:+Inline math is introduced with `$...$` or `\(...\)` and looks like $x+\in Y$.  Math can be embedded in post titles, as well!  To insert a+block-level equation, use `$$...$$` or `\[...\]`.  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}}$$@@ -25,17 +23,14 @@ $\int^n_0{\ldots} = \sum^n_{i=1}{b_0 G(u^s)} \Longrightarrow \frac{\tan x}{\lim_{h \to \infty}{\Phi}}$ -(NOTE: if the two images immediately above are identical, then you-must run the revision control version of Pandoc, which includes an-unreleased fix!)- 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.+found in "pageTemplate.html" and "postTemplate.html" 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.  See the configuration file, `blog.cfg`, for some of those+settings and see the templates for the rest.  Note that `mb` will not bother to regenerate the actual content of your blog posts unless the corresponding source file in `posts` has
skel/templates/pageTemplate.html view
@@ -4,14 +4,15 @@     <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.2.0/build/cssreset/reset-min.css">     <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.2.0/build/cssbase/base-min.css">     <link rel="stylesheet" type="text/css" href="http://yui.yahooapis.com/3.2.0/build/cssfonts/fonts-min.css">-    <link rel="stylesheet" type="text/css" href="$baseUrl$/stylesheets/stylesheet.css"/>+    <link rel="stylesheet" type="text/css" href="/stylesheets/stylesheet.css"/>     <link rel="alternate" type="application/rss+xml" href="$baseUrl$/feed.xml"/>+    $extraPageHead$   </head>   <body>     <div id="page">       <div id="header">-        <a id="listing" href="$baseUrl$/posts/">all posts</a>-        <a href="$baseUrl$">$title$</a>+        <a id="listing" href="/posts/">all posts</a>+        <a href="/">$title$</a>       </div>       $content$       <div id="footer">
src/MB/Files.hs view
@@ -5,61 +5,55 @@     , takeBaseName     ) import MB.Types-    ( Config(..)+    ( Blog(..)     , Post(..)     ) -indexHtml :: Config -> FilePath+indexHtml :: Blog -> FilePath indexHtml c = htmlDir c </> "index.html" -imageFilename :: Config -> String -> FilePath+imageFilename :: Blog -> String -> FilePath imageFilename c fn = imageDir c </> fn -rssXml :: Config -> FilePath+rssXml :: Blog -> FilePath rssXml c = htmlDir c </> "feed.xml" -eqPreambleFile :: Config -> String -> FilePath+eqPreambleFile :: Blog -> String -> FilePath eqPreambleFile config n = eqPreamblesDir config </> n -rssTemplatePath :: Config -> FilePath+rssTemplatePath :: Blog -> FilePath rssTemplatePath c = templateDir c </> "rssTemplate.xml" -listHtml :: Config -> FilePath+listHtml :: Blog -> FilePath listHtml c = postHtmlDir c </> "index.html" -listHtex :: Config -> FilePath-listHtex c = htmlTempDir c </> "list.htex"--postIndex :: Config -> FilePath+postIndex :: Blog -> FilePath postIndex c = postSourceDir c </> "posts-index" -listTmpHtml :: Config -> FilePath+listTmpHtml :: Blog -> FilePath listTmpHtml c = htmlTempDir c </> "list.html" -firstPost :: Config -> FilePath+firstPost :: Blog -> FilePath firstPost c = postSourceDir c </> "first-post.txt" -pageTemplatePath :: Config -> FilePath+pageTemplatePath :: Blog -> FilePath pageTemplatePath c = templateDir c </> "pageTemplate.html" -postTemplatePath :: Config -> FilePath+postTemplatePath :: Blog -> FilePath postTemplatePath c = templateDir c </> "postTemplate.html" -postUrl :: Config -> Post -> String-postUrl c p = baseUrl c ++ "/posts/" ++ postBaseName p ++ ".html"+postUrl :: Post -> String+postUrl p = "/posts/" ++ postBaseName p ++ ".html"  postBaseName :: Post -> String postBaseName = takeBaseName . postFilename -postHtex :: Config -> Post -> String-postHtex config p = htmlTempDir config </> postBaseName p ++ ".htex"--postIntermediateHtml :: Config -> Post -> FilePath+postIntermediateHtml :: Blog -> Post -> FilePath postIntermediateHtml config post =     postIntermediateDir config </> postBaseName post ++ ".html" -postFinalHtml :: Config -> Post -> FilePath+postFinalHtml :: Blog -> Post -> FilePath postFinalHtml config p = postHtmlDir config </> postBaseName p ++ ".html" -stylesheet :: Config -> FilePath+stylesheet :: Blog -> FilePath stylesheet c = stylesheetDir c </> "stylesheet.css"
src/MB/Gladtex.hs view
@@ -1,35 +1,65 @@ module MB.Gladtex-    ( gladTex+    ( gladtexProcessor     , checkForGladtex     ) where++import Control.Applicative ((<$>)) import System.Exit     ( ExitCode(..)     , exitFailure     )+import System.Directory+    ( copyFile+    ) import System.Process     ( readProcessWithExitCode     ) import Control.Monad     ( when     )+import qualified Text.Pandoc as Pandoc import MB.Types +gladtexProcessor :: Processor+gladtexProcessor =+    nullProcessor { applyWriterOptions = Just gladtexWriterOptions+                  , postProcessPost = Just processGladtex+                  , buildPostTitle = Just gladtexTitle+                  }++gladtexWriterOptions :: Pandoc.WriterOptions -> Pandoc.WriterOptions+gladtexWriterOptions opts =+    opts { Pandoc.writerHTMLMathMethod = Pandoc.GladTeX+         }+ gladtexProgName :: String gladtexProgName = "gladtex" -gladTex :: Config -> FilePath -> String -> IO ()-gladTex config htexPath color = do-  let args = [ "-d"-             , imageDir config-             , "-u"-             , baseUrl config ++ "/generated-images/"-             , "-r"-             , "120"-             , "-b"-             , "FFFFFF"-             , "-c"-             , color+processGladtex :: Blog -> FilePath -> Page -> IO ()+processGladtex blog path page = do+  -- Copy the final HTML path (which was passed in) to temp file since+  -- we ultimately need to write to the final HTML path.+  let htexPath = path ++ ".htex"+      textColor = case page of+                    Index -> "0000FF"+                    BlogPost -> "000000"++  copyFile path htexPath++  -- gladtex will overwrite the original .html file.+  gladTex blog htexPath textColor++gladTex :: Blog -> FilePath -> String -> IO ()+gladTex blog htexPath color = do+  let args = [ "-d", imageDir blog+             , "-u", "/generated-images/"+             , "-r", "120"+             , "-s", "4"+             , "-b", "FFFFFF"+             , "-c", color+             , "-l", "displaymath"+             , "-i", "math"              , htexPath              ] @@ -51,3 +81,12 @@                        "got exit status " ++ (show c)           exitFailure     _ -> return ()++gladtexTitle :: Page -> [Pandoc.Inline] -> [Pandoc.Inline]+gladtexTitle BlogPost ts = rewriteInline 190 <$> ts+gladtexTitle Index ts = rewriteInline 110 <$> ts++rewriteInline :: Int -> Pandoc.Inline -> Pandoc.Inline+rewriteInline dpi (Pandoc.Math v s) =+    Pandoc.Math v $ "<EQ ENV=\"math\" DPI=\"" ++ show dpi ++ "\">" ++ s ++ "</EQ>"+rewriteInline _ i = i
+ src/MB/Gnuplot.hs view
@@ -0,0 +1,110 @@+module MB.Gnuplot+    ( gnuplotProcessor+    )+where++import Control.Monad+    ( forM+    )+import Data.List+    ( intercalate+    )+import Data.Digest.Pure.SHA+    ( showDigest+    , sha1+    )+import Data.ByteString.Lazy.Char8+    ( pack+    )+import System.Directory+    ( doesFileExist+    )+import System.Process+    ( readProcessWithExitCode+    )+import System.Exit+    ( ExitCode(..)+    )+import qualified Text.Pandoc as Pandoc+import MB.Types+import qualified MB.Files as Files++gnuplotProcessor :: Processor+gnuplotProcessor =+    nullProcessor { preProcessPost = Just renderGnuPlot+                  }++renderGnuPlot :: Blog -> Post -> IO Post+renderGnuPlot config post = do+  let Pandoc.Pandoc m blocks = postAst post+  newBlocks <- forM blocks $ \blk ->+               case blk of+                 Pandoc.CodeBlock (preambleName, classes, _) s ->+                     renderGnuPlotScript config preambleName s classes+                 b -> return b++  return $ post { postAst = Pandoc.Pandoc m newBlocks }++loadPreamble :: Blog -> String -> IO (Maybe String)+loadPreamble config preambleName = do+  let filename = Files.eqPreambleFile config $ preambleName ++ ".txt"+  e <- doesFileExist filename+  case e of+    False -> return Nothing+    True -> do+           s <- readFile filename+           s `seq` return ()+           return $ Just s++renderGnuPlotScript :: Blog+                    -> String+                    -> String+                    -> [String]+                    -> IO Pandoc.Block+renderGnuPlotScript config preambleName rawScript classes = do+  putStrLn $ "Rendering equation graph, type=" ++ preambleName++  mPreamble <- loadPreamble config preambleName++  case mPreamble of+    Nothing -> do+      putStrLn $ "Error: no such gnuplot preamble: " ++ preambleName+      return $ Pandoc.Para [Pandoc.Str "[[COULD NOT DRAW EQUATION]]"]+    Just preamble -> do++      let scriptLines = lines rawScript+          preambleLines = lines preamble+          digestInput = preambleName ++ rawScript++      -- Generate an image name in the images/ directory of the blog+      -- data directory.  Use a hash of the preamble name and script+      -- contents so we can avoid rendering the image again if it+      -- already exists.+      let hash = showDigest $ sha1 $ pack digestInput+          imageFilename = preambleName ++ "-" ++ hash ++ ".png"+          imagePath = Files.imageFilename config imageFilename+          outputLines = [ "set term png enhanced"+                        , "set output \"" ++ imagePath ++ "\""+                        ]+          fullScript = intercalate "; " $ outputLines ++ preambleLines ++ scriptLines++      -- Invoke gnuplot to render the image+      (status, out, err) <- readProcessWithExitCode "gnuplot" ["-e", fullScript] ""++      case status of+        ExitSuccess -> return ()+        ExitFailure _ -> do+                       putStrLn "Could not render equation:"+                       putStrLn "Equation was:"+                       putStrLn rawScript+                       putStrLn "gnuplot output:"+                       putStrLn out+                       putStrLn err++      return $ Pandoc.Para [Pandoc.RawInline "html" $ concat [ "<img src=\"/generated-images/"+                                                       , imageFilename+                                                       , "\" class=\""+                                                       , intercalate " " classes+                                                       , "\">"+                                                       ]+                           ]
+ src/MB/Mathjax.hs view
@@ -0,0 +1,58 @@+module MB.Mathjax+    ( mathjaxProcessor+    )+where++import qualified Text.Pandoc as Pandoc+import MB.Types++mathjaxProcessor :: Processor+mathjaxProcessor =+    nullProcessor { applyWriterOptions = Just mathjaxOpts+                  , pageHead = Just mathjaxPageHead+                  }++mathjaxOpts :: Pandoc.WriterOptions -> Pandoc.WriterOptions+mathjaxOpts opts =+    opts { Pandoc.writerHTMLMathMethod = Pandoc.MathJax "MathJax/MathJax.js"+         }++mathjaxPageHead :: String+mathjaxPageHead =+    "<script type=\"text/x-mathjax-config\">\n\+    \    MathJax.Hub.Config(\n\+    \      {\"HTML-CSS\": { preferredFont: \"TeX\", availableFonts: [\"STIX\",\"TeX\"] },\n\+    \       tex2jax: {\n\+    \         element: null,\n\+    \         preview: \"none\",\n\+    \         skipTags: [\"script\",\"noscript\",\"style\",\"textarea\",\"pre\",\"code\"],\n\+    \         inlineMath: [\n\+    \           ['$', '$'],\n\+    \           [\"\\\\(\", \"\\\\)\"],\n\+    \         ],\n\+    \         displayMath: [\n\+    \           ['$$', '$$'],\n\+    \           [\"\\\\[\", \"\\\\]\"],\n\+    \         ],\n\+    \         processEscapes: true,\n\+    \         ignoreClass: \"tex2jax_ignore|dno\"\n\+    \       },\n\+    \       TeX: {\n\+    \         noUndefined: { attributes: {\n\+    \                          mathcolor: \"red\",\n\+    \                          mathbackground: \"#FFEEEE\",\n\+    \                          mathsize: \"90%\" }\n\+    \                      }\n\+    \       },\n\+    \       messageStyle: \"none\"\n\+    \    });\n\+    \</script>\n\+    \<script type=\"text/javascript\" src=\"http://cdn.mathjax.org/mathjax/latest/MathJax.js?config=TeX-AMS_HTML\"></script>\n\+    \<script type=\"text/javascript\">\n\+    \  // Uncomment this code and set the style of the \"page\" element\n\+    \  // below to visibility: hidden initially if you don't like the\n\+    \  // MathJax flicker during the rendering process. :(\n\+    \  // MathJax.Hub.Queue(function () {\n\+    \  //     document.getElementById(\"page\").style.visibility = \"visible\";\n\+    \  // });\n\+    \</script>\n"
src/MB/Processing.hs view
@@ -1,108 +1,58 @@ module MB.Processing-    ( processPost+    ( applyPreProcessors+    , applyPostProcessors+    , getWriterOptions+    , getRawPostTitle+    , getPostTitle     ) where -import Control.Monad-    ( forM-    )-import Data.List-    ( intercalate-    )-import Data.Digest.Pure.SHA-    ( showDigest-    , sha1-    )-import Data.ByteString.Lazy.Char8-    ( pack-    )-import System.Directory-    ( doesFileExist-    )-import System.Process-    ( readProcessWithExitCode-    )-import System.Exit-    ( ExitCode(..)-    )+import Control.Applicative ((<$>), (<*>), pure)+import Data.Maybe (catMaybes) import qualified Text.Pandoc as Pandoc import MB.Types-import qualified MB.Files as Files -processPost :: Config -> Post -> IO Post-processPost = renderGnuPlot--renderGnuPlot :: Config -> Post -> IO Post-renderGnuPlot config post = do-  let Pandoc.Pandoc m blocks = postAst post-  newBlocks <- forM blocks $ \blk ->-               case blk of-                 Pandoc.CodeBlock (preambleName, classes, _) s ->-                     renderGnuPlotScript config preambleName s classes-                 b -> return b--  return $ post { postAst = Pandoc.Pandoc m newBlocks }--loadPreamble :: Config -> String -> IO (Maybe String)-loadPreamble config preambleName = do-  let filename = Files.eqPreambleFile config $ preambleName ++ ".txt"-  e <- doesFileExist filename-  case e of-    False -> return Nothing-    True -> do-           s <- readFile filename-           s `seq` return ()-           return $ Just s+applyPreProcessors :: Blog -> Post -> IO Post+applyPreProcessors b post = applyPreProcessors_ b post (processors b) -renderGnuPlotScript :: Config-                    -> String-                    -> String-                    -> [String]-                    -> IO Pandoc.Block-renderGnuPlotScript config preambleName rawScript classes = do-  putStrLn $ "Rendering equation graph, type=" ++ preambleName+applyPostProcessors :: Blog -> FilePath -> Page -> IO ()+applyPostProcessors b path pg = applyPostProcessors_ b path pg (processors b) -  mPreamble <- loadPreamble config preambleName+applyPreProcessors_ :: Blog -> Post -> [Processor] -> IO Post+applyPreProcessors_ _ post [] = return post+applyPreProcessors_ b post (p:ps) = do+  post' <- case preProcessPost p of+             Nothing -> return post+             Just f -> f b post+  applyPreProcessors_ b post' ps -  case mPreamble of-    Nothing -> do-      putStrLn $ "Error: no such gnuplot preamble: " ++ preambleName-      return $ Pandoc.Para [Pandoc.Str "[[COULD NOT DRAW EQUATION]]"]-    Just preamble -> do+applyPostProcessors_ :: Blog -> FilePath -> Page -> [Processor] -> IO ()+applyPostProcessors_ _ _ _ [] = return ()+applyPostProcessors_ b pth pg (p:ps) = do+  case postProcessPost p of+    Nothing -> return ()+    Just f -> f b pth pg+  applyPostProcessors_ b pth pg ps -      let scriptLines = lines rawScript-          preambleLines = lines preamble-          digestInput = preambleName ++ rawScript+getWriterOptions :: Blog -> Pandoc.WriterOptions -> Pandoc.WriterOptions+getWriterOptions b = foldl (.) id (catMaybes $ applyWriterOptions <$> processors b) -      -- Generate an image name in the images/ directory of the blog-      -- data directory.  Use a hash of the preamble name and script-      -- contents so we can avoid rendering the image again if it-      -- already exists.-      let hash = showDigest $ sha1 $ pack digestInput-          imageFilename = preambleName ++ "-" ++ hash ++ ".png"-          imagePath = Files.imageFilename config imageFilename-          outputLines = [ "set term png enhanced"-                        , "set output \"" ++ imagePath ++ "\""-                        ]-          fullScript = intercalate "; " $ outputLines ++ preambleLines ++ scriptLines+getRawPostTitle :: Blog -> Post -> String+getRawPostTitle b p = head (fs ++ [fallback]) $ postTitle p+    where+      fallback = concat . (getInlineStr <$>)+      fs = catMaybes $ rawPostTitle <$> processors b -      -- Invoke gnuplot to render the image-      (status, out, err) <- readProcessWithExitCode "gnuplot" ["-e", fullScript] ""+getInlineStr :: Pandoc.Inline -> String+getInlineStr (Pandoc.Str s) = s+getInlineStr (Pandoc.Math _ s) = s+getInlineStr Pandoc.Space = " "+getInlineStr i = error $ "Unexpected inline in document title, got " ++ (show i) -      case status of-        ExitSuccess -> return ()-        ExitFailure _ -> do-                       putStrLn "Could not render equation:"-                       putStrLn "Equation was:"-                       putStrLn rawScript-                       putStrLn "gnuplot output:"-                       putStrLn out-                       putStrLn err+getPostTitle :: Blog -> Post -> Page -> String+getPostTitle b p s = concat $ getInlineStr <$> f (postTitle p)+    where+      f = foldl (.) id (reverse $ fs <*> (pure s)) -      return $ Pandoc.Para [Pandoc.HtmlInline $ concat [ "<img src=\"" ++ baseUrl config ++ "/generated-images/"-                                                       , imageFilename-                                                       , "\" class=\""-                                                       , intercalate " " classes-                                                       , "\">"-                                                       ]-                           ]+      fs :: [Page -> [Pandoc.Inline] -> [Pandoc.Inline]]+      fs = catMaybes $ buildPostTitle <$> processors b
+ src/MB/Startup.hs view
@@ -0,0 +1,113 @@+{-# LANGUAGE CPP #-}+module MB.Startup+    ( StartupConfig(..)+    , startupConfigFromEnv+#ifdef TESTING+    , startupConfig+    , baseDirEnvName+    , baseDirParamName+    , getDataDirFlag+#endif+    )+where++import Control.Monad+    ( mplus+    , when+    )+import System.Exit+    ( exitFailure+    )+import System.Environment+    ( getEnvironment+    , getArgs+    )+import System.Console.GetOpt++data StartupConfig = StartupConfig { listenMode :: Bool+                                   , dataDirectory :: FilePath+                                   , initDataDirectory :: Bool+                                   , forceRegeneration :: Bool+                                   }+                     deriving (Show, Eq)++data Flag = Listen+          | DataDir FilePath+          | InitDataDirectory+          | ForceRegenerate+            deriving (Eq)++getDataDirFlag :: [Flag] -> Maybe FilePath+getDataDirFlag [] = Nothing+getDataDirFlag (DataDir p:_) = Just p+getDataDirFlag (_:fs) = getDataDirFlag fs++options :: [OptDescr Flag]+options = [ Option ['d'] [baseDirParamName] (ReqArg DataDir "PATH")+                       $ "Path where blog files will be stored.  If this is\n" +++                             "not specified, " ++ baseDirEnvName ++ " must be set in the\n" +++                             "environment."++          , Option ['i'] ["init"] (NoArg InitDataDirectory)+                       $ "Initialize the blog data directory."+          , Option ['l'] ["listen"] (NoArg Listen)+                       $ "Make mb poll periodically and regenerate your\n" +++                             "blog content when something changes.  This is\n" +++                             "useful if you want to run a local web server\n" +++                             "to view your posts as you're writing them."+          , Option ['f'] ["force"] (NoArg ForceRegenerate)+                       "Force a rebuild of all blog content."+          ]++-- |Inspect the program environment to create a startup configuration.+-- If the configuration information is invalid or absent, this will+-- automatically emit usage info and exit the program.+startupConfigFromEnv :: IO StartupConfig+startupConfigFromEnv = do+  env  <- getEnvironment+  args <- getArgs++  case startupConfig args env of+    Nothing -> usage >> exitFailure+    Just conf -> do+      when (head (dataDirectory conf) /= '/') $+           do+             putStrLn $ baseDirEnvName ++ " or " +++                          "--" ++ baseDirParamName ++ "=<path>" +++                                   " must specify an absolute path"+             exitFailure++      return conf++-- |Create a startup configuration from the specified arguments list+-- and environment variable list.  Returns Nothing if the required+-- information was absent.+startupConfig :: [String] -> [(String, String)] -> Maybe StartupConfig+startupConfig args env =+    case getOpt Permute options args of+      (_, _, (_:_)) -> Nothing+      (flags, _, []) -> do+        let lm = Listen `elem` flags+            i = InitDataDirectory `elem` flags+            f = ForceRegenerate `elem` flags+        d <- getDataDirFlag flags `mplus` (lookup baseDirEnvName env)++        return $ StartupConfig { listenMode = lm+                               , dataDirectory = d+                               , initDataDirectory = i+                               , forceRegeneration = f+                               }++baseDirEnvName :: String+baseDirEnvName = "MB_BASE_DIR"++baseDirParamName :: String+baseDirParamName = "baseDir"++usage :: IO ()+usage = do+  let h = concat [ "Usage: mb [args]\n\n"+                 , "mb is a tool for creating and managing a mathematically-inclined\n"+                 , "weblog.\n"+                 ]+  putStrLn $ usageInfo h options
+ src/MB/Tikz.hs view
@@ -0,0 +1,102 @@+module MB.Tikz+    ( tikzProcessor+    )+where++import Data.List+    ( intercalate+    )+import Data.Digest.Pure.SHA+    ( showDigest+    , sha1+    )+import Data.ByteString.Lazy.Char8+    ( pack+    )+import System.Process+    ( readProcessWithExitCode+    )+import System.Exit+    ( ExitCode(..)+    )+import qualified Text.Pandoc as Pandoc+import MB.Types+import qualified MB.Files as Files++tikzProcessor :: Processor+tikzProcessor =+    nullProcessor { preProcessPost = Just renderTikz+                  }++renderTikz :: Blog -> Post -> IO Post+renderTikz config post = do+  let Pandoc.Pandoc m blocks = postAst post+  newBlocks <- mapM (renderTikzScript config) blocks+  return $ post { postAst = Pandoc.Pandoc m newBlocks }++renderTikzScript :: Blog+                 -> Pandoc.Block+                 -> IO Pandoc.Block+renderTikzScript config blk@(Pandoc.CodeBlock ("tikz", classes, _) rawScript) = do+  putStrLn "Rendering equation graph (tikz)"++  let digestInput = rawScript++      -- Generate an image name in the images/ directory of the blog+      -- data directory.  Use a hash of the preamble name and script+      -- contents so we can avoid rendering the image again if it+      -- already exists.+      hash = showDigest $ sha1 $ pack digestInput+      imageFilename = "tikz-" ++ hash ++ ".png"+      imagePath = Files.imageFilename config imageFilename+      preamble = unlines [ "\\documentclass{article}"+                         , "\\usepackage{tikz}"+                         , "\\usepackage{pgfplots}"+                         , "\\pgfrealjobname{tmp}"++                         , "\\begin{document}"+                         , "\\begin{figure}"+                         , "\\beginpgfgraphicnamed{testfigure}"+                         , "\\begin{tikzpicture}"+                         ]+      postamble = unlines [ "\\end{tikzpicture}"+                          , "\\endpgfgraphicnamed"+                          , "\\end{figure}"+                          , "\\end{document}"+                          ]++      latexSource = preamble ++ rawScript ++ postamble++  writeFile "/tmp/tmp.tex" latexSource++  (s1, out1, _) <- readProcessWithExitCode "pdflatex" [ "-output-directory", "/tmp"+                                                      , "--jobname", "testfigure", "/tmp/tmp.tex"] ""+  case s1 of+    ExitFailure _ -> putStrLn out1 >> return blk+    ExitSuccess -> do+            -- Convert the temporary file to a PNG.+            (s2, out2, err) <- readProcessWithExitCode "convert" [ "-density", "120"+                                                                 , "-quality", "100"+                                                                 , "/tmp/testfigure.pdf"+                                                                 , imagePath+                                                                 ] ""++            case s2 of+              ExitFailure _ -> do+                      putStrLn "Could not render Tikz picture:"+                      putStrLn "Equation was:"+                      putStrLn latexSource+                      putStrLn "dvipng output:"+                      putStrLn out2+                      putStrLn err+                      return blk+              ExitSuccess ->+                  return $ Pandoc.Para [Pandoc.RawInline "html" $+                                              concat [ "<img src=\"/generated-images/"+                                                     , imageFilename+                                                     , "\" class=\""+                                                     , intercalate " " classes+                                                     , "\">"+                                                     ]+                                       ]+renderTikzScript _ b = return b
src/MB/Types.hs view
@@ -1,46 +1,64 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-} module MB.Types-    ( Config(..)+    ( Blog(..)     , Post(..)     , Template     , ChangeSummary(..)+    , Page(..)+    , Processor(..)+    , nullProcessor     ) where import Data.Time.Clock     ( UTCTime     )-import qualified Text.Pandoc.Definition as Pandoc+import qualified Text.Pandoc as Pandoc import Text.StringTemplate     ( StringTemplate     )  type Template = StringTemplate String -data Config = Config { baseDir :: FilePath-                     , postSourceDir :: FilePath-                     , htmlDir :: FilePath-                     , stylesheetDir :: FilePath-                     , postHtmlDir :: FilePath-                     , postIntermediateDir :: FilePath-                     , imageDir :: FilePath-                     , templateDir :: FilePath-                     , htmlTempDir :: FilePath-                     , baseUrl :: String-                     , eqPreamblesDir :: FilePath-                     , title :: String-                     , authorName :: String-                     , authorEmail :: String-                     , configPath :: FilePath-                     , blogPosts :: [Post]-                     }+data Blog = Blog { baseDir :: FilePath+                 , postSourceDir :: FilePath+                 , htmlDir :: FilePath+                 , stylesheetDir :: FilePath+                 , postHtmlDir :: FilePath+                 , postIntermediateDir :: FilePath+                 , imageDir :: FilePath+                 , templateDir :: FilePath+                 , htmlTempDir :: FilePath+                 , baseUrl :: String+                 , eqPreamblesDir :: FilePath+                 , title :: String+                 , authorName :: String+                 , authorEmail :: String+                 , configPath :: FilePath+                 , blogPosts :: [Post]+                 , processors :: [Processor]+                 } -data Post = Post { postTitle :: Int -> String-                 , postTitleRaw :: String+data Post = Post { postTitle :: [Pandoc.Inline]                  , postPath :: String                  , postFilename :: String                  , postModificationTime :: UTCTime                  , postAst :: Pandoc.Pandoc                  }++data Page = BlogPost+          | Index++data Processor =+    Processor { applyWriterOptions :: Maybe (Pandoc.WriterOptions -> Pandoc.WriterOptions)+              , preProcessPost :: Maybe (Blog -> Post -> IO Post)+              , postProcessPost :: Maybe (Blog -> FilePath -> Page -> IO ())+              , pageHead :: Maybe String+              , buildPostTitle :: Maybe (Page -> [Pandoc.Inline] -> [Pandoc.Inline])+              , rawPostTitle :: Maybe ([Pandoc.Inline] -> String)+              }++nullProcessor :: Processor+nullProcessor =+    Processor Nothing Nothing Nothing Nothing Nothing Nothing  -- Summarize changes in files so we know what to do during the -- regeneration phase.  postsChanged and configChanged are the primary
src/MB/Util.hs view
@@ -2,8 +2,6 @@     ( copyTree     , toUtcTime     , toLocalTime-    , pandocTitle-    , pandocTitleRaw     , rssModificationTime     , loadPostIndex     , getModificationTime@@ -92,12 +90,12 @@   dstFExists <- doesFileExist dstPath   dstDExists <- doesDirectoryExist dstPath -  when (dstFExists || dstDExists) $ do+  when (dstFExists) $ do     putStrLn $ "Cannot copy " ++ (show srcPath) ++ " to existing destination path " ++                  (show dstPath) ++ "; remove to continue."     exitFailure -  createDirectory dstPath+  when (not dstDExists) $ createDirectory dstPath   copyTree' srcPath dstPath    where@@ -127,22 +125,6 @@   tz <- getCurrentTimeZone   return $ utcToLocalTime tz u -pandocTitle :: Pandoc.Pandoc -> Int -> String-pandocTitle (Pandoc.Pandoc m _) dpi = concat $ map getStr $ Pandoc.docTitle m-    where-      getStr (Pandoc.Str s) = s-      getStr (Pandoc.Math _ s) = "<EQ DPI=\"" ++ show dpi ++ "\">" ++ s ++ "</EQ>"-      getStr Pandoc.Space = " "-      getStr i = error $ "Unexpected inline in document title, got " ++ (show i)--pandocTitleRaw :: Pandoc.Pandoc -> String-pandocTitleRaw (Pandoc.Pandoc m _) = concat $ map getStr $ Pandoc.docTitle m-    where-      getStr (Pandoc.Str s) = s-      getStr (Pandoc.Math _ s) = s-      getStr Pandoc.Space = " "-      getStr i = error $ "Unexpected inline in document title, got " ++ (show i)- rssModificationTime :: Post -> String rssModificationTime =     formatTime defaultTimeLocale rfc822DateFormat . postModificationTime@@ -152,9 +134,9 @@   fileContent <- readFile fullPath   t <- getModificationTime fullPath   let doc = Pandoc.readMarkdown Pandoc.defaultParserState fileContent+      Pandoc.Pandoc m _ = doc -  return $ Post { postTitle = pandocTitle doc-                , postTitleRaw = pandocTitleRaw doc+  return $ Post { postTitle = Pandoc.docTitle m                 , postPath = fullPath                 , postFilename = takeFileName fullPath                 , postModificationTime = t@@ -231,8 +213,8 @@                    , postIndexChanged                    ] -summarizeChanges :: Config -> IO ChangeSummary-summarizeChanges config = do+summarizeChanges :: Blog -> Bool -> IO ChangeSummary+summarizeChanges config forceAll = do   -- Determine whether the configuration file changed.  Check to see   -- if it's newer than the index.html file, or if no index.html   -- exists then that's equivalent to "the config changed"@@ -252,7 +234,8 @@                             return $ t > baseTime    let configChanged' = configMtime > baseTime-      modifiedPosts = filter (\p -> postModificationTime p > baseTime) $ blogPosts config+      needsRebuild p = forceAll || (postModificationTime p > baseTime)+      modifiedPosts = filter needsRebuild $ blogPosts config    -- Determine whether any templates changed   templateFiles <- dirFilenames (templateDir config)@@ -260,8 +243,8 @@                             mtime <- getModificationTime f                             return $ mtime > baseTime -  return $ ChangeSummary { configChanged = configChanged'+  return $ ChangeSummary { configChanged = configChanged' || forceAll                          , postsChanged = map postFilename modifiedPosts-                         , templatesChanged = or templateChanges-                         , postIndexChanged = postIndexChanged'+                         , templatesChanged = or (forceAll : templateChanges)+                         , postIndexChanged = postIndexChanged' || forceAll                          }
src/Main.hs view
@@ -1,51 +1,17 @@ module Main where  import Control.Applicative-    ( (<*>)-    , pure-    ) import Control.Monad-    ( when-    , forM_-    , forM-    ) import Control.Concurrent-    ( threadDelay-    )+import Data.Maybe import System.IO-    ( IOMode(WriteMode)-    , Handle-    , openFile-    , hPutStr-    , hClose-    ) import System.Exit-    ( exitFailure-    )-import System.Environment-    ( getEnvironment-    , getArgs-    ) import System.Directory-    ( doesDirectoryExist-    , doesFileExist-    , removeFile-    , copyFile-    , createDirectory-    ) import System.FilePath-    ( (</>)-    ) import System.Posix.Files     ( createSymbolicLink     )-import Data.Maybe-    ( isNothing-    ) import Data.Time.LocalTime-    ( TimeZone(timeZoneName)-    , getCurrentTimeZone-    ) import qualified Text.Pandoc as Pandoc import qualified MB.Config as Config import MB.Templates@@ -53,8 +19,6 @@     , renderTemplate     ) import MB.Processing-    ( processPost-    ) import MB.Util     ( copyTree     , toLocalTime@@ -64,62 +28,67 @@     , serializePostIndex     , summarizeChanges     )-import MB.Gladtex-    ( gladTex-    , checkForGladtex-    ) 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" -baseDirEnvName :: String-baseDirEnvName = "MB_BASE_DIR"- configFilename :: String configFilename = "blog.cfg" -commonTemplateAttrs :: Config -> [(String, String)]-commonTemplateAttrs config = [ ( "baseUrl", baseUrl config )-                             , ( "title", title config )-                             , ( "authorName", authorName config )-                             , ( "authorEmail", authorEmail config )-                             ]+commonTemplateAttrs :: Blog -> [(String, String)]+commonTemplateAttrs blog =+    [ ( "baseUrl", baseUrl blog )+    , ( "title", title blog )+    , ( "authorName", authorName blog )+    , ( "authorEmail", authorEmail blog )+    , ( "extraPageHead", extraPageHead blog )+    ] -fillTemplate :: Config -> Template -> [(String, String)] -> String-fillTemplate config t attrs = renderTemplate attrs' t-    where attrs' = commonTemplateAttrs config ++ attrs+extraPageHead :: Blog -> String+extraPageHead b = concat $ catMaybes $ pageHead <$> processors b -writeTemplate :: Config -> Handle -> Template -> [(String, String)] -> IO ()-writeTemplate config h t attrs = hPutStr h $ fillTemplate config t attrs+fillTemplate :: Blog -> Template -> [(String, String)] -> String+fillTemplate blog t attrs = renderTemplate attrs' t+    where attrs' = commonTemplateAttrs blog ++ attrs -pandocWriterOptions :: Pandoc.WriterOptions-pandocWriterOptions =-    Pandoc.defaultWriterOptions { Pandoc.writerHTMLMathMethod = Pandoc.GladTeX-                                }+writeTemplate :: Blog -> Handle -> Template -> [(String, String)] -> IO ()+writeTemplate blog h t attrs = hPutStr h $ fillTemplate blog t attrs -writePost :: Handle -> Post -> IO ()-writePost h post = do+writePost :: Blog -> Handle -> Post -> IO ()+writePost blog h post = do+  let writerOpts = getWriterOptions blog Pandoc.defaultWriterOptions   created <- postModificationString post-  hPutStr h $ "<h1>" ++ postTitle post 175 ++ "</h1>"+  hPutStr h $ "<h1>" ++ getPostTitle blog post BlogPost ++ "</h1>"   hPutStr h $ "<span class=\"post-created\">Posted " ++ created ++ "</span>"-  hPutStr h $ Pandoc.writeHtmlString pandocWriterOptions (postAst post)+  hPutStr h $ Pandoc.writeHtmlString writerOpts (postAst post) -buildLinks :: Config -> Maybe Post -> Maybe Post -> String-buildLinks config prev next =+buildLinks :: Blog -> Maybe Post -> Maybe Post -> String+buildLinks _blog prev next =     "<div id=\"prev-next-links\">"-      ++ link "next-link" "older" next-      ++ link "prev-link" "newer" prev+      ++ link "next-link" "older &raquo;" next+      ++ link "prev-link" "&laquo; 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 config p +++              "<a class=\"" ++ cls ++ "\" href=\"" ++ Files.postUrl p ++                                 "\">" ++ name ++ "</a>"  jsInfo :: Post -> String@@ -131,9 +100,9 @@     "};\n" ++     "</script>\n" -buildPage :: Handle -> Config -> String -> Maybe String -> IO ()-buildPage h config content extraTitle = do-  eTmpl <- loadTemplate $ Files.pageTemplatePath config+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@@ -142,77 +111,67 @@           let attrs = [ ("content", content)                       ] ++ maybe [] (\t -> [("extraTitle", t)]) extraTitle -          writeTemplate config h tmpl attrs+          writeTemplate blog h tmpl attrs           hClose h -buildPost :: Handle -> Config -> Post -> (Maybe Post, Maybe Post) -> IO ()-buildPost h config post prevNext = do-  eTmpl <- loadTemplate $ Files.postTemplatePath config+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 config post+          html <- readFile $ Files.postIntermediateHtml blog post            let attrs = [ ("post", html)-                      , ("nextPrevLinks", uncurry (buildLinks config) prevNext)+                      , ("nextPrevLinks", uncurry (buildLinks blog) prevNext)                       , ("jsInfo", jsInfo post)                       ] -          let out = (fillTemplate config tmpl attrs)-          buildPage h config out $ Just $ postTitleRaw post--generatePost :: Config -> Post -> ChangeSummary -> IO ()-generatePost config post summary = do-  let tempHtml = htmlTempDir config </> Files.postBaseName post ++ ".html"-      finalHtml = Files.postIntermediateHtml config post+          let out = (fillTemplate blog tmpl attrs)+          buildPage h blog out $ Just $ getRawPostTitle blog post -  let generate = (postFilename post `elem` (postsChanged summary))+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 -    h <- openFile (Files.postHtex config post) WriteMode-    writePost h =<< processPost config 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+    newPost <- applyPreProcessors blog post -    -- Remove the temporary file.-    removeFile $ Files.postHtex config post-    removeFile tempHtml+    h <- openFile finalHtml WriteMode+    writePost blog h newPost+    hClose h+    applyPostProcessors blog finalHtml BlogPost -generatePosts :: Config -> ChangeSummary -> IO ()-generatePosts config summary = do+generatePosts :: Blog -> ChangeSummary -> IO ()+generatePosts blog summary = do   let numRegenerated = if configChanged summary-                       then length $ blogPosts config+                       then length $ blogPosts blog                        else length $ postsChanged summary   when (numRegenerated > 0) $ putStrLn $ "Rendering " ++ (show numRegenerated) ++ " post(s)..."    let n = length posts-      posts = blogPosts config+      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 config p summary-        h <- openFile (Files.postFinalHtml config p) WriteMode-        buildPost h config p (prevPost, nextPost)+        generatePost blog p summary+        h <- openFile (Files.postFinalHtml blog p) WriteMode+        buildPost h blog p (prevPost, nextPost)         hClose h -generateIndex :: Config -> IO ()-generateIndex config = do-  let dest = Files.postFinalHtml config post-      index = Files.indexHtml config-      post = head $ blogPosts config+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@@ -225,17 +184,17 @@   localTime <- toLocalTime $ postModificationTime p   return $ show localTime ++ "  " ++ timeZoneName tz -generateList :: Config -> IO ()-generateList config = do+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 config) $ \p ->+  entries <- forM (blogPosts blog) $ \p ->              do                created <- postModificationString p                return $ concat [ "<div class=\"listing-entry\"><span class=\"post-title\">"-                               , "<a href=\"" ++ Files.postUrl config p ++ "\">"-                               , postTitle p 110+                               , "<a href=\"" ++ Files.postUrl p ++ "\">"+                               , getPostTitle blog p Index                                , "</a></span><span class=\"post-created\">Posted "                                , created                                , "</span></div>\n"@@ -243,59 +202,58 @@    let content = "<div id=\"all-posts\">" ++ concat entries ++ "</div>" -  h <- openFile (Files.listHtex config) WriteMode-  buildPage h config content Nothing+  h <- openFile (Files.listHtml blog) WriteMode+  buildPage h blog content Nothing   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+  applyPostProcessors blog (Files.listHtml blog) Index -rssItem :: Config -> Post -> String-rssItem config p =+rssItem :: Blog -> Post -> String+rssItem blog p =     concat [ "<item>"-           , "<title>" ++ postTitleRaw p ++ "</title>\n"-           , "<link>" ++ Files.postUrl config p ++ "</link>\n"+           , "<title>" ++ getRawPostTitle blog p ++ "</title>\n"+           , "<link>" ++ baseUrl blog ++ Files.postUrl p ++ "</link>\n"            , "<pubDate>" ++ rssModificationTime p ++ "</pubDate>\n"-           , "<guid>" ++ Files.postUrl config p ++ "</guid>\n"+           , "<guid>" ++ baseUrl blog ++ Files.postUrl p ++ "</guid>\n"            , "</item>\n"            ] -generateRssFeed :: Config -> IO ()-generateRssFeed config = do-  h <- openFile (Files.rssXml config) WriteMode+generateRssFeed :: Blog -> IO ()+generateRssFeed blog = do+  h <- openFile (Files.rssXml blog) WriteMode -  eTmpl <- loadTemplate $ Files.rssTemplatePath config+  eTmpl <- loadTemplate $ Files.rssTemplatePath blog    case eTmpl of     Left msg -> putStrLn msg >> exitFailure     Right tmpl ->         do-          let items = map (rssItem config) $ blogPosts config+          let items = map (rssItem blog) $ blogPosts blog               itemStr = concat items               attrs = [ ("items", itemStr)                       ] -          writeTemplate config h tmpl attrs+          writeTemplate blog h tmpl attrs           hClose h -setup :: FilePath -> IO ()-setup dir = do-  exists <- doesDirectoryExist dir+initializeDataDir :: FilePath -> IO ()+initializeDataDir dir = do+  existsBase <- doesDirectoryExist dir+  existsConfig <- doesFileExist $ dir </> configFilename+   dataDir <- skelDir -  when (not exists) $ do+  when (not existsBase || not existsConfig) $ do           putStrLn $ "Setting up data directory using skeleton: " ++ dataDir+          when (not existsBase) $ createDirectory dir           copyTree dataDir dir -ensureDirs :: Config -> IO ()-ensureDirs config = do+  when existsConfig $+       putStrLn $ "Data directory already initialized; found " +++                    (dir </> configFilename)++ensureDirs :: Blog -> IO ()+ensureDirs blog = do   let dirs = [ postSourceDir              , htmlDir              , stylesheetDir@@ -307,14 +265,11 @@              , eqPreamblesDir              ] -  forM_ (dirs <*> pure config) $ \d ->+  forM_ (dirs <*> pure blog) $ \d ->       do         exists <- doesDirectoryExist d         when (not exists) $ createDirectory d -preserveM :: (Monad m) => (a -> m b) -> a -> m (a, b)-preserveM act val = act val >>= \r -> return (val, r)- scanForChanges :: FilePath -> (FilePath -> IO Bool) -> IO () scanForChanges dir act = do   scan@@ -325,8 +280,20 @@           threadDelay $ 1 * 1000 * 1000           scan -mkConfig :: FilePath -> IO Config-mkConfig base = do+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 @@ -351,44 +318,57 @@   let postSrcDir = base </> "posts"   allPosts <- loadPostIndex postSrcDir -  return $ Config { 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-                  }+  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 -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 ""-  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."+      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 -regenerateContent :: FilePath -> IO Bool-regenerateContent dir = do-  config <- mkConfig dir-  summary <- summarizeChanges config+      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 config+      putStrLn $ "Blog directory: " ++ baseDir blog        when (configChanged summary) $            putStrLn "Configuration file changed; regenerating all content."@@ -401,43 +381,27 @@       when (postIndexChanged summary) $            putStrLn "Post index changed; regenerating next/previous links." -      generatePosts config summary+      generatePosts blog summary -      generateIndex config-      generateList config-      generateRssFeed config+      linkIndexPage blog+      generatePostList blog+      generateRssFeed blog -      writeFile (Files.postIndex config) $-                serializePostIndex $ blogPosts config+      writeFile (Files.postIndex blog) $+                serializePostIndex $ blogPosts blog        putStrLn "Done."       return True-    False -> return False  main :: IO () main = do-  env <- getEnvironment-  args <- getArgs--  checkForGladtex--  let mBase = lookup baseDirEnvName env--  when (isNothing mBase) $ usage >> exitFailure--  let Just dir = mBase--  when (head dir /= '/') $ do-         putStrLn $ baseDirEnvName ++ " must contain an absolute path"-         exitFailure+  conf <- startupConfigFromEnv+  let dir = dataDirectory conf -  setup dir-  config <- mkConfig dir-  ensureDirs config+  when (initDataDirectory conf) $ initializeDataDir dir -  case args of-    [] -> do-         didWork <- regenerateContent dir+  case listenMode conf of+    False -> do+         didWork <- regenerateContent (forceRegeneration conf) dir          when (not didWork) $ putStrLn "No changes found!"-    ["-l"] -> scanForChanges dir regenerateContent-    _ -> usage >> exitFailure+    True -> scanForChanges dir (regenerateContent False)
+ tests/src/InitTests.hs view
@@ -0,0 +1,9 @@+module InitTests where++import Test.Framework+    ( testGroup+    , Test)++tests :: Test+tests = testGroup "blog initialization tests" [+        ]
+ tests/src/MB/StartupTests.hs view
@@ -0,0 +1,34 @@+module MB.StartupTests where++import Test.Framework+    ( testGroup+    , Test)+import Test.Framework.Providers.HUnit+import Test.HUnit+    ( (@=?), assertFailure )++import MB.Startup++tests :: Test+tests = testGroup "Startup configuration tests" [+         testGroup "Base directory tests" $ map testBaseDir [+                      -- (desc, cmdline, env, base dir result)+                         ("base dir in environment only"+                         , [], [(baseDirEnvName, "foo")], "foo")+                       , ("base dir on command line only"+                         , ["--" ++ baseDirParamName, "foo"], [], "foo")+                       , ("base dir in both places, command line takes precedence"+                         , ["--" ++ baseDirParamName, "foo"], [(baseDirEnvName, "bar")], "foo")+                       ]+        , testCase "No base directory" $ Nothing @=? startupConfig [] []+        ]++testBaseDir :: (String, [String], [(String, String)], FilePath) -> Test+testBaseDir (desc, args, env, result) =+    testCase desc assert+        where mConfig = startupConfig args env+              assert = case mConfig of+                         Nothing -> assertFailure "could not construct configuration"+                         Just cfg -> result @=? dataDirectory cfg++
+ tests/src/Main.hs view
@@ -0,0 +1,11 @@+module Main where++import qualified MB.StartupTests as StartupTests+import qualified InitTests as InitTests++import Test.Framework ( defaultMain )++main :: IO ()+main = defaultMain [ StartupTests.tests+                   , InitTests.tests+                   ]