diff --git a/README b/README
deleted file mode 100644
--- a/README
+++ /dev/null
@@ -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
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -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>)
diff --git a/mathblog.cabal b/mathblog.cabal
--- a/mathblog.cabal
+++ b/mathblog.cabal
@@ -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
diff --git a/skel/html/images/newer-subdued.png b/skel/html/images/newer-subdued.png
deleted file mode 100644
Binary files a/skel/html/images/newer-subdued.png and /dev/null differ
diff --git a/skel/html/images/newer.png b/skel/html/images/newer.png
deleted file mode 100644
Binary files a/skel/html/images/newer.png and /dev/null differ
diff --git a/skel/html/images/older-subdued.png b/skel/html/images/older-subdued.png
deleted file mode 100644
Binary files a/skel/html/images/older-subdued.png and /dev/null differ
diff --git a/skel/html/images/older.png b/skel/html/images/older.png
deleted file mode 100644
Binary files a/skel/html/images/older.png and /dev/null differ
diff --git a/skel/html/stylesheets/stylesheet.css b/skel/html/stylesheets/stylesheet.css
--- a/skel/html/stylesheets/stylesheet.css
+++ b/skel/html/stylesheets/stylesheet.css
@@ -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;
 }
diff --git a/skel/posts/first-post.txt b/skel/posts/first-post.txt
--- a/skel/posts/first-post.txt
+++ b/skel/posts/first-post.txt
@@ -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
diff --git a/skel/templates/pageTemplate.html b/skel/templates/pageTemplate.html
--- a/skel/templates/pageTemplate.html
+++ b/skel/templates/pageTemplate.html
@@ -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">
diff --git a/src/MB/Files.hs b/src/MB/Files.hs
--- a/src/MB/Files.hs
+++ b/src/MB/Files.hs
@@ -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"
diff --git a/src/MB/Gladtex.hs b/src/MB/Gladtex.hs
--- a/src/MB/Gladtex.hs
+++ b/src/MB/Gladtex.hs
@@ -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
diff --git a/src/MB/Gnuplot.hs b/src/MB/Gnuplot.hs
new file mode 100644
--- /dev/null
+++ b/src/MB/Gnuplot.hs
@@ -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
+                                                       , "\">"
+                                                       ]
+                           ]
diff --git a/src/MB/Mathjax.hs b/src/MB/Mathjax.hs
new file mode 100644
--- /dev/null
+++ b/src/MB/Mathjax.hs
@@ -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"
diff --git a/src/MB/Processing.hs b/src/MB/Processing.hs
--- a/src/MB/Processing.hs
+++ b/src/MB/Processing.hs
@@ -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
diff --git a/src/MB/Startup.hs b/src/MB/Startup.hs
new file mode 100644
--- /dev/null
+++ b/src/MB/Startup.hs
@@ -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
diff --git a/src/MB/Tikz.hs b/src/MB/Tikz.hs
new file mode 100644
--- /dev/null
+++ b/src/MB/Tikz.hs
@@ -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
diff --git a/src/MB/Types.hs b/src/MB/Types.hs
--- a/src/MB/Types.hs
+++ b/src/MB/Types.hs
@@ -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
diff --git a/src/MB/Util.hs b/src/MB/Util.hs
--- a/src/MB/Util.hs
+++ b/src/MB/Util.hs
@@ -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
                          }
diff --git a/src/Main.hs b/src/Main.hs
--- a/src/Main.hs
+++ b/src/Main.hs
@@ -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)
diff --git a/tests/src/InitTests.hs b/tests/src/InitTests.hs
new file mode 100644
--- /dev/null
+++ b/tests/src/InitTests.hs
@@ -0,0 +1,9 @@
+module InitTests where
+
+import Test.Framework
+    ( testGroup
+    , Test)
+
+tests :: Test
+tests = testGroup "blog initialization tests" [
+        ]
diff --git a/tests/src/MB/StartupTests.hs b/tests/src/MB/StartupTests.hs
new file mode 100644
--- /dev/null
+++ b/tests/src/MB/StartupTests.hs
@@ -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
+
+
diff --git a/tests/src/Main.hs b/tests/src/Main.hs
new file mode 100644
--- /dev/null
+++ b/tests/src/Main.hs
@@ -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
+                   ]
