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 +0/−350
- README.md +470/−0
- mathblog.cabal +57/−27
- skel/html/images/newer-subdued.png binary
- skel/html/images/newer.png binary
- skel/html/images/older-subdued.png binary
- skel/html/images/older.png binary
- skel/html/stylesheets/stylesheet.css +7/−17
- skel/posts/first-post.txt +11/−16
- skel/templates/pageTemplate.html +4/−3
- src/MB/Files.hs +17/−23
- src/MB/Gladtex.hs +52/−13
- src/MB/Gnuplot.hs +110/−0
- src/MB/Mathjax.hs +58/−0
- src/MB/Processing.hs +43/−93
- src/MB/Startup.hs +113/−0
- src/MB/Tikz.hs +102/−0
- src/MB/Types.hs +40/−22
- src/MB/Util.hs +11/−28
- src/Main.hs +175/−211
- tests/src/InitTests.hs +9/−0
- tests/src/MB/StartupTests.hs +34/−0
- tests/src/Main.hs +11/−0
− 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 »" next+ ++ link "prev-link" "« newer" prev ++ "</div>" where link cls name Nothing = "<span class=\"" ++ cls ++ "-subdued\">" ++ name ++ "</span>" link cls name (Just p) =- "<a class=\"" ++ cls ++ "\" href=\"" ++ Files.postUrl 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+ ]