on master help sign in
Merge branch 'hakyll3'

Conflicts: hakyll.cabal src/Text/Hakyll/Tags.hs

commit90b25105830d6e4b0943ab55f9317bd142533acf authorJasper Van der Jeugt <jaspervdj@gmail.com> dateTue 1 Mar 2011, 14:50 (16 years ago) parent8ef5a3e8b727b9
111 files changed+4356−3908
.ghci+1−1 README.markdown+1−6 data/templates/atom-item.xml+5−5 data/templates/atom.xml+7−7 data/templates/rss-item.xml+5−5 data/templates/rss.xml+6−6 examples/brochure/hakyll.hs+18−13 examples/brochure/templates/default.html+8−8 examples/hakyll/changelog.markdown+4 examples/hakyll/css/default.css+4 examples/hakyll/examples/brochure.zip examples/hakyll/hakyll.hs+44−33 examples/hakyll/images/arrow-composition.png examples/hakyll/images/tutorial8-categories.png examples/hakyll/images/tutorial8-tags.png examples/hakyll/index.markdown+13−7 examples/hakyll/sidebar.markdown+6−7 examples/hakyll/templates/default.html+7−7 examples/hakyll/tutorial.markdown+236 examples/hakyll/tutorials/part01.markdown−235 examples/hakyll/tutorials/part02.markdown−91 examples/hakyll/tutorials/part03.markdown−174 examples/hakyll/tutorials/part04.markdown−60 examples/hakyll/tutorials/part05.markdown−157 examples/hakyll/tutorials/part06.markdown−84 examples/hakyll/tutorials/part07.markdown−221 examples/hakyll/tutorials/part08.markdown−97 examples/hakyll/tutorials/part09.markdown−104 hakyll.cabal+84−42 src-inotify/Hakyll/Web/Preview/Poll.hs+51 src-interval/Hakyll/Web/Preview/Poll.hs+36 src/Hakyll.hs+55 src/Hakyll/Core/CompiledItem.hs+45 src/Hakyll/Core/Compiler.hs+333 src/Hakyll/Core/Compiler/Internal.hs+146 src/Hakyll/Core/Configuration.hs+44 src/Hakyll/Core/CopyFile.hs+29 src/Hakyll/Core/DirectedGraph.hs+85 src/Hakyll/Core/DirectedGraph/DependencySolver.hs+70 src/Hakyll/Core/DirectedGraph/Dot.hs+30 src/Hakyll/Core/DirectedGraph/Internal.hs+43 src/Hakyll/Core/Identifier.hs+59 src/Hakyll/Core/Identifier/Pattern.hs+160 src/Hakyll/Core/Logger.hs+90 src/Hakyll/Core/ResourceProvider.hs+75 src/Hakyll/Core/ResourceProvider/FileResourceProvider.hs+29 src/Hakyll/Core/Routes.hs+136 src/Hakyll/Core/Rules.hs+161 src/Hakyll/Core/Rules/Internal.hs+75 src/Hakyll/Core/Run.hs+207 src/Hakyll/Core/Store.hs+88 src/Hakyll/Core/UnixFilter.hs+76 src/Hakyll/Core/Util/Arrow.hs+25 src/Hakyll/Core/Util/File.hs+90 src/Hakyll/Core/Util/String.hs+48 src/Hakyll/Core/Writable.hs+22 src/Hakyll/Main.hs+113 src/Text/Hakyll/Internal/CompressCss.hs → src/Hakyll/Web/CompressCss.hs+23−8 src/Hakyll/Web/Feed.hs+124 src/Hakyll/Web/FileType.hs+55 src/Hakyll/Web/Page.hs+124 src/Hakyll/Web/Page/Internal.hs+50 src/Hakyll/Web/Page/Metadata.hs+131 src/Hakyll/Web/Page/Read.hs+60 src/Hakyll/Web/Pandoc.hs+110 src/Hakyll/Web/Preview/Server.hs+72 src/Hakyll/Web/RelativizeUrls.hs+62 src/Hakyll/Web/Tags.hs+180 src/Hakyll/Web/Template.hs+109 src/Hakyll/Web/Template/Internal.hs+45 src/Hakyll/Web/Template/Read.hs+10 src/Hakyll/Web/Template/Read/Hakyll.hs+35 src/Hakyll/Web/Template/Read/Hamlet.hs+46 src/Hakyll/Web/Util/Url.hs+30 src/Network/Hakyll/SimpleServer.hs−215 src/Text/Hakyll.hs−185 src/Text/Hakyll/Configurations/Static.hs−59 src/Text/Hakyll/Context.hs−16 src/Text/Hakyll/ContextManipulations.hs−124 src/Text/Hakyll/CreateContext.hs−114 src/Text/Hakyll/Feed.hs−112 src/Text/Hakyll/File.hs−196 src/Text/Hakyll/HakyllAction.hs−98 src/Text/Hakyll/HakyllMonad.hs−99 src/Text/Hakyll/Internal/Cache.hs−53 src/Text/Hakyll/Internal/FileType.hs−49 src/Text/Hakyll/Internal/Template.hs−86 src/Text/Hakyll/Internal/Template/Hamlet.hs−56 src/Text/Hakyll/Internal/Template/Template.hs−34 src/Text/Hakyll/Page.hs−108 src/Text/Hakyll/Paginate.hs−94 src/Text/Hakyll/Pandoc.hs−57 src/Text/Hakyll/Regex.hs−77 src/Text/Hakyll/Render.hs−126 src/Text/Hakyll/Tags.hs−172 src/Text/Hakyll/Util.hs−34 tests/CompressCss.hs−42 tests/File.hs−62 tests/Hakyll/Core/DirectedGraph/Tests.hs+36 tests/Hakyll/Core/Identifier/Tests.hs+22 tests/Hakyll/Core/Routes/Tests.hs+24 tests/Hakyll/Web/Page/Tests.hs+32 tests/Hakyll/Web/RelativizeUrls/Tests.hs+20 tests/Hakyll/Web/Template/Tests.hs+44 tests/Main.hs−22 tests/Page.hs−92 tests/Regex.hs−24 tests/Template.hs−70 tests/TestSuite.hs+26 tests/TestSuite/Util.hs+16 tests/Util.hs−54
.ghci+1−1
@@ -1 +1 @@
1−:set -isrc -itests -idist/build/autogen
1+:set -isrc -isrc-inotify -itests -idist/build/autogen
README.markdown+1−6
@@ -1,11 +1,6 @@
11# Hakyll
22
3−Hakyll is a simple static site generator library in Haskell. It is mostly
4−inspired by [Jekyll](http://github.com/mojombo/jekyll), but I like to
5−believe it is simpler. An example site where it is used is
6−[my personal homepage](http://jaspervdj.be) of which
7−[the source code](http://github.com/jaspervdj/jaspervdj) is available here on
8−github as well, as a reference.
3+Hakyll is a static site generator library in Haskell.
94
105## Installation
116
data/templates/atom-item.xml+5−5
@@ -1,7 +1,7 @@
11<entry>
2− <title>$title</title>
3− <link href="$absolute/$url" />
4− <id>$absolute/$url</id>
5− <updated>$timestamp</updated>
6− <summary type="html"><![CDATA[$description]]></summary>
2+ <title>$title$</title>
3+ <link href="$root$$url$" />
4+ <id>$root$$url$</id>
5+ <updated>$timestamp$</updated>
6+ <summary type="html"><![CDATA[$description$]]></summary>
77</entry>
data/templates/atom.xml+7−7
@@ -1,12 +1,12 @@
11<?xml version="1.0" encoding="utf-8"?>
22<feed xmlns="http://www.w3.org/2005/Atom">
3− <title>$title</title>
4− <link href="$absolute/$url" rel="self" />
5− <link href="$absolute" />
6− <id>$absolute/$url</id>
3+ <title>$title$</title>
4+ <link href="$root$$url$" rel="self" />
5+ <link href="$root$" />
6+ <id>$root$$url$</id>
77 <author>
8− <name>$authorName</name>
8+ <name>$authorName$</name>
99 </author>
10− <updated>$timestamp</updated>
11− $body
10+ <updated>$timestamp$</updated>
11+ $body$
1212</feed>
data/templates/rss-item.xml+5−5
@@ -1,7 +1,7 @@
11<item>
2− <title>$title</title>
3− <link>$absolute/$url</link>
4− <description><![CDATA[$description]]></description>
5− <pubDate>$timestamp</pubDate>
6− <guid>$absolute/$url</guid>
2+ <title>$title$</title>
3+ <link>$root$$url$</link>
4+ <description><![CDATA[$description$]]></description>
5+ <pubDate>$timestamp$</pubDate>
6+ <guid>$root$$url$</guid>
77</item>
data/templates/rss.xml+6−6
@@ -1,12 +1,12 @@
11<?xml version="1.0" encoding="utf-8"?>
22<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
33 <channel>
4− <title>$title</title>
5− <link>$absolute</link>
6− <description><![CDATA[$description]]></description>
7− <atom:link href="$absolute/$url" rel="self"
4+ <title>$title$</title>
5+ <link>$root$</link>
6+ <description><![CDATA[$description$]]></description>
7+ <atom:link href="$root$$url$" rel="self"
88 type="application/rss+xml" />
9− <lastBuildDate>$timestamp</lastBuildDate>
10− $body
9+ <lastBuildDate>$timestamp$</lastBuildDate>
10+ $body$
1111 </channel>
1212</rss>
examples/brochure/hakyll.hs+18−13
@@ -1,13 +1,18 @@
1−import Text.Hakyll (hakyll)
2−import Text.Hakyll.File (directory)
3−import Text.Hakyll.Render (css, static, renderChain)
4−import Text.Hakyll.CreateContext (createPage)
5−
6−main = hakyll "http://example.com" $ do
7− directory css "css"
8− render "about.rst"
9− render "index.markdown"
10− render "code.lhs"
11− where
12− render = renderChain ["templates/default.html"]
13− . createPage
1+{-# LANGUAGE OverloadedStrings #-}
2+import Control.Arrow ((>>>))
3+import Control.Monad (forM_)
4+
5+import Hakyll
6+
7+main :: IO ()
8+main = hakyll $ do
9+ route "css/*" idRoute
10+ compile "css/*" compressCssCompiler
11+
12+ compile "templates/*" templateCompiler
13+
14+ forM_ ["about.rst", "index.markdown", "code.lhs"] $ \page -> do
15+ route page $ setExtension "html"
16+ compile page $ readPageCompiler
17+ >>> applyTemplateCompiler "templates/default.html"
18+ >>> relativizeUrlsCompiler
examples/brochure/templates/default.html+8−8
@@ -4,18 +4,18 @@
44<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
55 <head>
66 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7− <title>MyAweSomeCompany - $title</title>
8− <link rel="stylesheet" type="text/css" href="$root/css/default.css" />
9− <link rel="stylesheet" type="text/css" href="$root/css/syntax.css" />
7+ <title>MyAweSomeCompany - $title$</title>
8+ <link rel="stylesheet" type="text/css" href="/css/default.css" />
9+ <link rel="stylesheet" type="text/css" href="/css/syntax.css" />
1010 </head>
1111 <body>
12− <h1>MyAweSomeCompany - $title</h1>
12+ <h1>MyAweSomeCompany - $title$</h1>
1313 <div id="navigation">
14− <a href="$root/index.html">Home</a>
15− <a href="$root/about.html">About</a>
16− <a href="$root/code.html">Code</a>
14+ <a href="/index.html">Home</a>
15+ <a href="/about.html">About</a>
16+ <a href="/code.html">Code</a>
1717 </div>
1818
19− $body
19+ $body$
2020 </body>
2121</html>
examples/hakyll/changelog.markdown+4
@@ -2,6 +2,10 @@
22title: Changelog
33---
44
5+## Hakyll 3
6+
7+- Complete rewrite
8+
59## Hakyll 2.4.1
610
711- Add a number of utility functions
examples/hakyll/css/default.css+4
@@ -75,6 +75,10 @@ h3 {
7575 text-transform: uppercase;
7676}
7777
78+h1 a, h2 a, h3 a {
79+ text-decoration: none;
80+}
81+
7882div.column {
7983 width: 50%;
8084 float: left;
examples/hakyll/examples/brochure.zip

Binary file not shown.

examples/hakyll/hakyll.hs+44−33
@@ -1,39 +1,50 @@
1−import Text.Hakyll
2−import Text.Hakyll.Render
3−import Text.Hakyll.File
4−import Text.Hakyll.CreateContext
5−import Control.Monad.Reader (liftIO)
6−import System.Directory
7−import Control.Monad (mapM_, forM_, liftM)
8−import Data.List (sort)
1+{-# LANGUAGE OverloadedStrings #-}
2+import Hakyll
3+import Control.Monad (forM_)
4+import Control.Arrow ((>>>), arr)
5+import Text.Pandoc
96
10−main = hakyll "http://jaspervdj.be/hakyll" $ do
11− directory css "css"
12− directory static "images"
13− directory static "examples"
14− directory static "reference"
7+main :: IO ()
8+main = hakyll $ do
9+ route "css/*" idRoute
10+ compile "css/*" compressCssCompiler
1511
16− tutorials <- liftM sort $ getRecursiveContents "tutorials"
17− let tutorialPage = createListing "tutorials.html"
18− ["templates/tutorialitem.html"]
19− (map createPage tutorials)
20− [("title", Left "Tutorials")]
21− renderChain ["templates/tutorials.html", "templates/default.html"]
22− (withSidebar tutorialPage)
12+ -- Static directories
13+ forM_ ["images/*", "examples/*", "reference/*"] $ \f -> do
14+ route f idRoute
15+ compile f copyFileCompiler
2316
24− mapM_ (render' ["templates/default.html"]) $
25− [ "about.markdown"
26− , "index.markdown"
27− , "philosophy.markdown"
28− , "reference.markdown"
29− , "changelog.markdown"
30− ]
17+ -- Pages
18+ forM_ pages $ \p -> do
19+ route p $ setExtension "html"
20+ compile p $ pageCompiler
21+ >>> requireA "sidebar.markdown" (setFieldA "sidebar" $ arr pageBody)
22+ >>> applyTemplateCompiler "templates/default.html"
23+ >>> relativizeUrlsCompiler
3124
32− forM_ tutorials $ render' [ "templates/tutorial.html"
33− , "templates/default.html"
34− ]
25+ -- Tutorial
26+ route "tutorial.markdown" $ setExtension "html"
27+ compile "tutorial.markdown" $ readPageCompiler
28+ >>> pageRenderPandocWith defaultHakyllParserState withToc
29+ >>> requireA "sidebar.markdown" (setFieldA "sidebar" $ arr pageBody)
30+ >>> applyTemplateCompiler "templates/default.html"
31+ >>> relativizeUrlsCompiler
3532
33+ -- Sidebar
34+ compile "sidebar.markdown" pageCompiler
35+
36+ -- Templates
37+ compile "templates/*" templateCompiler
3638 where
37− render' templates = renderChain templates . withSidebar . createPage
38− withSidebar a = a `combine` createPage "sidebar.markdown"
39−
39+ withToc = defaultHakyllWriterOptions
40+ { writerTableOfContents = True
41+ , writerTemplate = "<h2>Table of contents</h2>\n$toc$\n$body$"
42+ , writerStandalone = True
43+ }
44+
45+ pages = [ "about.markdown"
46+ , "changelog.markdown"
47+ , "index.markdown"
48+ , "philosophy.markdown"
49+ , "reference.markdown"
50+ ]
examples/hakyll/images/arrow-composition.pngdeleted

Binary file not shown.

examples/hakyll/images/tutorial8-categories.pngdeleted

Binary file not shown.

examples/hakyll/images/tutorial8-tags.pngdeleted

Binary file not shown.

examples/hakyll/index.markdown+13−7
@@ -2,6 +2,12 @@
22title: Home
33---
44
5+## Hakyll 3
6+
7+Hakyll 3 has been released, and it can be installed by running
8+`cabal install hakyll`. For a limited time (but as long as is necessary) you can
9+access the old site and documentation [here](/hakyll2)
10+
511## Overview
612
713Hakyll is a [Haskell](http://haskell.org) library for generating static sites,
@@ -34,10 +40,10 @@ Some sites written in Hakyll:
3440
3541## Getting Started
3642
37−You can get the latest version from hackage using `cabal install hakyll`. I
38−have written a few [tutorials](tutorials.html), and reading them is highly
39−recommended if you want to get started with hakyll. Also, there are a few
40−examples available in
41−[the github repo](http://github.com/jaspervdj/Hakyll/tree/master/examples/),
42−including this site. For any questions you might have, there is also a
43−[google discussion group](http://groups.google.com/group/hakyll).
43+You can get the latest version from hackage using `cabal install hakyll`. Then,
44+you can:
45+
46+- read the [tutorial](/tutorial.html);
47+- mail the [google discussion group](http://groups.google.com/group/hakyll);
48+- ask questions on the IRC channel: `#hakyll` on
49+ [freenode](http://freenode.net/).
examples/hakyll/sidebar.markdown+6−7
@@ -1,9 +1,8 @@
1−--- sidebar
21## Navigation
32
4−[home]($root/index.html)
5−[philosophy]($root/philosophy.html)
6−[about]($root/about.html)
7−[tutorials]($root/tutorials.html)
8−[reference]($root/reference.html)
9−[changelog]($root/changelog.html)
3+[home](/index.html)
4+[philosophy](/philosophy.html)
5+[about](/about.html)
6+[tutorial](/tutorial.html)
7+[reference](/reference.html)
8+[changelog](/changelog.html)
examples/hakyll/templates/default.html+7−7
@@ -4,11 +4,11 @@
44<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
55 <head>
66 <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
7− <title>Hakyll - $title</title>
7+ <title>Hakyll - $title$</title>
88
99 <!-- Stylesheets. -->
10− <link rel="stylesheet" type="text/css" href="$root/css/default.css" />
11− <link rel="stylesheet" type="text/css" href="$root/css/syntax.css" />
10+ <link rel="stylesheet" type="text/css" href="/css/default.css" />
11+ <link rel="stylesheet" type="text/css" href="/css/syntax.css" />
1212
1313 <!-- Metadata. -->
1414 <meta name="keywords" content="hakyll,static site generator,static,site,generator,haskell,blog"/>
@@ -17,16 +17,16 @@
1717 <body>
1818 <div id="main">
1919 <div id="header">
20− <img src="$root/images/lambda.png" alt="lambda" />
21− <h1>Hakyll - $title</h1>
20+ <img src="/images/lambda.png" alt="lambda" />
21+ <h1>Hakyll - $title$</h1>
2222 </div>
2323 <!-- Sidebar. -->
2424 <div id="sidebar">
25− $sidebar
25+ $sidebar$
2626 </div>
2727
2828 <div id="content">
29− $body
29+ $body$
3030 </div>
3131
3232 <div id="footer">
examples/hakyll/tutorial.markdownnew file+236
@@ -0,0 +1,236 @@
1+---
2+title: Tutorial
3+---
4+
5+Why static websites?
6+--------------------
7+
8+Modern web frameworks make it easy to create huge dynamic websites. Why would
9+anyone still care about a static website?
10+
11+- Static websites are fast, because it's simply files served directly from the
12+ hard disk.
13+- Static websites are secure. Nobody has ever found an SQL injection in static
14+ pages.
15+- Static websites are easy to deploy. Just copy them to your webhost using
16+ (S)FTP/rsync/scp and you are done. They work on all webhosts: no CGI or extra
17+ modules needed for the web server.
18+
19+Why Hakyll?
20+-----------
21+
22+Hakyll is a [Haskell] library meant for creating small-to-medium sized static
23+websites. It is a powerful publishing tool, precisely because of the power of
24+Haskell. By using the awesome [pandoc] library, it is able to create your
25+website from a large variety of input formats.
26+
27+[Haskell]: http://haskell.org/
28+[pandoc]: http://johnmacfarlane.net/pandoc/
29+
30+Features include:
31+
32+- easy templating system;
33+- a simple HTTP server for previewing and compiling your website on the go;
34+- powerful syntax highlighting;
35+- modules for common items such as tags and feeds;
36+- easily extensible.
37+
38+A simple brochure site
39+----------------------
40+
41+### The two layers
42+
43+Hakyll consists of two important layers:
44+
45+- A top-level declarative eDSL, used to describe the relations between the
46+ different items,
47+- A lower-level filter-like eDSL built on Arrows.
48+
49+Both layer are used in the configuration file of your website. This
50+configuration file is conventionally called `hakyll.hs` and placed at the root
51+of your website directory.
52+
53+### The Rules DSL
54+
55+The Rules DSL is probably the simpler one. Let's look at a very simple example
56+of a `hakyll.hs`. This piece of code might look a little confusing, but don't
57+worry, we'll walk through it in detail.
58+
59+~~~~~{.haskell}
60+{-# LANGUAGE OverloadedStrings #-}
61+import Control.Arrow ((>>>))
62+import Control.Monad (forM_)
63+
64+import Hakyll
65+
66+main :: IO ()
67+main = hakyll $ do
68+ route "css/*" idRoute
69+ compile "css/*" compressCssCompiler
70+
71+ compile "templates/*" templateCompiler
72+
73+ forM_ ["about.rst", "index.markdown", "code.lhs"] $ \page -> do
74+ route page $ setExtension "html"
75+ compile page $ readPageCompiler
76+ >>> applyTemplateCompiler "templates/default.html"
77+ >>> relativizeUrlsCompiler
78+~~~~~
79+
80+This is enough code to create a small brochure site! You can find all code
81+and files necessary to build this site [right here](/examples/brochure.zip)
82+-- feel free to play around with it!
83+
84+To create your site, compile and run your `hakyll.hs`:
85+
86+ [jasper@phoenix] ghc --make hakyll.hs
87+ [jasper@phoenix] ./hakyll preview
88+
89+Alternatively,
90+
91+ [jasper@phoenix] runghc hakyll.hs preview
92+
93+Our code begins with a number of imports. Nothing out of the ordinary here, but
94+do note that we use the `OverloadedStrings` extension for conciseness.
95+
96+~~~~~{.haskell}
97+{-# LANGUAGE OverloadedStrings #-}
98+import Control.Arrow ((>>>))
99+import Control.Monad (forM_)
100+
101+import Hakyll
102+~~~~~
103+
104+Our entry point is simply a `main` function with the type `IO ()` -- as in every
105+other Haskell application. However, we directly wrap it in a `hakyll` function,
106+which marks our declarative eDSL. Inside this function, we no longer operate in
107+the `IO` monad, we operate in the pure `RulesM` monad.
108+
109+~~~~~{.haskell}
110+main :: IO ()
111+main = hakyll $ do
112+~~~~~
113+
114+The `RulesM` monad is composed of a few functions. A first important one is
115+`route`: this creates a new rule for routing items. This rule is applied to all
116+items it matches -- and matching is done using the `"css/*"` [pattern].
117+`idRoute` simply means that an item will be routed to it's own filename. For
118+example, `css/screen.css` will be routed to `css/screen.css` -- not very
119+exciting.
120+
121+Note that a [Pattern] matches an [Identifier], it doesn't match filenames.
122+
123+[Pattern]: /reference/Hakyll-Core-Identifier-Pattern.html
124+[Identifier]: /reference/Hakyll-Core-Identifier.html
125+
126+~~~~~{.haskell}
127+route "css/*" idRoute
128+~~~~~
129+
130+Apart from specifying where the items should go (using `route`), we also have to
131+specify *how* they need to be compiled. This is done using the `compile`
132+function. It takes a `Compiler` as its second argument. These compilers can
133+consist of very complicated constructions, but Hakyll also provides a number of
134+good default compilers. The `compressCssCompiler` compiler will simply compress
135+the CSS found in the files.
136+
137+~~~~~{.haskell}
138+compile "css/*" compressCssCompiler
139+~~~~~
140+
141+Next, we're going to render some pages. We're going to style the results a
142+little, so we're going to need a [Template]. We simply compile a template using
143+the `defaultTemplateRead` compiler, it's good enough in most cases.
144+
145+[Template]: /reference/Hakyll-Web-Template.html
146+
147+We don't use a route for these templates, after all, we don't want to route them
148+anywhere, we just want to use them to style our pages a little.
149+
150+~~~~~{.haskell}
151+compile "templates/*" templateCompiler
152+~~~~~
153+
154+We can conclude that some rules do not *directly* add an output page on our
155+site. In this case, we compile the template so it is available to the compiler
156+later[^1].
157+
158+[^1]: Actually, since the rules DSL is declarative, we could also add the
159+ template compile rule at the bottom -- this would make no difference.
160+
161+Now, it's time to actually render our pages. We use the `forM_` monad combinator
162+so we can describe all files at once (instead of compiling all three files
163+manually).
164+
165+~~~~~{.haskell}
166+forM_ ["about.rst", "index.markdown", "code.lhs"] $ \page -> do
167+~~~~~
168+
169+The pages all have different extensions. In our website, we only want to see
170+`.html` files. Hakyll provides a route to do just that:
171+
172+~~~~~{.haskell}
173+route page $ setExtension "html"
174+~~~~~
175+
176+The [Rules] reference page has a complete listing of the API used.
177+
178+[Rules]: /reference/Hakyll-Core-Rules.html
179+
180+The compilation of our pages is slightly more complicated: we're using another
181+DSL there.
182+
183+### The Compiler DSL
184+
185+The gist of it is that the `Compiler a b` type has two parameters -- it is an
186+Arrow, and we can chain compilers using the `>>>` operator. The [Compiler]
187+reference page has some more readable information on this subject.
188+
189+[Compiler]: /reference/Hakyll-Core-Compiler.html
190+
191+~~~~~{.haskell}
192+compile page $ pageCompiler
193+ >>> applyTemplateCompiler "templates/default.html"
194+ >>> relativizeUrlsCompiler
195+~~~~~
196+
197+Note that we can only use `applyTemplateCompiler` with
198+`"templates/default.html"` because we compiled `"templates/default.html"`. If we
199+didn't list a rule for that item, the compilation would fail (Hakyll would not
200+find the template).
201+
202+Now, let's look at the concrete compiler:
203+
204+- `pageCompiler` starts by reading the [Page], parsing it, and rendering it
205+ using [pandoc].
206+- `applyTemplateCompiler` applies a [Template] which we have already loaded.
207+- `relativizeUrlsCompiler` will [relativize] the URL's so we have a site we can
208+ deploy everywhere.
209+
210+[Page]: /reference/Hakyll-Web-Page.html
211+[relativize]: /reference/Hakyll-Web-RelativizeUrls.html
212+
213+Various tips and tricks
214+-----------------------
215+
216+### Syntax highlighting
217+
218+Syntax highlighting is enabled by default in Hakyll. However, you also need to
219+enable it in pandoc. If no syntax highlighting shows up, try
220+
221+ [jasper@phoenix] cabal install --reinstall -fhighlighting pandoc
222+
223+### When to rebuild
224+
225+If you execute a `./hakyll build`, Hakyll will build your site incrementally.
226+This means it will be very fast, but it will not pick up _all_ changes.
227+
228+- In case you edited `hakyll.hs`, you first want to compile it again.
229+- It is generally recommended to do a `./hakyll rebuild` before you deploy your
230+ site.
231+
232+After rebuilding your site, all files will look as "modified" to the filesystem.
233+This means that when you upload your site, it will usually transfer all files --
234+this can generate more traffic than necessary, since it is possible that some
235+files were not actually modified. If you use `rsync`, you can counter this using
236+the `--checksum` option.
examples/hakyll/tutorials/part01.markdowndeleted−235
@@ -1,235 +0,0 @@
1−---
2−title: Quickstart
3−what: explains how to create a simple brochure site
4−---
5−
6−## Getting started
7−
8−First, make sure you have Hakyll installed. The recommended way to do this is
9−through [hackage] using [cabal-install]. This tutorial also assumes you have a
10−basic knowledge of Haskell.
11−
12−[hackage]: http://hackage.haskell.org/
13−[cabal-install]: http://www.haskell.org/haskellwiki/Cabal-Install
14−
15−~~~~~
16−[jasper@alice ~]$ cabal install hakyll
17−~~~~~
18−
19−## Building a simple static site
20−
21−As an example to get started with, we're going to develop a so called
22−"Brochure Site" for an imaginary company. The first step is to create a
23−directory for our new site.
24−
25−~~~~~
26−[jasper@alice Sites]$ mkdir brochure
27−[jasper@alice Sites]$ cd brochure/
28−[jasper@alice brochure]$
29−~~~~~
30−
31−I have a [zip file] with the files we need for this
32−tutorial available. Please unzip it in the brochure directory we just created.
33−We'll first have a look at what we're going to create (because we're curious
34−and all that).
35−
36−[zip file]: $root/examples/brochure.zip
37−
38−~~~~~
39−[jasper@alice brochure]$ ghc --make hakyll.hs
40−[1 of 1] Compiling Main ( hakyll.hs, hakyll.o )
41−Linking hakyll ...
42−[jasper@alice brochure]$ ./hakyll preview
43−Starting hakyll server on port 8000...
44−~~~~~
45−
46−If you now point your browser at [localhost:8000] you should see our simple
47−brochure site.
48−
49−[localhost:8000]: http://localhost:8000/
50−
51−## hakyll.hs
52−
53−The main configuration file of a Hakyll site is traditionally called
54−`hakyll.hs`. It is nothing special, just a small Haskell program. There is no
55−magic going on.
56−
57−~~~~~{.haskell}
58−import Text.Hakyll (hakyll)
59−import Control.Monad.Trans (liftIO)
60−main = hakyll "http://example.com" $ do
61− liftIO $ putStrLn "I'm in your computer, generating your site!"
62−~~~~~
63−
64−Note how we wrap everything in the `hakyll` function. This is useful because
65−it will generate a very nice main function. We also pass the full site URL to
66−the `hakyll` function. If you don't have an URL for your site yet, it doesn't
67−really matter for now; just fill in anything then. The URL is only used for
68−certain specific purposes where a full URL is needed, such as rendering RSS
69−feeds.
70−
71−## Context
72−
73−Let's look at one of the most important types in Hakyll.
74−
75−~~~~~{.haskell}
76−type Context = Map String String
77−~~~~~
78−
79−A `Context` is a key-value mapping, used to represent pieces of information.
80−One way to write such a `Context`, is a page.
81−
82−## Pages
83−
84−Another important concept in Hakyll is pages. Pages are text files that can be
85−written in markdown, html, rst... basically anything Pandoc supports.
86−Furthermore, they can also contain some metadata. The metadata is placed in the
87−file header and surrounded by `---` lines. Each line should contain a
88−`key: value` pair. Let's have a look at the `index.markdown` page.
89−
90− ---
91− title: About
92− ---
93− Nullam imperdiet sodales orci vitae molestie.
94− Nunc quam orci, pharetra a rhoncus vitae,
95− eleifend id felis. Suspendisse potenti...
96−
97−This contains one `key: value` pair, namely `title: About`. The rest of the
98−file is treated as markdown by pandoc. If you want to know more about
99−markdown, I think [this](http://daringfireball.net/projects/markdown/syntax)
100−is a pretty good page.
101−
102−## Templates
103−
104−Another concept are the so-called templates. Templates are text files (usually
105−html files) containing a number of keys. The syntax for these keys is
106−`$identifier`. Our example site contains one template, namely
107−`templates/default.html`. Let's have a better look at that.
108−
109−~~~~~{.html}
110−<html>
111− <head>
112− <title>MyAweSomeCompany - $title</title>
113− <link rel="stylesheet" type="text/css"
114− href="$$root/css/default.css" />
115− <link rel="stylesheet" type="text/css"
116− href="$$root/css/syntax.css" />
117− </head>
118− <body>
119− <h1>MyAweSomeCompany - $title</h1>
120− <div id="navigation">
121− <a href="$$root/index.html">Home</a>
122− <a href="$$root/about.html">About</a>
123− <a href="$$root/code.html">Code</a>
124− </div>
125−
126− $body
127− </body>
128−</html>
129−~~~~~
130−
131−We can see how our `Page` would fit in. When we render the page we saw using
132−this template, `$title` would be replaced by `About`, and `$body` would be
133−replaced by the body of the about page. `body` is the traditional name for the
134−body of any page - that is the convention in Hakyll. Also note that in this
135−case, `$body` would be replaced by a chunk of html - the result of the
136−markdown-to-html conversion.
137−
138−## The $$root key
139−
140−There are a few "special" keys in Hakyll: one of them is the $$root key. What
141−is so special about it? Well, internally, it is treated differently - but this
142−should not concern you. The thing is that it is the only key you can also use
143−in __Pages__.
144−
145−It will be substituted by a relative url part (like `..` or `../..`) so it
146−points to the root directory of your site. It is recommended to use this
147−whenever you need it, it can save you some time from messing with absolute
148−and relative URL's.
149−
150−## Putting it all together
151−
152−Now, we'll render the page using the `renderChain` function. This function
153−takes a list of templates and a `Context`. In our case, we only have one
154−template, and our `Context` is the about page we just saw - we can load that
155−using the `createPage` function.
156−
157−~~~~~{.haskell}
158−import Text.Hakyll (hakyll)
159−import Text.Hakyll.Render (renderChain)
160−import Text.Hakyll.CreateContext (createPage)
161−main = hakyll "http://example.com" $ do
162− renderChain ["templates/default.html"]
163− (createPage "index.markdown")
164−~~~~~
165−
166−Or, to render all our three pages:
167−
168−~~~~~{.haskell}
169−import Text.Hakyll (hakyll)
170−import Text.Hakyll.Render (renderChain)
171−import Text.Hakyll.CreateContext (createPage)
172−main = hakyll "http://example.com" $ do
173− render "about.rst"
174− render "index.markdown"
175− render "code.lhs"
176− where render = renderChain ["templates/default.html"]
177− . createPage
178−~~~~~
179−
180−As you can see, we can render a variety of formats. This will create the
181−following files:
182−
183−~~~~~
184−_site/about.html
185−_site/index.html
186−_site/code.html
187−~~~~~
188−
189−## CSS, images and other static files
190−
191−Now, we also have a css file we would like to have in the `_site` directory.
192−Static files can be rendered using the `static` function in Hakyll. We could
193−use:
194−
195−~~~~~{.haskell}
196−import Text.Hakyll (hakyll)
197−import Text.Hakyll.Render (static)
198−main = hakyll "http://example.com" $ do
199− static "css/default.css"
200−~~~~~
201−
202−This would work, but let's not forget that Hakyll also has css compression. If
203−we want to use that, we would use `css` instead of `static`.
204−
205−~~~~~{.haskell}
206−import Text.Hakyll (hakyll)
207−import Text.Hakyll.Render (css)
208−main = hakyll "http://example.com" $ do
209− css "css/default.css"
210−~~~~~
211−
212−If we were to create another css file, we would have to add a line to our
213−`hakyll.hs` configuration file. This is pretty stupid, because the whole
214−directory `css` contains only css files. That's why Hakyll has a `directory`
215−function, which will execute a given function on an entire directory. So,
216−our example would become:
217−
218−~~~~~{.haskell}
219−import Text.Hakyll (hakyll)
220−import Text.Hakyll.Render (css)
221−import Text.Hakyll.File (directory)
222−main = hakyll "http://example.com" $ do
223− directory css "css"
224−~~~~~
225−
226−## Deploying
227−
228−To setup your site, simply copy the contents of `_site` to your hosting provider
229−using your favorite piece of software.
230−
231−## The gist of it
232−
233−- You render "pages" with "templates".
234−- The most common render function is `renderChain`.
235−- Hakyll also deals with static files and css.
examples/hakyll/tutorials/part02.markdowndeleted−91
@@ -1,91 +0,0 @@
1−---
2−title: Arrows: A Crash Course
3−what: illustrates how Arrows are used in hakyll
4−---
5−
6−## Do I really need to know this stuff?
7−
8−Maybe. You don't need it when you only use the basic Hakyll functions, but
9−Arrows are used a lot in the Hakyll code, and a lot of the more advanced
10−features make use of Arrows. Besides, it's a pretty interesting subject.
11−
12−## What is an "Arrow"
13−
14−Arrows are comparable with monads. In fact, monads are a subset of arrows.
15−Arrows allow you to represent a "computation". This is all pretty vague, so
16−let's skip directly to the Arrows used in Hakyll.
17−
18−## HakyllAction
19−
20−The Arrow used throughout Hakyll is called `HakyllAction`. Arrows have two
21−type parameters, so it's actually `HakyllAction a b`. You can think of `a`
22−as the input for our action, and `b` is the corresponding output. Let's look
23−at the type of `createPage`:
24−
25−~~~~~{.haskell}
26−createPage :: FilePath -> HakyllAction () Context
27−~~~~~
28−
29−So, you give `createPage` a `FilePath`, and it creates a `HakyllAction` that
30−produces a `Context` out of thin air. Now, we want to render the `Context` we
31−just loaded with a template. The type of the `render` function is:
32−
33−~~~~~{.haskell}
34−render :: FilePath -> HakyllAction Context Context
35−~~~~~
36−
37−We pass the file name of a template to the `render` function, and we get a
38−`HakyllAction` that creates a `Context` from another `Context`. The result
39−of the `render` operation (so basically the rendered template) will be placed
40−in the `$body` field of the new `Context`. But we still haven't saved our
41−result, so let's do that using the `writePage` function.
42−
43−~~~~~{.haskell}
44−writePage :: HakyllAction Context ()
45−~~~~~
46−
47−This function writes our result and returns nothing.
48−
49−## Composition
50−
51−Now, let's look at the big picture.
52−
53−![Arrow illustration]($root/images/arrow-composition.png)
54−
55−If these were regular functions, we could've composed them using the `.`
56−operator. Since they're arrows, we'll have to use the `>>>` operator.
57−
58−~~~~~{.haskell}
59−test :: HakyllAction () ()
60−test = createPage "test.markdown"
61− >>> render "template.html"
62− >>> writePage
63−~~~~~
64−
65−Now, we only have to execute our test.
66−
67−~~~~~{.haskell}
68−runHakyllActionIfNeeded test
69−~~~~~
70−
71−## Aso, the point emerges
72−
73−The `runHakyllActionIfNeeded` suggests why we use arrows. `HakyllAction` is more
74−than just a function, it also tracks dependencies. This causes Hakyll to only
75−execute our functions when it is really needed. In this particular case, `test`
76−would only be executed if either `test.markdown` or `template.html` were
77−recently changed.
78−
79−## So what's renderChain then?
80−
81−Now, we have done pretty much the same as we did with the `renderChain` function
82−in the first part. That's right, the `renderChain` is more or less implemented
83−like this. So, you will probably use `renderChain` in most cases, but it's
84−handy if you know how it works.
85−
86−## The gist of it
87−
88−- Arrows really aren't complicated.
89−- Compose them using `>>>`.
90−- `HakyllAction` tracks dependencies for you. Use it.
91−- In most cases, you will just use `renderChain`.
examples/hakyll/tutorials/part03.markdowndeleted−174
@@ -1,174 +0,0 @@
1−---
2−title: How to write pages
3−what: elaborates a little on writing pages
4−---
5−
6−## The structure of a Page
7−
8−The most important thing to realize is that a page is reduced to a `Context`,
9−and therefore is just a key-value mapping. Another example:
10−
11− ---
12− title: About
13− author: Mia Wallace
14− ---
15− Hello there! This is
16− a simple about page.
17−
18−This will produce the following mapping:
19−
20−- `$title`: About
21−- `$author`: Mia Wallace
22−- `$body`: Hello there! This is a simple about page.
23−
24−`$body` is the traditional name for the main body part of a page. If the page
25−has a `.markdown` extension for example, this would also be rendered by pandoc.
26−But pages are more flexible. The following is also a valid page:
27−
28− Hello there! This is
29− a simple about page.
30−
31−This will produce one key-value pair:
32−
33−- `$body`: Hello there! This is a simple about page.
34−
35−But Hakyll can do more than this. You can add extra sections, apart from the
36−body, and even leave out the body.
37−
38− ---
39− author: Vincent Vega
40−
41− --- prelude
42− A small introduction goes here. I can write *markdown*
43− here, by the way. Well, assuming this page has a
44− `.markdown` extension.
45−
46− --- main
47− I can write some more things here.
48−
49− ---
50− The body comes last, and is optional.
51−
52−This will produce the following:
53−
54−- `$author`: Vincent Vega
55−- `$prelude`: A small introduction goes here. I can write *markdown* here, by the
56− way. Well, assuming this page has a `.markdown` extension.
57−- `$main`: I can write some more things here.
58−- `$body`: The body comes last, and is optional.
59−
60−The example from this tutorial (we will see later) uses this to build a
61−three-column system for the website, separating content from layout.
62−
63−## Combining Contexts
64−
65−Now you know that pages, and `Context`s in general, are basically nothing more
66−than key-values mappings, it is time to abuse this fact. There is another
67−way to create a `Context`, called `combine`.
68−
69−The type signature of the `combine` function does a pretty good job at
70−explaining it:
71−
72−~~~~~{.haskell}
73−combine :: HakyllAction () Context
74− -> HakyllAction () Context
75− -> HakyllAction () Context
76−~~~~~
77−
78−This means we can take two `Context`s values and combine them. This is
79−basically a `Map.union`: The result will contain all keys from both `Context`s,
80−with there corresponding values. If a key is present in both `Context`s, the
81−value from the first argument will be chosen. This is, for example, almost
82−always the case with the `$url` field (since almost all `Context`s have an url
83−in Hakyll).
84−
85−Combining two `Context`s, but overriding the `$url` is quite common, so there is
86−another function that helps us here:
87−
88−~~~~~{.haskell}
89−combineWithUrl :: FilePath
90− -> HakyllAction () Context
91− -> HakyllAction () Context
92− -> HakyllAction () Context
93−~~~~~
94−
95−## The example
96−
97−Now that we have the tools, we'll get on to the example. This time, we'll
98−be making a more advanced brochure site. Here [is a zip file] containing the
99−source code for the tutorial.
100−
101−[is a zip file]: $root/examples/morepages.zip
102−
103−Every page consists of three sections, originally named `section1`, `section2`
104−and `section3`. So our pages look more or less like this:
105−
106− ---
107− title: About
108−
109− --- section1
110− ## Mattis
111− Nullam imperdiet sodales orci vitae molestie. Nunc...
112−
113− --- section2
114− ## Orci
115− Vivamus eget mauris sit amet nulla laoreet lobortis.
116− Nulla in...
117−
118− --- section3
119− ## Augue
120− In urna ante, pulvinar et imperdiet nec, fermentum ac...
121−
122−The cool thing is we do not have to specify how these will be layed out. In our
123−template, we decide to use a simple three column system:
124−
125−~~~~~{.html}
126−<div class="column"> $section1 </div>
127−<div class="column"> $section2 </div>
128−<div class="column"> $section3 </div>
129−~~~~~
130−
131−The columns are then floated using css. So far so good, but what if we wanted
132−an additional text block on every page? An easy solution would be to add this
133−to the template, but then our layout-content separation idea will be broken
134−again. So we simply add to the template:
135−
136−~~~~~{.html}
137−<div class="footer"> $footer </div>
138−~~~~~
139−
140−And now we will use `combine` to put the footer on every page - so we need to
141−add the footer page to every `Context`. We write a small auxiliary function
142−that combines a given `Context` with the footer:
143−
144−~~~~~{.haskell}
145−withFooter = flip combine $ createPage "footer.markdown"
146−~~~~~
147−
148−Note that we use `flip` here - we want `footer.markdown` to be our second
149−argument. That is because Hakyll will take the `$url` from the first `Context`,
150−so all pages would be rendered to `footer.html` - obviously not what we want.
151−Now, were we previously wrote:
152−
153−~~~~~{.haskell}
154−render "about.markdown"
155−where render = renderChain ["templates/default.html"]
156− . createPage
157−~~~~~
158−
159−We simply have to add our footer:
160−
161−~~~~~{.haskell}
162−render "about.markdown"
163−where render = renderChain ["templates/default.html"]
164− . withFooter
165− . createPage
166−~~~~~
167−
168−And now every page will include the footer.
169−
170−## The gist of it
171−
172−- Pages are just key-value mappings.
173−- You can have multiple sections in every page.
174−- Combine pages using the `combine` function.
examples/hakyll/tutorials/part04.markdowndeleted−60
@@ -1,60 +0,0 @@
1−---
2−title: How to write templates
3−what: more information on template writing
4−---
5−
6−## Simple templates
7−
8−Simple templates are simply HTML files, with `$identifiers`. An example:
9−
10−~~~~~{.html}
11−<html>
12− <head>
13− <title>$title</title>
14− </head>
15− <body>
16− $body
17− </body>
18−</html>
19−~~~~~
20−
21−## Markup in templates
22−
23−Most of the examples in these tutorials use HTML for templates. However, since
24−Hakyll 2.2, it is possible use other markup languages in your templates. Simply
25−use an appropriate extension, and Hakyll will pick it up. For example, you could
26−write your `templates/post.markdown` template as:
27−
28− # $title
29−
30− _On $date_
31−
32− $body
33−
34−__Warning__: you shouldn't use markdown for your "root" template, as these
35−templates will never insert things like the doctype for you -- so you always
36−need at least one top-level HTML template.
37−
38−## Hamlet templates
39−
40−From Hakyll 2.3 onwards, it is possible to use [hamlet] templates. You can find
41−more information about hamlet on that website. Usage is fairly simple -- since
42−pages are strictly key-value mappings, only `$variable$` control is supported in
43−hamlet templates. As an example, here is the template that can be used for the
44−brochure site, but in hamlet:
45−
46− !!!
47− %html
48− %head
49− %title MyAweSomeCompany - $$title$
50− %body
51− %h1 MyAweSomeCompany - $$title$
52− #navigation
53− %a!href="$$root$/index.html" Home
54− %a!href="$$root$/about.html" About
55− %a!href="$$root$/code.html" Code
56− $body$
57−
58−Hakyll will recognise hamlet templates automatically by the `.hamlet` extension.
59−
60−[hamlet]: http://docs.yesodweb.com/hamlet/
examples/hakyll/tutorials/part05.markdowndeleted−157
@@ -1,157 +0,0 @@
1−---
2−title: Creating a Blog
3−what: creates a simple blog
4−---
5−
6−## Creating a simple blog with Hakyll
7−
8−After we created a simple brochure site, we're going to try something more
9−advanced: we are going to create a simple blog system.
10−
11−A [zip file] containing the source for this tutorial is also available.
12−
13−[zip file]: $root/examples/simpleblog.zip
14−
15−Blogs, as you probably know, are composed of posts. In Hakyll, we're going
16−to use simple pages for posts. All posts are located in the `posts`
17−directory. But we're not going to use the `directory` command here - you will
18−see why later. First, some trivial things like css.
19−
20−~~~~~{.haskell}
21−main = hakyll "http://example.com" $ do
22− directory css "css"
23−~~~~~
24−
25−## Finding the posts
26−
27−`Text.Hakyll.File` contains a handy function `getRecursiveContents`, which will
28−provide us with all the blog posts. The blog posts have a
29−`yyyy-mm-dd-title.extension` naming scheme. This is just a simple trick so we
30−can sort them easily (sorting on filename implies sorting on date). You could of
31−course name them whatever you want, but it's always a good idea to stick to the
32−conventions. They contain some metadata, too:
33−
34− title: A first post
35− author: Julius Caesar
36− date: November 5, 2009
37− ---
38− Lorem ipsum dolor sit amet, consectetur adipiscing elit.
39− Vivamus pretium leo adipiscing lectus iaculis lobortis.
40− Vivamus scelerisque velit dignissim metus...
41−
42−Now, we find the posts and sort them reversed, so the most recent post will
43−become the first item in the list:
44−
45−~~~~~{.haskell}
46−postPaths <- liftM (reverse . sort) $ getRecursiveContents "posts"
47−~~~~~
48−
49−Our `postPaths` value is now of the type `[FilePath]`. We want to be able to
50−render all posts, so we pass them to the `createPage` function.
51−
52−~~~~~{.haskell}
53−let postPages = map createPage postPaths
54−~~~~~
55−
56−We have two templates we want to render our posts with: first we would like to
57−render them using `templates/post.html`, and we want to render the result
58−using `templates/default.html`. This can be done with the `renderChain`
59−function:
60−
61−~~~~~{.haskell}
62−mapM_ (renderChain [ "templates/post.html"
63− , "templates/default.html"
64− ]) postPages
65−~~~~~
66−
67−Remember that the `renderChain` works by rendering the item using the first
68−template, creating a new page with the render result in the `$body` field, and
69−so on until it has been rendered with all templates.
70−
71−Now, we have the posts rendered. What is left is to generate some kind of index
72−page with links to those posts. We want one general list showing all posts, and
73−we want to show a few recent posts on the index page.
74−
75−## Creating listings.
76−
77−`createPage` is the easiest way of reading a `Context`. But in this case, we
78−want something more custom, so we'll use the `createCustomPage` function. This
79−allows us to create a more specific `Context`.
80−
81−~~~~~{.haskell}
82−createCustomPage :: FilePath
83− -> [(String, Either String (HakyllAction () String))]
84− -> HakyllAction () Context
85−~~~~~
86−
87−The first argument is the `url` of the page to generate. For our index page,
88−this will be, `index.html`. The second argument is obviously our `key: value`
89−mapping. But why the `Either`? This, once again, is about dependency handling.
90−The idea is that you can choose which type to use for the value:
91−
92−- `String`: Simply a `String`.
93−- `HakyllAction () String`: Here, you can give an `HakyllAction` Arrow action
94− that can produce a String. However - this action _will not be executed_ when
95− the file in `_site` is up-to-date.
96−
97−However, in this specific case - a list of posts - there is an easier, and more
98−high-level approach than `createCustomPage`[^1]. Let's look at the type
99−signature of `createListing`:
100−
101−~~~~~{.haskell}
102−createListing :: FilePath
103− -> [FilePath]
104− -> [HakyllAction () Context]
105− -> [(String, Either String (HakyllAction () String))]
106− -> HakyllAction () Context
107−~~~~~
108−
109−[^1]: Since Hakyll-1.3 onwards.
110−
111−The first argument is the destination url. For our blog, this is of course
112−`index.html`. The second argument is a list templates to render _each_ `Context`
113−with. We use only `templates/postitem.html` here. This is, as you can see, a
114−simple template:
115−
116−~~~~~{.html}
117−<li>
118− <a href="$$root/$url">$title</a>
119− - <em>$date</em> - by <em>$author</em>
120−</li>
121−~~~~~
122−
123−We then give a list of `Context`s to render. For our index, these are the 3 last
124−posts. The last argument of the `createListing` functions lets you specify
125−additional key-value pairs, like in `createCustomPage`. We use this to set the
126−title of our page. So, we create our index page using:
127−
128−~~~~~{.haskell}
129−let index = createListing "index.html"
130− ["templates/postitem.html"]
131− (take 3 postPages)
132− [("title", Left "Home")]
133−~~~~~
134−
135−The result of this will be a `HakyllAction () Context`. This `Context`'s `$body`
136−will contain a concatenation of all the 3 posts, rendered with the
137−`templates/postitem.html` template.
138−
139−Now, we only have to render it: first using the `index.html` template - which
140−adds some more information to our index - then using the
141−`templates/default.html` template.
142−
143−~~~~~{.haskell}
144−renderChain ["index.html", "templates/default.html"] index
145−~~~~~
146−
147−Note that the `index.html` in the `renderChain` list is also a template. Now,
148−you might want to take your time to read the `index.html` template and the other
149−files in the zip so you understand what is going on here.
150−
151−## The gist of it
152−
153−- You can find blogposts using `getRecursiveContents`.
154−- The convention is to call them `yyyy-mm-dd-rest-of-title.extension`. This
155− allows us to sort them easily.
156−- You can use `createCustomPage` or `createListing` to create custom pages and
157− simple listings.
examples/hakyll/tutorials/part06.markdowndeleted−84
@@ -1,84 +0,0 @@
1−---
2−title: Creating feeds
3−what: adds an rss feed to the simple blog
4−---
5−
6−## Adding Feeds
7−
8−In this tutorial, we're going to add an RSS feed to the blog we wrote in the
9−previous part. Here is a [zip file] containing the source.
10−
11−[zip file]: $root/examples/feedblog.zip
12−
13−You will be glad to hear that Hakyll has native support for RSS as well as Atom
14−feeds[^1]. This simplifies our object a lot.
15−
16−[^1]: Since Hakyll-2.0
17−
18−This is the first time that the absolute URL of your site you have to give to
19−the `hakyll` function actually matters. That's because the specifications of
20−those feed formats dictate you need the full URL of them.
21−
22−## Creating a configuration
23−
24−The first thing to do is creating a configuration for your feed. You could
25−place this code outside of your main function, as is done in the example.
26−
27−~~~~~{.haskell}
28−myFeedConfiguration = FeedConfiguration
29− { feedUrl = "rss.xml"
30− , feedTitle = "SimpleBlog RSS feed."
31− , feedDescription = "Simple demo of a feed created with Hakyll."
32− , feedAuthorName = "Jasper Van der Jeugt"
33− }
34−~~~~~
35−
36−Note that we enter the url of the feed in our configuration. So the function
37−to render our feed only takes two arguments, the configuration and a list of
38−items to put in the feed. Let's put the three most recent posts in our feed.
39−
40−~~~~~{.haskell}
41−renderRss myFeedConfiguration (take 3 postPages)
42−~~~~~
43−
44−## But it's not that easy
45−
46−If you look at our generated RSS feed (build the site), you will see
47−`$description` tags appearing in our final render. We don't want that! How
48−did they get there in the first place?
49−
50−To render feeds, Hakyll expects a number of fields in the renderables you put
51−in the feed. They are:
52−
53−- `$title`: Title of the item. This is set in our posts, since we use a `title`
54− metadata field.
55−- `$url`: Url of the item. This is automatically set by Hakyll, so you shouldn't
56− worry about it.
57−- `$description`: A description of our item to appear in the feed reader.
58−
59−The latter is obviously the problem: we don't have a description in our posts.
60−In fact, we would like to copy the `$body` key to the `$description` key, so
61−people can read the full post in their feed readers.
62−
63−## Where arrows come in
64−
65−The `Text.Hakyll.ContextManipulations` module contains a number of simple
66−functions that create Arrows for us. One of these functions is `copyValue`,
67−which takes a source and a destination key. So, we need to pass our
68−items through this Arrow first.
69−
70−~~~~~{.haskell}
71−renderRss myFeedConfiguration $
72− map (>>> copyValue "body" "description") (take 3 postPages)
73−~~~~~
74−
75−And that's that, now our feed gets rendered properly. Exercise for the reader
76−is to add a Atom feed[^2].
77−
78−[^2]: Hint: look around in the [reference]($root/reference.html).
79−
80−## The gist of it
81−
82−- Hakyll has native support for RSS and Atom feeds.
83−- The items must contain `$title` and `$description` fields.
84−- Arrows can be used to copy values in a `Context`.
examples/hakyll/tutorials/part07.markdowndeleted−221
@@ -1,221 +0,0 @@
1−---
2−title: Tags and manipulations
3−what: enhances our blog with tags and explains context manipulations.
4−---
5−
6−## Context manipulations
7−
8−Here, have [a zip file]($root/examples/tagblog.zip) for this tutorial.
9−
10−You probably remember that `Context` objects are just key-value mappings. We can
11−render those with templates, and then the `$key`'s in the template get
12−substituted by the appropriate values. This is a rather flexible system, but
13−there are limitations. Some of these limitations can be solved using
14−_context manipulations_.
15−
16−Like rendering actions, _context manipulations_ are also simply
17−`HakyllAction Context Context` arrows. The `Text.Hakyll.ContextManipulations`
18−contains some functions to easily construct easy variants.
19−
20−One of the most general functions is the `renderValue` function. Let's have a
21−look at it's type.
22−
23−~~~~~{.haskell}
24−renderValue :: String
25− -> String
26− -> (String -> String)
27− -> HakyllAction Context Context
28−~~~~~
29−
30−This is the preferred way of creating context manipulations. The first argument
31−is the `key` to manipulate. The second argument is the `key` where the new value
32−should be placed. If this is the same as the first argument, it will be
33−replaced. The third argument is the function to manipulate the `value` with.
34−
35−As a simple example, let's write a function that puts the `$title` in uppercase.
36−
37−~~~~~{.haskell}
38−import Data.Char (toUpper)
39−
40−titleUpper :: HakyllAction Context Context
41−titleUpper = renderValue "title" "title" $ map toUpper
42−~~~~~
43−
44−Because the destination `key` is the same as the source `key`, we can also use
45−the `changeValue` function here.
46−
47−~~~~~{.haskell}
48−titleUpper = changeValue "title" $ map toUpper
49−~~~~~
50−
51−For further reading, refer to the `Text.Hakyll.ContextManipulations`
52−documentation.
53−
54−## Applying Context Manipulations
55−
56−Because we're dealing with Arrows again, we can use `>>>` to apply our
57−manipulations. For example, we could use or title manipulation like this:
58−
59−~~~~~{.haskell}
60−renderChain ["templates/default.html"]
61− (createPage "index.markdown" >>> titleUpper)
62−~~~~~
63−
64−## Rendering dates
65−
66−As you remember, in our previous blog, all posts had a file name like
67−`posts/yyyy-mm-dd-title.extension`, as is the Hakyll convention. But they also
68−had a metadata field `date`, containing a human-readable date. This is not very
69−D.R.Y., of course! Hakyll has a specialized `renderValue` function to deal with
70−dates encoded in paths: `renderDate`.
71−
72−~~~~~{.haskell}
73−postManipulation :: HakyllAction Context Context
74−postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
75−~~~~~
76−
77−That manipulation will:
78−- Read the date from the file name the post was loaded from.
79−- Parse the date and render it in a `%B %e, %Y` format. This is a
80− `Month day, Year` format.
81−- Put the result in the `date` metadata field.
82−- If the date could not be parsed, it will put `"Unknown date"` in the `date`
83− metadata field.
84−
85−So, we can throw away our `date: ` lines from our posts, and still use `$date`
86−in our templates.
87−
88−## Abstracting the post list
89−
90−Now, we're going to render tags. This is also done using context manipulations.
91−Hakyll has a specialized module to deal with tags, provided by
92−`Text.Hakyll.Tags`. This module assumes tags are comma separated, and placed in
93−the `tags` metadata field.
94−
95− ---
96− title: A third post
97− author: Publius Ovidius Naso
98− tags: epic fail, ovidius
99− ---
100− Pellentesque tempor blandit elit, vel...
101−
102−But first things first. We need to render a post list for every tag. We already
103−had some code to render a list of all posts. We're just going to abstract this
104−code into a more general function:
105−
106−~~~~{.haskell}
107−renderPostList url title posts = do
108− let list = createListingWith url ["templates/postitem.html"]
109− posts [("title", Left title)]
110− renderChain ["posts.html", "templates/default.html"] list
111−~~~~~
112−
113−Our "render all posts" action can now be written as:
114−
115−~~~~~{.haskell}
116−renderPostList "posts.html" "All posts" renderablePosts
117−~~~~~
118−
119−## Tag links
120−
121−We want to display the tags for our post under the title. But if we use the
122−`$tags` key in a template, we will just have the plain tags - they will not be
123−clickable. We can again solve this with a `ContextManipulation`. We have a
124−function that produces an url for a given tag:
125−
126−~~~~~{.haskell}
127−tagToUrl tag = "$root/tags/" ++ removeSpaces tag ++ ".html"
128−~~~~~
129−
130−`removeSpaces` is an auxiliary function from `Text.Hakyll.File`. Now, there is
131−a specialized `renderValue` function for creating linked tags called
132−`renderTagLinks`. This function simply takes a function that produces an url
133−for a given tag - the function we just wrote. Let's extend our
134−`postManipulation`.
135−
136−~~~~~{.haskell}
137−postManipulation :: HakyllAction Context Context
138−postManipulation = renderDate "date" "%B %e, %Y" "Unknown date"
139− >>> renderTagLinks tagToUrl
140−~~~~~
141−
142−We apply this manipulation when we load the tags.
143−
144−~~~~~{.haskell}
145−let renderablePosts =
146− map ((>>> postManipulation) . createPage) postPaths
147−~~~~~
148−
149−So, the `renderTagLinks` function replaces the `$tags` value from
150−`epic fail, random` to `<a href="$root/tags/epic-fail.html">epic fail</a>, ...`.
151−If we click a tag, we get a `404`. That's because we haven't generated the
152−post lists for every tag.
153−
154−## The Tag Map
155−
156−Hakyll provides a function called `readTagMap`. Let's inspect it's type.
157−
158−~~~~~{.haskell}
159−type TagMap = Map String [HakyllAction () Context]
160−readTagMap String [FilePath] -> HakyllAction () TagMap
161−~~~~~
162−
163−You give it a list of paths, and it creates a map that, for every tag, holds
164−a number of posts. We can easily use this to render a post list for every tag.
165−The first argument given is an "identifier", unique to this tag map. Hakyll
166−needs this so it can cache the tags.
167−
168−~~~~~{.haskell}
169−let tagMap = readTagMap "postTags" postPaths
170−~~~~~
171−
172−When we have the `TagMap`, we can need to render a post list for every tag.
173−There is a function in Hakyll designed more or less for this purpose:
174−`withTagMap`. This takes a `TagMap` and an action to execute for every tag and
175−it's associated posts. We pass a small function to it we create ourselves[^1]:
176−
177−[^1]: Exercise for the reader: why do we use `>>> postManipulation` again here?
178−
179−~~~~~{.haskell}
180−let renderListForTag tag posts =
181− renderPostList (tagToUrl tag)
182− ("Posts tagged " ++ tag)
183− (map (>>> postManipulation) posts)
184−withTagMap tagMap renderPostList
185−~~~~~
186−
187−There we go. We now have clickable tags, and a post list for every tag.
188−
189−## A Tag Cloud
190−
191−A tag cloud is a commonly found thing on blogs. Hakyll also provides code to
192−generate a tag cloud. Let's have a look at the `renderTagCloud` function.
193−
194−~~~~~{.haskell}
195−renderTagCloud :: (String -> String)
196− -> Float
197− -> Float
198− -> HakyllAction TagMap String
199−~~~~~
200−
201−The first argument is, once again, a function to create an url for a given tag.
202−Then, we give a minimum and a maximum font size in percent, and we get a tag
203−cloud Arrow back. We can add this to our index:
204−
205−~~~~~{.haskell}
206−let tagCloud = tagMap >>> renderTagCloud tagToUrl 100 200
207− index = createListing "index.html"
208− ["templates/postitem.html"]
209− (take 3 renderablePosts)
210− [ ("title", Left "Home")
211− , ("tagcloud", Right tagCloud)
212− ]
213−renderChain ["index.html", "templates/default.html"] index
214−~~~~~
215−
216−## The gist of it
217−
218−- There's some handy, simple functions in `Text.Hakyll.ContextManipulations`.
219−- Seperate tags by commas and put them in the `$tags` field.
220−- Use `withTagMap` to render a list for every tag.
221−- Hakyll can also create tag clouds.
examples/hakyll/tutorials/part08.markdowndeleted−97
@@ -1,97 +0,0 @@
1−---
2−title: Interlude
3−what: gives some various tips and tricks about Hakyll (quite handy, read this!)
4−---
5−
6−## Syntax-highlighting
7−
8−Pandoc (which Hakyll uses as a backend) offers powerful syntax highlighting.
9−To enable this, Pandoc needs to be compiled with highlighting support. If this
10−is not the case, you can fix this using:
11−
12−~~~~~
13−[jasper@alice ~]$ cabal install --reinstall -fhighlighting pandoc
14−~~~~~
15−
16−## Auto-compilation
17−
18−Hakyll features a simple _auto-compilation_ mode. This is invoked by running
19−
20−~~~~~
21−[jasper@alice ~]$ ./hakyll preview
22−Starting hakyll server on port 8000...
23−~~~~~
24−
25−Now, Hakyll will recompile your site when you refresh in your browser. This will
26−not update your site automatically when `hakyll.hs` changes. So if you make any
27−changes to the configuration file, you'll have to compile it again, and then you
28−can enter `preview` mode again.
29−
30−If you use a custom `HakyllConfiguration`, you can select your custom
31−`PreviewMode`:
32−
33−- `BuildOnRequest`: rebuild site when the preview server receives a request
34− (default).
35−- `BuildOnInterval`: build when you change files.
36−
37−## When to rebuild
38−
39−If you execute a `./hakyll build`, Hakyll will build your site incrementally.
40−This means it will be very fast, but it will not pick up _all_ changes.
41−
42−- In case you edited `hakyll.hs`, you first want to compile it again.
43−- It is generally recommended to do a `./hakyll rebuild` before you deploy your
44− site.
45−
46−After rebuilding your site, all files will look as "modified" to the filesystem.
47−This means that when you upload your site, it will usually transfer all files --
48−this can generate more traffic than necessary, since it is possible that some
49−files were not actually modified. If you use `rsync`, you can counter this using
50−the `--checksum` option.
51−
52−## Pretty URL's
53−
54−There is an option in Hakyll to produce pretty URL's, which is disabled by
55−default because it can be confusing when you're first introduced to Hakyll.
56−
57−It can be enabled this way:
58−
59−~~~~~{.haskell}
60−import Text.Hakyll
61−import Text.Hakyll.HakyllMonad
62−
63−myConfig :: HakyllConfiguration
64−myConfig = (defaultHakyllConfiguration "http://jaspervdj.be")
65− { enableIndexUrl = True
66− }
67−
68−main = hakyllWithConfiguration myConfig $ do
69− -- Further code here
70−~~~~~
71−
72−The effect will be that the internal `toUrl` function will behave differently.
73−A few examples:
74−
75−- `about.html` will be rendered to `about/index.html`.
76−- `posts/2010-02-16-a-post.markdown` will be rendered to
77− `posts/2010-02-16-a-post/index.html`.
78−- However, `index.markdown` will still be rendered to `index.html`. Likewise,
79− `posts/index.html` would be rendered to `posts.index.html`.
80−
81−The benefit of this is simply prettier URL's. That is, if you consider
82−`example.com/about` prettier than `example.com/about.html`.
83−
84−## Default values
85−
86−At some point, you might want to use a number of global key-value pairs, for
87−example, `$author`. There are two possible ways to achieve this.
88−
89−- There is an option in `HakyllConfiguration` supporting this, called
90− `additionalContext`. For an example on how to use `HakyllConfiguration`, see
91− the pretty URL's section above.
92−
93−- Another option is to use a `defaults.markdown` file, simply containing some
94− metadata, and then `combine` this file with other pages. The advantage is
95− that autocompilation mode will pick up changes in this file[^1].
96−
97−[^1]: Original idea by zenzike.
examples/hakyll/tutorials/part09.markdowndeleted−104
@@ -1,104 +0,0 @@
1−---
2−title: CategoryBlog
3−what: explains how to use categories instead of tags
4−---
5−
6−## Categories
7−
8−Most people familiar with "tags" will also know the concept "categories".
9−
10−![Tags illustration]($root/images/tutorial8-tags.png)
11−
12−In fact, tags are harder to implement because they have to be represented as a
13−many-to-many relation, and categories are a simple 1-to-many relation.
14−
15−![Tags illustration]($root/images/tutorial8-categories.png)
16−
17−This is also the reason you can "simulate" categories using tags. In this
18−tutorial we will adapt the blog to use categories instead of tags. Here is
19−[a zip file]($root/examples/categoryblog.zip) containing the files used in this
20−tutorial.
21−
22−## About category support
23−
24−Categories are simpler, but they are usually used in custom ways. That's why
25−Hakyll provides less "standard" functions to deal with them. But this gives us
26−another chance to learn some of the things we can do with Hakyll.
27−
28−## Reading Categories
29−
30−Tags are located in the `tags` metadata field. Since one post can only belong
31−in one category, a different approach was chosen here. The category of a post
32−is determined by the subfolder it is in. Here you see the directory layout for
33−our posts using categories:
34−
35− posts
36− |-- coding
37− | |-- 2009-11-05-a-first-post.markdown
38− | |-- 2009-11-28-a-third-post.markdown
39− | `-- 2009-12-04-this-blog-aint-dead.markdown
40− `-- random
41− |-- 2009-11-10-another-post.markdown
42− `-- 2009-12-23-almost-christmas.markdown
43−
44−Because we find all our posts in different subdirectories, sorting them is a
45−little harder: we still want them sorted by date, so it boils down to sorting
46−them by "base name". I hope it does not surprise you Hakyll provides a function
47−for that:
48−
49−~~~~~{.haskell}
50−postPaths <- liftM (reverse . sortByBaseName)
51− (getRecursiveContents "posts")
52−~~~~~
53−
54−We reverse them again, because we want the most recent posts first. Now, we can
55−use the `readCategoryMap` function instead of `readTagMap`, which has the same
56−signature, but assigns categories based on the folders the posts are in.
57−
58−~~~~~{.haskell}
59−categoryMap <- readCategoryMap "categoryMap" renderablePosts
60−~~~~~
61−
62−The rest of the `hakyll.hs` is very similar to the one in the previous
63−tutorial, except we want to render a category list instead of a tag cloud.
64−
65−## Rendering a category list
66−
67−Because rendering a category list is quite easy, and it would be hard to
68−write a "general" function for this, hakyll does not provide such a function --
69−but it is not hard to write. First, we write an auxiliary function that produces
70−a list item for one category:
71−
72−~~~~~{.haskell}
73−categoryListItem category posts =
74− "<li>" ++ link category (categoryToUrl category)
75− ++ " - " ++ show (length posts) ++ " items.</li>"
76−~~~~~
77−
78−This is nothing more that some basic string concatenation to create a `li` HTML
79−element. The function that applies this on every element in the `TagMap` is more
80−interesting:
81−
82−~~~~~{.haskell}
83−categoryList :: HakyllAction TagMap String
84−categoryList = arr $ uncurry categoryListItem <=< toList
85−~~~~~
86−
87−This function might seem a little harder to understand if you are not familiar
88−with the `<=<` operator -- but it's just right-to-left monad composition in the
89−list monad. `uncurry categoryListItem <=< toList` is a pure function we want to
90−execute on the `TagMap`. But this is not possible in Hakyll[^1]. We need to make
91−an arrow of this function. The `arr` function solves this problem easily.
92−
93−[^1]: This is a feature, not a bug. It helps dependency handling.
94−
95−We then add this to our index page, and we are done. Feel free to hack around
96−with the source code. If you still have questions, feel free to ask them at the
97−[google discussion group](http://groups.google.com/group/hakyll).
98−
99−## The gist of it
100−
101−- Hakyll supports categories as well as tags.
102−- Tags are actually a generalization of categories.
103−- Use `readCategoryMap` to read categories.
104−- You need to write some custom functions to render category lists etc.
hakyll.cabal+84−42
@@ -1,5 +1,5 @@
11Name: hakyll
2−Version: 2.4.3
2+Version: 3.0.0.3
33
44Synopsis: A simple static site generator library.
55Description: A simple static site generator library, mainly aimed at
@@ -17,6 +17,8 @@ Data-Files: templates/atom.xml
1717 templates/atom-item.xml
1818 templates/rss.xml
1919 templates/rss-item.xml
20+extra-source-files: src-inotify/Hakyll/Web/Preview/Poll.hs
21+ src-interval/Hakyll/Web/Preview/Poll.hs
2022
2123build-type: Simple
2224
@@ -24,45 +26,85 @@ source-repository head
2426 type: git
2527 location: git://github.com/jaspervdj/Hakyll.git
2628
29+flag inotify
30+ description: Use the inotify bindings for the preview server. Better, but
31+ only works on Linux.
32+ default: False
33+
2734library
28− ghc-options: -Wall
29− hs-source-dirs: src
30− build-depends: base >= 4 && < 5,
31− filepath == 1.*,
32− directory == 1.*,
33− containers == 0.*,
34− pandoc == 1.*,
35− regex-base >= 0.93,
36− regex-tdfa >= 1.1,
37− network == 2.*,
38− mtl >= 1,
39− old-locale == 1.*,
40− old-time == 1.*,
41− time >= 1.1,
42− binary >= 0.5,
43− hamlet >= 0.4.2,
44− blaze-html >= 0.4
45− exposed-modules: Network.Hakyll.SimpleServer
46− Text.Hakyll
47− Text.Hakyll.Context
48− Text.Hakyll.ContextManipulations
49− Text.Hakyll.CreateContext
50− Text.Hakyll.File
51− Text.Hakyll.HakyllMonad
52− Text.Hakyll.Regex
53− Text.Hakyll.Render
54− Text.Hakyll.HakyllAction
55− Text.Hakyll.Paginate
56− Text.Hakyll.Page
57− Text.Hakyll.Pandoc
58− Text.Hakyll.Util
59− Text.Hakyll.Tags
60− Text.Hakyll.Feed
61− Text.Hakyll.Configurations.Static
62− other-modules: Paths_hakyll
63− Text.Hakyll.Internal.Cache
64− Text.Hakyll.Internal.CompressCss
65− Text.Hakyll.Internal.FileType
66− Text.Hakyll.Internal.Template
67− Text.Hakyll.Internal.Template.Template
68− Text.Hakyll.Internal.Template.Hamlet
35+ ghc-options: -Wall
36+ hs-source-dirs: src
37+
38+ if flag(inotify)
39+ hs-source-dirs: src-inotify
40+ build-depends: hinotify >= 0.3
41+ else
42+ hs-source-dirs: src-interval
43+
44+ build-depends: base >= 4 && < 5,
45+ filepath == 1.*,
46+ directory == 1.*,
47+ containers == 0.*,
48+ pandoc == 1.*,
49+ regex-base >= 0.93,
50+ regex-pcre >= 0.93,
51+ mtl >= 1,
52+ old-locale == 1.*,
53+ old-time == 1.*,
54+ time >= 1.1,
55+ binary >= 0.5,
56+ hamlet >= 0.7,
57+ blaze-html >= 0.4,
58+ snap-server >= 0.4,
59+ snap-core >= 0.4,
60+ bytestring >= 0.9,
61+ utf8-string >= 0.3,
62+ tagsoup >= 0.12,
63+ hopenssl >= 1.4,
64+ unix >= 2.4,
65+ strict-concurrency >= 0.2
66+ exposed-modules: Hakyll
67+ Hakyll.Main
68+ Hakyll.Web.Util.Url
69+ Hakyll.Web.Preview.Server
70+ Hakyll.Web.Preview.Poll
71+ Hakyll.Web.CompressCss
72+ Hakyll.Web.Template
73+ Hakyll.Web.Feed
74+ Hakyll.Web.Tags
75+ Hakyll.Web.Pandoc
76+ Hakyll.Web.FileType
77+ Hakyll.Web.Page
78+ Hakyll.Web.Template.Read
79+ Hakyll.Web.RelativizeUrls
80+ Hakyll.Web.Page.Read
81+ Hakyll.Web.Page.Metadata
82+ Hakyll.Core.ResourceProvider.FileResourceProvider
83+ Hakyll.Core.Configuration
84+ Hakyll.Core.Identifier.Pattern
85+ Hakyll.Core.UnixFilter
86+ Hakyll.Core.Util.Arrow
87+ Hakyll.Core.Util.File
88+ Hakyll.Core.Util.String
89+ Hakyll.Core.ResourceProvider
90+ Hakyll.Core.CompiledItem
91+ Hakyll.Core.Compiler
92+ Hakyll.Core.CopyFile
93+ Hakyll.Core.Run
94+ Hakyll.Core.Store
95+ Hakyll.Core.Writable
96+ Hakyll.Core.Identifier
97+ Hakyll.Core.DirectedGraph.Dot
98+ Hakyll.Core.DirectedGraph.DependencySolver
99+ Hakyll.Core.DirectedGraph
100+ Hakyll.Core.Rules
101+ Hakyll.Core.Routes
102+ Hakyll.Core.Logger
103+ other-modules: Paths_hakyll
104+ Hakyll.Web.Template.Read.Hakyll
105+ Hakyll.Web.Template.Read.Hamlet
106+ Hakyll.Web.Template.Internal
107+ Hakyll.Web.Page.Internal
108+ Hakyll.Core.Compiler.Internal
109+ Hakyll.Core.DirectedGraph.Internal
110+ Hakyll.Core.Rules.Internal
src-inotify/Hakyll/Web/Preview/Poll.hsnew file+51
@@ -0,0 +1,51 @@
1+-- | Filesystem polling with an inotify backend. Works only on linux.
2+--
3+module Hakyll.Web.Preview.Poll
4+ ( previewPoll
5+ ) where
6+
7+import Control.Monad (forM_, when)
8+import Data.Set (Set)
9+import qualified Data.Set as S
10+import System.FilePath (takeDirectory, (</>))
11+import Data.List (isPrefixOf)
12+
13+import System.INotify
14+
15+import Hakyll.Core.Configuration
16+import Hakyll.Core.ResourceProvider
17+import Hakyll.Core.Identifier
18+
19+-- | Calls the given callback when the directory tree changes
20+--
21+previewPoll :: HakyllConfiguration -- ^ Configuration
22+ -> Set Resource -- ^ Resources to watch
23+ -> IO () -- ^ Action called when something changes
24+ -> IO () -- ^ Can block forever
25+previewPoll _ resources callback = do
26+ -- Initialize inotify
27+ inotify <- initINotify
28+
29+ let -- A set of file paths
30+ paths = S.map (toFilePath . unResource) resources
31+
32+ -- A list of directories. Run it through a set so we have every
33+ -- directory only once.
34+ directories = S.toList $ S.map (notEmpty . takeDirectory) paths
35+
36+ -- Problem: we can't add a watcher for "". So we make sure a directory
37+ -- name is not empty
38+ notEmpty "" = "."
39+ notEmpty x = x
40+
41+ -- Execute the callback when path is known
42+ ifResource path =
43+ let path' = if "./" `isPrefixOf` path then drop 2 path else path
44+ in when (path' `S.member` paths) callback
45+
46+ -- Add a watcher for every directory
47+ forM_ directories $ \directory -> do
48+ _ <- addWatch inotify [Modify] directory $ \e -> case e of
49+ (Modified _ (Just p)) -> ifResource $ directory </> p
50+ _ -> return ()
51+ return ()
src-interval/Hakyll/Web/Preview/Poll.hsnew file+36
@@ -0,0 +1,36 @@
1+-- | Interval-based implementation of preview polling, for the platforms which
2+-- are not supported by inotify.
3+--
4+module Hakyll.Web.Preview.Poll
5+ ( previewPoll
6+ ) where
7+
8+import Control.Applicative ((<$>))
9+import Control.Concurrent (threadDelay)
10+import Control.Monad (when)
11+import System.Time (getClockTime)
12+import Data.Set (Set)
13+import qualified Data.Set as S
14+import System.Directory (getModificationTime)
15+
16+import Hakyll.Core.Configuration
17+import Hakyll.Core.Identifier
18+import Hakyll.Core.ResourceProvider
19+
20+-- | A preview thread that periodically recompiles the site.
21+--
22+previewPoll :: HakyllConfiguration -- ^ Configuration
23+ -> Set Resource -- ^ Resources to watch
24+ -> IO () -- ^ Action called when something changes
25+ -> IO () -- ^ Can block forever
26+previewPoll _ resources callback = do
27+ let files = map (toFilePath . unResource) $ S.toList resources
28+ time <- getClockTime
29+ loop files time
30+ where
31+ delay = 1000000
32+ loop files time = do
33+ threadDelay delay
34+ modified <- any (time <) <$> mapM getModificationTime files
35+ when modified callback
36+ loop files =<< getClockTime
src/Hakyll.hsnew file+55
@@ -0,0 +1,55 @@
1+-- | Top-level module exporting all modules that are interesting for the user
2+--
3+module Hakyll
4+ ( module Hakyll.Core.Compiler
5+ , module Hakyll.Core.CopyFile
6+ , module Hakyll.Core.Configuration
7+ , module Hakyll.Core.Identifier
8+ , module Hakyll.Core.Identifier.Pattern
9+ , module Hakyll.Core.ResourceProvider
10+ , module Hakyll.Core.Routes
11+ , module Hakyll.Core.Rules
12+ , module Hakyll.Core.UnixFilter
13+ , module Hakyll.Core.Util.Arrow
14+ , module Hakyll.Core.Util.File
15+ , module Hakyll.Core.Util.String
16+ , module Hakyll.Core.Writable
17+ , module Hakyll.Main
18+ , module Hakyll.Web.CompressCss
19+ , module Hakyll.Web.Feed
20+ , module Hakyll.Web.FileType
21+ , module Hakyll.Web.Page
22+ , module Hakyll.Web.Page.Metadata
23+ , module Hakyll.Web.Page.Read
24+ , module Hakyll.Web.Pandoc
25+ , module Hakyll.Web.RelativizeUrls
26+ , module Hakyll.Web.Tags
27+ , module Hakyll.Web.Template
28+ , module Hakyll.Web.Util.Url
29+ ) where
30+
31+import Hakyll.Core.Compiler
32+import Hakyll.Core.CopyFile
33+import Hakyll.Core.Configuration
34+import Hakyll.Core.Identifier
35+import Hakyll.Core.Identifier.Pattern
36+import Hakyll.Core.ResourceProvider
37+import Hakyll.Core.Routes
38+import Hakyll.Core.Rules
39+import Hakyll.Core.UnixFilter
40+import Hakyll.Core.Util.Arrow
41+import Hakyll.Core.Util.File
42+import Hakyll.Core.Util.String
43+import Hakyll.Core.Writable
44+import Hakyll.Main
45+import Hakyll.Web.CompressCss
46+import Hakyll.Web.Feed
47+import Hakyll.Web.FileType
48+import Hakyll.Web.Page
49+import Hakyll.Web.Page.Metadata
50+import Hakyll.Web.Page.Read
51+import Hakyll.Web.Pandoc
52+import Hakyll.Web.RelativizeUrls
53+import Hakyll.Web.Tags
54+import Hakyll.Web.Template
55+import Hakyll.Web.Util.Url
src/Hakyll/Core/CompiledItem.hsnew file+45
@@ -0,0 +1,45 @@
1+-- | A module containing a box datatype representing a compiled item. This
2+-- item can be of any type, given that a few restrictions hold:
3+--
4+-- * we need a 'Typeable' instance to perform type-safe casts;
5+--
6+-- * we need a 'Binary' instance so we can serialize these items to the cache;
7+--
8+-- * we need a 'Writable' instance so the results can be saved.
9+--
10+{-# LANGUAGE ExistentialQuantification #-}
11+module Hakyll.Core.CompiledItem
12+ ( CompiledItem (..)
13+ , compiledItem
14+ , unCompiledItem
15+ ) where
16+
17+import Data.Binary (Binary)
18+import Data.Typeable (Typeable, cast)
19+import Data.Maybe (fromMaybe)
20+
21+import Hakyll.Core.Writable
22+
23+-- | Box type for a compiled item
24+--
25+data CompiledItem = forall a. (Binary a, Typeable a, Writable a)
26+ => CompiledItem a
27+
28+instance Writable CompiledItem where
29+ write p (CompiledItem x) = write p x
30+
31+-- | Box a value into a 'CompiledItem'
32+--
33+compiledItem :: (Binary a, Typeable a, Writable a)
34+ => a
35+ -> CompiledItem
36+compiledItem = CompiledItem
37+
38+-- | Unbox a value from a 'CompiledItem'
39+--
40+unCompiledItem :: (Binary a, Typeable a, Writable a)
41+ => CompiledItem
42+ -> a
43+unCompiledItem (CompiledItem x) = fromMaybe error' $ cast x
44+ where
45+ error' = error "Hakyll.Core.CompiledItem.unCompiledItem: Unsupported type"
src/Hakyll/Core/Compiler.hsnew file+333
@@ -0,0 +1,333 @@
1+-- | A Compiler manages targets and dependencies between targets
2+--
3+-- The most distinguishing property of a 'Compiler' is that it is an Arrow. A
4+-- compiler of the type @Compiler a b@ is simply a compilation phase which takes
5+-- an @a@ as input, and produces a @b@ as output.
6+--
7+-- Compilers are chained using the '>>>' arrow operation. If we have a compiler
8+--
9+-- > getResourceString :: Compiler Resource String
10+--
11+-- which reads the resource, and a compiler
12+--
13+-- > readPage :: Compiler String (Page String)
14+--
15+-- we can chain these two compilers to get a
16+--
17+-- > (getResourceString >>> readPage) :: Compiler Resource (Page String)
18+--
19+-- Most compilers can be created by combining smaller compilers using '>>>'.
20+--
21+-- More advanced constructions are also possible using arrow, and sometimes
22+-- these are needed. For a good introduction to arrow, you can refer to
23+--
24+-- <http://en.wikibooks.org/wiki/Haskell/Understanding_arrows>
25+--
26+-- A construction worth writing a few paragraphs about here are the 'require'
27+-- functions. Different variants of this function are exported here, but they
28+-- all serve more or less the same goal.
29+--
30+-- When you use only '>>>' to chain your compilers, you get a linear pipeline --
31+-- it is not possible to add extra items from other compilers along the way.
32+-- This is where the 'require' functions come in.
33+--
34+-- This function allows you to reference other items, which are then added to
35+-- the pipeline. Let's look at this crappy ASCII illustration which represents
36+-- a pretty common scenario:
37+--
38+-- > read resource >>> pandoc render >>> layout >>> relativize URL's
39+-- >
40+-- > @templates/fancy.html@
41+--
42+-- We want to construct a pipeline of compilers to go from our resource to a
43+-- proper webpage. However, the @layout@ compiler takes more than just the
44+-- rendered page as input: it needs the @templates/fancy.html@ template as well.
45+--
46+-- This is an example of where we need the @require@ function. We can solve
47+-- this using a construction that looks like:
48+--
49+-- > ... >>> pandoc render >>> require >>> layout >>> ...
50+-- > |
51+-- > @templates/fancy.html@ ------/
52+--
53+-- This illustration can help us understand the type signature of 'require'.
54+--
55+-- > require :: (Binary a, Typeable a, Writable a)
56+-- > => Identifier
57+-- > -> (b -> a -> c)
58+-- > -> Compiler b c
59+--
60+-- Let's look at it in detail:
61+--
62+-- > (Binary a, Typeable a, Writable a)
63+--
64+-- These are constraints for the @a@ type. @a@ (the template) needs to have
65+-- certain properties for it to be required.
66+--
67+-- > Identifier
68+--
69+-- This is simply @templates/fancy.html@: the 'Identifier' of the item we want
70+-- to 'require', in other words, the name of the item we want to add to the
71+-- pipeline somehow.
72+--
73+-- > (b -> a -> c)
74+--
75+-- This is a function given by the user, specifying /how/ the two items shall be
76+-- merged. @b@ is the output of the previous compiler, and @a@ is the item we
77+-- just required -- the template. This means @c@ will be the final output of the
78+-- 'require' combinator.
79+--
80+-- > Compiler b c
81+--
82+-- Indeed, we have now constructed a compiler which takes a @b@ and produces a
83+-- @c@. This means that we have a linear pipeline again, thanks to the 'require'
84+-- function. So, the 'require' function actually helps to reduce to complexity
85+-- of Hakyll applications!
86+--
87+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
88+module Hakyll.Core.Compiler
89+ ( Compiler
90+ , runCompiler
91+ , getIdentifier
92+ , getRoute
93+ , getRouteFor
94+ , getResourceString
95+ , fromDependency
96+ , require_
97+ , require
98+ , requireA
99+ , requireAll_
100+ , requireAll
101+ , requireAllA
102+ , cached
103+ , unsafeCompiler
104+ , traceShowCompiler
105+ , mapCompiler
106+ , timedCompiler
107+ , byExtension
108+ ) where
109+
110+import Prelude hiding ((.), id)
111+import Control.Arrow ((>>>), (&&&), arr)
112+import Control.Applicative ((<$>))
113+import Control.Monad.Reader (ask)
114+import Control.Monad.Trans (liftIO)
115+import Control.Category (Category, (.), id)
116+import Data.Maybe (fromMaybe)
117+import System.FilePath (takeExtension)
118+
119+import Data.Binary (Binary)
120+import Data.Typeable (Typeable)
121+
122+import Hakyll.Core.Identifier
123+import Hakyll.Core.Identifier.Pattern
124+import Hakyll.Core.CompiledItem
125+import Hakyll.Core.Writable
126+import Hakyll.Core.ResourceProvider
127+import Hakyll.Core.Compiler.Internal
128+import Hakyll.Core.Store
129+import Hakyll.Core.Rules.Internal
130+import Hakyll.Core.Routes
131+import Hakyll.Core.Logger
132+
133+-- | Run a compiler, yielding the resulting target and it's dependencies. This
134+-- version of 'runCompilerJob' also stores the result
135+--
136+runCompiler :: Compiler () CompileRule -- ^ Compiler to run
137+ -> Identifier -- ^ Target identifier
138+ -> ResourceProvider -- ^ Resource provider
139+ -> Routes -- ^ Route
140+ -> Store -- ^ Store
141+ -> Bool -- ^ Was the resource modified?
142+ -> Logger -- ^ Logger
143+ -> IO CompileRule -- ^ Resulting item
144+runCompiler compiler identifier provider routes store modified logger = do
145+ -- Run the compiler job
146+ result <-
147+ runCompilerJob compiler identifier provider routes store modified logger
148+
149+ -- Inspect the result
150+ case result of
151+ -- In case we compiled an item, we will store a copy in the cache first,
152+ -- before we return control. This makes sure the compiled item can later
153+ -- be accessed by e.g. require.
154+ CompileRule (CompiledItem x) ->
155+ storeSet store "Hakyll.Core.Compiler.runCompiler" identifier x
156+
157+ -- Otherwise, we do nothing here
158+ _ -> return ()
159+
160+ return result
161+
162+-- | Get the identifier of the item that is currently being compiled
163+--
164+getIdentifier :: Compiler a Identifier
165+getIdentifier = fromJob $ const $ CompilerM $ compilerIdentifier <$> ask
166+
167+-- | Get the route we are using for this item
168+--
169+getRoute :: Compiler a (Maybe FilePath)
170+getRoute = getIdentifier >>> getRouteFor
171+
172+-- | Get the route for a specified item
173+--
174+getRouteFor :: Compiler Identifier (Maybe FilePath)
175+getRouteFor = fromJob $ \identifier -> CompilerM $ do
176+ routes <- compilerRoutes <$> ask
177+ return $ runRoutes routes identifier
178+
179+-- | Get the resource we are compiling as a string
180+--
181+getResourceString :: Compiler Resource String
182+getResourceString = fromJob $ \resource -> CompilerM $ do
183+ provider <- compilerResourceProvider <$> ask
184+ liftIO $ resourceString provider resource
185+
186+-- | Auxiliary: get a dependency
187+--
188+getDependency :: (Binary a, Writable a, Typeable a)
189+ => Identifier -> CompilerM a
190+getDependency identifier = CompilerM $ do
191+ store <- compilerStore <$> ask
192+ fmap (fromMaybe error') $ liftIO $
193+ storeGet store "Hakyll.Core.Compiler.runCompiler" identifier
194+ where
195+ error' = error $ "Hakyll.Core.Compiler.getDependency: "
196+ ++ show identifier
197+ ++ " not found in the cache, the cache might be corrupted or"
198+ ++ " the item you are referring to might not exist"
199+
200+
201+-- | Variant of 'require' which drops the current value
202+--
203+require_ :: (Binary a, Typeable a, Writable a)
204+ => Identifier
205+ -> Compiler b a
206+require_ identifier =
207+ fromDependency identifier >>> fromJob (const $ getDependency identifier)
208+
209+-- | Require another target. Using this function ensures automatic handling of
210+-- dependencies
211+--
212+require :: (Binary a, Typeable a, Writable a)
213+ => Identifier
214+ -> (b -> a -> c)
215+ -> Compiler b c
216+require identifier = requireA identifier . arr . uncurry
217+
218+-- | Arrow-based variant of 'require'
219+--
220+requireA :: (Binary a, Typeable a, Writable a)
221+ => Identifier
222+ -> Compiler (b, a) c
223+ -> Compiler b c
224+requireA identifier = (id &&& require_ identifier >>>)
225+
226+-- | Variant of 'requireAll' which drops the current value
227+--
228+requireAll_ :: (Binary a, Typeable a, Writable a)
229+ => Pattern
230+ -> Compiler b [a]
231+requireAll_ pattern = fromDependencies (const getDeps) >>> fromJob requireAll_'
232+ where
233+ getDeps = matches pattern . map unResource . resourceList
234+ requireAll_' = const $ CompilerM $ do
235+ deps <- getDeps . compilerResourceProvider <$> ask
236+ mapM (unCompilerM . getDependency) deps
237+
238+-- | Require a number of targets. Using this function ensures automatic handling
239+-- of dependencies
240+--
241+requireAll :: (Binary a, Typeable a, Writable a)
242+ => Pattern
243+ -> (b -> [a] -> c)
244+ -> Compiler b c
245+requireAll pattern = requireAllA pattern . arr . uncurry
246+
247+-- | Arrow-based variant of 'requireAll'
248+--
249+requireAllA :: (Binary a, Typeable a, Writable a)
250+ => Pattern
251+ -> Compiler (b, [a]) c
252+ -> Compiler b c
253+requireAllA pattern = (id &&& requireAll_ pattern >>>)
254+
255+cached :: (Binary a, Typeable a, Writable a)
256+ => String
257+ -> Compiler Resource a
258+ -> Compiler Resource a
259+cached name (Compiler d j) = Compiler d $ const $ CompilerM $ do
260+ logger <- compilerLogger <$> ask
261+ identifier <- compilerIdentifier <$> ask
262+ store <- compilerStore <$> ask
263+ modified <- compilerResourceModified <$> ask
264+ report logger $ "Checking cache: " ++ if modified then "modified" else "OK"
265+ if modified
266+ then do v <- unCompilerM $ j $ Resource identifier
267+ liftIO $ storeSet store name identifier v
268+ return v
269+ else do v <- liftIO $ storeGet store name identifier
270+ case v of Just v' -> return v'
271+ Nothing -> error'
272+ where
273+ error' = error "Hakyll.Core.Compiler.cached: Cache corrupt!"
274+
275+-- | Create an unsafe compiler from a function in IO
276+--
277+unsafeCompiler :: (a -> IO b) -- ^ Function to lift
278+ -> Compiler a b -- ^ Resulting compiler
279+unsafeCompiler f = fromJob $ CompilerM . liftIO . f
280+
281+-- | Compiler for debugging purposes
282+--
283+traceShowCompiler :: Show a => Compiler a a
284+traceShowCompiler = fromJob $ \x -> CompilerM $ do
285+ logger <- compilerLogger <$> ask
286+ report logger $ show x
287+ return x
288+
289+-- | Map over a compiler
290+--
291+mapCompiler :: Compiler a b
292+ -> Compiler [a] [b]
293+mapCompiler (Compiler d j) = Compiler d $ mapM j
294+
295+-- | Log and time a compiler
296+--
297+timedCompiler :: String -- ^ Message
298+ -> Compiler a b -- ^ Compiler to time
299+ -> Compiler a b -- ^ Resulting compiler
300+timedCompiler msg (Compiler d j) = Compiler d $ \x -> CompilerM $ do
301+ logger <- compilerLogger <$> ask
302+ timed logger msg $ unCompilerM $ j x
303+
304+-- | Choose a compiler by extension
305+--
306+-- Example:
307+--
308+-- > route "css/*" $ setExtension "css"
309+-- > compile "css/*" $ byExtension (error "Not a (S)CSS file")
310+-- > [ (".css", compressCssCompiler)
311+-- > , (".scss", sass)
312+-- > ]
313+--
314+-- This piece of code will select the @compressCssCompiler@ for @.css@ files,
315+-- and the @sass@ compiler (defined elsewhere) for @.scss@ files.
316+--
317+byExtension :: Compiler a b -- ^ Default compiler
318+ -> [(String, Compiler a b)] -- ^ Choices
319+ -> Compiler a b -- ^ Resulting compiler
320+byExtension defaultCompiler choices = Compiler deps job
321+ where
322+ -- Lookup the compiler, give an error when it is not found
323+ lookup' identifier =
324+ let extension = takeExtension $ toFilePath identifier
325+ in fromMaybe defaultCompiler $ lookup extension choices
326+ -- Collect the dependencies of the choice
327+ deps = do
328+ identifier <- dependencyIdentifier <$> ask
329+ compilerDependencies $ lookup' identifier
330+ -- Collect the job of the choice
331+ job x = CompilerM $ do
332+ identifier <- compilerIdentifier <$> ask
333+ unCompilerM $ compilerJob (lookup' identifier) x
src/Hakyll/Core/Compiler/Internal.hsnew file+146
@@ -0,0 +1,146 @@
1+-- | Internally used compiler module
2+--
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+module Hakyll.Core.Compiler.Internal
5+ ( Dependencies
6+ , DependencyEnvironment (..)
7+ , CompilerEnvironment (..)
8+ , CompilerM (..)
9+ , Compiler (..)
10+ , runCompilerJob
11+ , runCompilerDependencies
12+ , fromJob
13+ , fromDependencies
14+ , fromDependency
15+ ) where
16+
17+import Prelude hiding ((.), id)
18+import Control.Applicative (Applicative, pure, (<*>), (<$>))
19+import Control.Monad.Reader (ReaderT, Reader, ask, runReaderT, runReader)
20+import Control.Monad ((<=<), liftM2)
21+import Data.Set (Set)
22+import qualified Data.Set as S
23+import Control.Category (Category, (.), id)
24+import Control.Arrow (Arrow, ArrowChoice, arr, first, left)
25+
26+import Hakyll.Core.Identifier
27+import Hakyll.Core.ResourceProvider
28+import Hakyll.Core.Store
29+import Hakyll.Core.Routes
30+import Hakyll.Core.Logger
31+
32+-- | A set of dependencies
33+--
34+type Dependencies = Set Identifier
35+
36+-- | Environment in which the dependency analyzer runs
37+--
38+data DependencyEnvironment = DependencyEnvironment
39+ { -- | Target identifier
40+ dependencyIdentifier :: Identifier
41+ , -- | Resource provider
42+ dependencyResourceProvider :: ResourceProvider
43+ }
44+
45+-- | Environment in which a compiler runs
46+--
47+data CompilerEnvironment = CompilerEnvironment
48+ { -- | Target identifier
49+ compilerIdentifier :: Identifier
50+ , -- | Resource provider
51+ compilerResourceProvider :: ResourceProvider
52+ , -- | Site routes
53+ compilerRoutes :: Routes
54+ , -- | Compiler store
55+ compilerStore :: Store
56+ , -- | Flag indicating if the underlying resource was modified
57+ compilerResourceModified :: Bool
58+ , -- | Logger
59+ compilerLogger :: Logger
60+ }
61+
62+-- | The compiler monad
63+--
64+newtype CompilerM a = CompilerM
65+ { unCompilerM :: ReaderT CompilerEnvironment IO a
66+ } deriving (Monad, Functor, Applicative)
67+
68+-- | The compiler arrow
69+--
70+data Compiler a b = Compiler
71+ { compilerDependencies :: Reader DependencyEnvironment Dependencies
72+ , compilerJob :: a -> CompilerM b
73+ }
74+
75+instance Functor (Compiler a) where
76+ fmap f ~(Compiler d j) = Compiler d $ fmap f . j
77+
78+instance Applicative (Compiler a) where
79+ pure = Compiler (return S.empty) . const . return
80+ ~(Compiler d1 f) <*> ~(Compiler d2 j) =
81+ Compiler (liftM2 S.union d1 d2) $ \x -> f x <*> j x
82+
83+instance Category Compiler where
84+ id = Compiler (return S.empty) return
85+ ~(Compiler d1 j1) . ~(Compiler d2 j2) =
86+ Compiler (liftM2 S.union d1 d2) (j1 <=< j2)
87+
88+instance Arrow Compiler where
89+ arr f = Compiler (return S.empty) (return . f)
90+ first ~(Compiler d j) = Compiler d $ \(x, y) -> do
91+ x' <- j x
92+ return (x', y)
93+
94+instance ArrowChoice Compiler where
95+ left ~(Compiler d j) = Compiler d $ \e -> case e of
96+ Left l -> Left <$> j l
97+ Right r -> Right <$> return r
98+
99+-- | Run a compiler, yielding the resulting target and it's dependencies
100+--
101+runCompilerJob :: Compiler () a -- ^ Compiler to run
102+ -> Identifier -- ^ Target identifier
103+ -> ResourceProvider -- ^ Resource provider
104+ -> Routes -- ^ Route
105+ -> Store -- ^ Store
106+ -> Bool -- ^ Was the resource modified?
107+ -> Logger -- ^ Logger
108+ -> IO a
109+runCompilerJob compiler identifier provider route store modified logger =
110+ runReaderT (unCompilerM $ compilerJob compiler ()) env
111+ where
112+ env = CompilerEnvironment
113+ { compilerIdentifier = identifier
114+ , compilerResourceProvider = provider
115+ , compilerRoutes = route
116+ , compilerStore = store
117+ , compilerResourceModified = modified
118+ , compilerLogger = logger
119+ }
120+
121+runCompilerDependencies :: Compiler () a
122+ -> Identifier
123+ -> ResourceProvider
124+ -> Dependencies
125+runCompilerDependencies compiler identifier provider =
126+ runReader (compilerDependencies compiler) env
127+ where
128+ env = DependencyEnvironment
129+ { dependencyIdentifier = identifier
130+ , dependencyResourceProvider = provider
131+ }
132+
133+fromJob :: (a -> CompilerM b)
134+ -> Compiler a b
135+fromJob = Compiler (return S.empty)
136+
137+fromDependencies :: (Identifier -> ResourceProvider -> [Identifier])
138+ -> Compiler b b
139+fromDependencies collectDeps = flip Compiler return $ do
140+ DependencyEnvironment identifier provider <- ask
141+ return $ S.fromList $ collectDeps identifier provider
142+
143+-- | Wait until another compiler has finished before running this compiler
144+--
145+fromDependency :: Identifier -> Compiler a a
146+fromDependency = fromDependencies . const . const . return
src/Hakyll/Core/Configuration.hsnew file+44
@@ -0,0 +1,44 @@
1+-- | Exports a datastructure for the top-level hakyll configuration
2+--
3+module Hakyll.Core.Configuration
4+ ( HakyllConfiguration (..)
5+ , defaultHakyllConfiguration
6+ ) where
7+
8+import System.FilePath (takeFileName)
9+import Data.List (isPrefixOf, isSuffixOf)
10+
11+data HakyllConfiguration = HakyllConfiguration
12+ { -- | Directory in which the output written
13+ destinationDirectory :: FilePath
14+ , -- | Directory where hakyll's internal store is kept
15+ storeDirectory :: FilePath
16+ , -- | Function to determine ignored files
17+ --
18+ -- In 'defaultHakyllConfiguration', the following files are ignored:
19+ --
20+ -- * files starting with a @.@
21+ --
22+ -- * files ending with a @~@
23+ --
24+ -- * files ending with @.swp@
25+ --
26+ ignoreFile :: FilePath -> Bool
27+ }
28+
29+-- | Default configuration for a hakyll application
30+--
31+defaultHakyllConfiguration :: HakyllConfiguration
32+defaultHakyllConfiguration = HakyllConfiguration
33+ { destinationDirectory = "_site"
34+ , storeDirectory = "_cache"
35+ , ignoreFile = ignoreFile'
36+ }
37+ where
38+ ignoreFile' path
39+ | "." `isPrefixOf` fileName = True
40+ | "~" `isSuffixOf` fileName = True
41+ | ".swp" `isSuffixOf` fileName = True
42+ | otherwise = False
43+ where
44+ fileName = takeFileName path
src/Hakyll/Core/CopyFile.hsnew file+29
@@ -0,0 +1,29 @@
1+-- | Exports simple compilers to just copy files
2+--
3+{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-}
4+module Hakyll.Core.CopyFile
5+ ( CopyFile (..)
6+ , copyFileCompiler
7+ ) where
8+
9+import Control.Arrow ((>>^))
10+import System.Directory (copyFile)
11+
12+import Data.Typeable (Typeable)
13+import Data.Binary (Binary)
14+
15+import Hakyll.Core.ResourceProvider
16+import Hakyll.Core.Writable
17+import Hakyll.Core.Compiler
18+import Hakyll.Core.Identifier
19+
20+-- | Newtype construct around 'FilePath' which will copy the file directly
21+--
22+newtype CopyFile = CopyFile {unCopyFile :: FilePath}
23+ deriving (Show, Eq, Ord, Binary, Typeable)
24+
25+instance Writable CopyFile where
26+ write dst (CopyFile src) = copyFile src dst
27+
28+copyFileCompiler :: Compiler Resource CopyFile
29+copyFileCompiler = getIdentifier >>^ CopyFile . toFilePath
src/Hakyll/Core/DirectedGraph.hsnew file+85
@@ -0,0 +1,85 @@
1+-- | Representation of a directed graph. In Hakyll, this is used for dependency
2+-- tracking.
3+--
4+module Hakyll.Core.DirectedGraph
5+ ( DirectedGraph
6+ , fromList
7+ , member
8+ , nodes
9+ , neighbours
10+ , reverse
11+ , reachableNodes
12+ , sanitize
13+ ) where
14+
15+import Prelude hiding (reverse)
16+import Data.Monoid (mconcat)
17+import Data.Set (Set)
18+import Data.Maybe (fromMaybe)
19+import qualified Data.Map as M
20+import qualified Data.Set as S
21+
22+import Hakyll.Core.DirectedGraph.Internal
23+
24+-- | Construction of directed graphs
25+--
26+fromList :: Ord a
27+ => [(a, Set a)] -- ^ List of (node, reachable neighbours)
28+ -> DirectedGraph a -- ^ Resulting directed graph
29+fromList = DirectedGraph . M.fromList . map (\(t, d) -> (t, Node t d))
30+
31+-- | Check if a node lies in the given graph
32+--
33+member :: Ord a
34+ => a -- ^ Node to check for
35+ -> DirectedGraph a -- ^ Directed graph to check in
36+ -> Bool -- ^ If the node lies in the graph
37+member n = M.member n . unDirectedGraph
38+
39+-- | Get all nodes in the graph
40+--
41+nodes :: Ord a
42+ => DirectedGraph a -- ^ Graph to get the nodes from
43+ -> Set a -- ^ All nodes in the graph
44+nodes = M.keysSet . unDirectedGraph
45+
46+-- | Get a set of reachable neighbours from a directed graph
47+--
48+neighbours :: Ord a
49+ => a -- ^ Node to get the neighbours of
50+ -> DirectedGraph a -- ^ Graph to search in
51+ -> Set a -- ^ Set containing the neighbours
52+neighbours x = fromMaybe S.empty . fmap nodeNeighbours
53+ . M.lookup x . unDirectedGraph
54+
55+-- | Reverse a directed graph (i.e. flip all edges)
56+--
57+reverse :: Ord a
58+ => DirectedGraph a
59+ -> DirectedGraph a
60+reverse = mconcat . map reverse' . M.toList . unDirectedGraph
61+ where
62+ reverse' (id', Node _ neighbours') = fromList $
63+ zip (S.toList neighbours') $ repeat $ S.singleton id'
64+
65+-- | Find all reachable nodes from a given set of nodes in the directed graph
66+--
67+reachableNodes :: Ord a => Set a -> DirectedGraph a -> Set a
68+reachableNodes set graph = reachable (setNeighbours set) set
69+ where
70+ reachable next visited
71+ | S.null next = visited
72+ | otherwise = reachable (sanitize' neighbours') (next `S.union` visited)
73+ where
74+ sanitize' = S.filter (`S.notMember` visited)
75+ neighbours' = setNeighbours (sanitize' next)
76+
77+ setNeighbours = S.unions . map (`neighbours` graph) . S.toList
78+
79+-- | Remove all dangling pointers, i.e. references to notes that do
80+-- not actually exist in the graph.
81+--
82+sanitize :: Ord a => DirectedGraph a -> DirectedGraph a
83+sanitize (DirectedGraph graph) = DirectedGraph $ M.map sanitize' graph
84+ where
85+ sanitize' (Node t n) = Node t $ S.filter (`M.member` graph) n
src/Hakyll/Core/DirectedGraph/DependencySolver.hsnew file+70
@@ -0,0 +1,70 @@
1+-- | Given a dependency graph, this module provides a function that will
2+-- generate an order in which the graph can be visited, so that all the
3+-- dependencies of a given node have been visited before the node itself is
4+-- visited.
5+--
6+module Hakyll.Core.DirectedGraph.DependencySolver
7+ ( solveDependencies
8+ ) where
9+
10+import Prelude
11+import qualified Prelude as P
12+import Data.Set (Set)
13+import Data.Maybe (mapMaybe)
14+import qualified Data.Map as M
15+import qualified Data.Set as S
16+
17+import Hakyll.Core.DirectedGraph
18+import Hakyll.Core.DirectedGraph.Internal
19+
20+-- | Solve a dependency graph. This function returns an order to run the
21+-- different nodes
22+--
23+solveDependencies :: Ord a
24+ => DirectedGraph a -- ^ Graph
25+ -> [a] -- ^ Resulting plan
26+solveDependencies = P.reverse . order [] [] S.empty
27+
28+-- | Produce a reversed order using a stack
29+--
30+order :: Ord a
31+ => [a] -- ^ Temporary result
32+ -> [Node a] -- ^ Backtrace stack
33+ -> Set a -- ^ Items in the stack
34+ -> DirectedGraph a -- ^ Graph
35+ -> [a] -- ^ Ordered result
36+order temp stack set graph@(DirectedGraph graph')
37+ -- Empty graph - return our current result
38+ | M.null graph' = temp
39+ | otherwise = case stack of
40+
41+ -- Empty stack - pick a node, and add it to the stack
42+ [] ->
43+ let (tag, node) = M.findMin graph'
44+ in order temp (node : stack) (S.insert tag set) graph
45+
46+ -- At least one item on the stack - continue using this item
47+ (node : stackTail) ->
48+ -- Check which dependencies are still in the graph
49+ let tag = nodeTag node
50+ deps = S.toList $ nodeNeighbours node
51+ unsatisfied = mapMaybe (`M.lookup` graph') deps
52+ in case unsatisfied of
53+
54+ -- All dependencies for node are satisfied, we can return it and
55+ -- remove it from the graph
56+ [] -> order (tag : temp) stackTail (S.delete tag set)
57+ (DirectedGraph $ M.delete tag graph')
58+
59+ -- There is at least one dependency left. We need to solve that
60+ -- one first...
61+ (dep : _) -> if nodeTag dep `S.member` set
62+ -- The dependency is already in our stack - cycle detected!
63+ then cycleError
64+ -- Continue with the dependency
65+ else order temp (dep : node : stackTail)
66+ (S.insert (nodeTag dep) set)
67+ graph
68+ where
69+ cycleError = error $ "Hakyll.Core.DirectedGraph.DependencySolver.order: "
70+ ++ "Cycle detected!" -- TODO: Dump cycle
src/Hakyll/Core/DirectedGraph/Dot.hsnew file+30
@@ -0,0 +1,30 @@
1+-- | Dump a directed graph in dot format. Used for debugging purposes
2+--
3+module Hakyll.Core.DirectedGraph.Dot
4+ ( toDot
5+ , writeDot
6+ ) where
7+
8+import Hakyll.Core.DirectedGraph
9+import qualified Data.Set as S
10+
11+-- | Convert a directed graph into dot format for debugging purposes
12+--
13+toDot :: Ord a
14+ => (a -> String) -- ^ Convert nodes to dot names
15+ -> DirectedGraph a -- ^ Graph to dump
16+ -> String -- ^ Resulting string
17+toDot showTag graph = unlines $ concat
18+ [ return "digraph dependencies {"
19+ , concatMap showNode (S.toList $ nodes graph)
20+ , return "}"
21+ ]
22+ where
23+ showNode node = map (showEdge node) $ S.toList $ neighbours node graph
24+ showEdge x y = " \"" ++ showTag x ++ "\" -> \"" ++ showTag y ++ "\";"
25+
26+-- | Write out the @.dot@ file to a given file path. See 'toDot' for more
27+-- information.
28+--
29+writeDot :: Ord a => FilePath -> (a -> String) -> DirectedGraph a -> IO ()
30+writeDot path showTag = writeFile path . toDot showTag
src/Hakyll/Core/DirectedGraph/Internal.hsnew file+43
@@ -0,0 +1,43 @@
1+-- | Internal structure of the DirectedGraph type. Not exported outside of the
2+-- library.
3+--
4+module Hakyll.Core.DirectedGraph.Internal
5+ ( Node (..)
6+ , DirectedGraph (..)
7+ ) where
8+
9+import Prelude hiding (reverse, filter)
10+import Data.Monoid (Monoid, mempty, mappend)
11+import Data.Set (Set)
12+import Data.Map (Map)
13+import qualified Data.Map as M
14+import qualified Data.Set as S
15+
16+-- | A node in the directed graph
17+--
18+data Node a = Node
19+ { nodeTag :: a -- ^ Tag identifying the node
20+ , nodeNeighbours :: Set a -- ^ Edges starting at this node
21+ } deriving (Show)
22+
23+-- | Append two nodes. Useful for joining graphs.
24+--
25+appendNodes :: Ord a => Node a -> Node a -> Node a
26+appendNodes (Node t1 n1) (Node t2 n2)
27+ | t1 /= t2 = error'
28+ | otherwise = Node t1 (n1 `S.union` n2)
29+ where
30+ error' = error $ "Hakyll.Core.DirectedGraph.Internal.appendNodes: "
31+ ++ "Appending differently tagged nodes"
32+
33+-- | Type used to represent a directed graph
34+--
35+newtype DirectedGraph a = DirectedGraph {unDirectedGraph :: Map a (Node a)}
36+ deriving (Show)
37+
38+-- | Allow users to concatenate different graphs
39+--
40+instance Ord a => Monoid (DirectedGraph a) where
41+ mempty = DirectedGraph M.empty
42+ mappend (DirectedGraph m1) (DirectedGraph m2) = DirectedGraph $
43+ M.unionWith appendNodes m1 m2
src/Hakyll/Core/Identifier.hsnew file+59
@@ -0,0 +1,59 @@
1+-- | An identifier is a type used to uniquely identify a resource, target...
2+--
3+-- One can think of an identifier as something similar to a file path. An
4+-- identifier is a path as well, with the different elements in the path
5+-- separated by @/@ characters. Examples of identifiers are:
6+--
7+-- * @posts/foo.markdown@
8+--
9+-- * @index@
10+--
11+-- * @error/404@
12+--
13+-- The most important difference between an 'Identifier' and a file path is that
14+-- the identifier for an item is not necesserily the file path.
15+--
16+-- For example, we could have an @index@ identifier, generated by Hakyll. The
17+-- actual file path would be @index.html@, but we identify it using @index@.
18+--
19+-- @posts/foo.markdown@ could be an identifier of an item that is rendered to
20+-- @posts/foo.html@. In this case, the identifier is the name of the source
21+-- file of the page.
22+--
23+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
24+module Hakyll.Core.Identifier
25+ ( Identifier (..)
26+ , parseIdentifier
27+ , toFilePath
28+ ) where
29+
30+import Control.Arrow (second)
31+import Data.Monoid (Monoid)
32+
33+import GHC.Exts (IsString, fromString)
34+import System.FilePath (joinPath)
35+
36+-- | An identifier used to uniquely identify a value
37+--
38+newtype Identifier = Identifier {unIdentifier :: [String]}
39+ deriving (Eq, Ord, Monoid)
40+
41+instance Show Identifier where
42+ show = toFilePath
43+
44+instance IsString Identifier where
45+ fromString = parseIdentifier
46+
47+-- | Parse an identifier from a string
48+--
49+parseIdentifier :: String -> Identifier
50+parseIdentifier = Identifier . filter (not . null) . split'
51+ where
52+ split' [] = [[]]
53+ split' str = let (pre, post) = second (drop 1) $ break (== '/') str
54+ in pre : split' post
55+
56+-- | Convert an identifier to a relative 'FilePath'
57+--
58+toFilePath :: Identifier -> FilePath
59+toFilePath = joinPath . unIdentifier
src/Hakyll/Core/Identifier/Pattern.hsnew file+160
@@ -0,0 +1,160 @@
1+-- | Module providing pattern matching and capturing on 'Identifier's.
2+--
3+-- A very simple pattern could be, for example, @foo\/bar@. This pattern will
4+-- only match the exact @foo\/bar@ identifier.
5+--
6+-- To match more than one identifier, there are different captures that one can
7+-- use:
8+--
9+-- * @*@: matches exactly one element of an identifier;
10+--
11+-- * @**@: matches one or more elements of an identifier.
12+--
13+-- Some examples:
14+--
15+-- * @foo\/*@ will match @foo\/bar@ and @foo\/foo@, but not @foo\/bar\/qux@ nor
16+-- @foo@;
17+--
18+-- * @**@ will match any non-empty identifier;
19+--
20+-- * @foo\/**@ will match @foo\/bar@ and @foo\/bar\/qux@, but not @bar\/foo@ nor
21+-- @foo@;
22+--
23+-- A small warning: patterns are not globs. Using @foo\/*.markdown@ will not do
24+-- what you probably intended, as it will only match the file which is literally
25+-- called @foo\/*.markdown@. Remember that these captures only work on elements
26+-- of identifiers as a whole; not on parts of these elements.
27+--
28+-- Furthermore, the 'match' function allows the user to get access to the
29+-- elements captured by the capture elements in the pattern.
30+--
31+module Hakyll.Core.Identifier.Pattern
32+ ( Pattern
33+ , parsePattern
34+ , match
35+ , doesMatch
36+ , matches
37+ , fromCapture
38+ , fromCaptureString
39+ , fromCaptures
40+ ) where
41+
42+import Data.List (intercalate)
43+import Control.Monad (msum)
44+import Data.Maybe (isJust)
45+import Data.Monoid (mempty, mappend)
46+
47+import GHC.Exts (IsString, fromString)
48+
49+import Hakyll.Core.Identifier
50+
51+-- | One base element of a pattern
52+--
53+data PatternComponent = CaptureOne
54+ | CaptureMany
55+ | Literal String
56+ deriving (Eq)
57+
58+instance Show PatternComponent where
59+ show CaptureOne = "*"
60+ show CaptureMany = "**"
61+ show (Literal s) = s
62+
63+-- | Type that allows matching on identifiers
64+--
65+newtype Pattern = Pattern {unPattern :: [PatternComponent]}
66+ deriving (Eq)
67+
68+instance Show Pattern where
69+ show = intercalate "/" . map show . unPattern
70+
71+instance IsString Pattern where
72+ fromString = parsePattern
73+
74+-- | Parse a pattern from a string
75+--
76+parsePattern :: String -> Pattern
77+parsePattern = Pattern . map toPattern . unIdentifier . parseIdentifier
78+ where
79+ toPattern x | x == "*" = CaptureOne
80+ | x == "**" = CaptureMany
81+ | otherwise = Literal x
82+
83+-- | Match an identifier against a pattern, generating a list of captures
84+--
85+match :: Pattern -> Identifier -> Maybe [Identifier]
86+match (Pattern p) (Identifier i) = fmap (map Identifier) $ match' p i
87+
88+-- | Check if an identifier matches a pattern
89+--
90+doesMatch :: Pattern -> Identifier -> Bool
91+doesMatch p = isJust . match p
92+
93+-- | Given a list of identifiers, retain only those who match the given pattern
94+--
95+matches :: Pattern -> [Identifier] -> [Identifier]
96+matches p = filter (doesMatch p)
97+
98+-- | Split a list at every possible point, generate a list of (init, tail) cases
99+--
100+splits :: [a] -> [([a], [a])]
101+splits ls = reverse $ splits' [] ls
102+ where
103+ splits' lx ly = (lx, ly) : case ly of
104+ [] -> []
105+ (y : ys) -> splits' (lx ++ [y]) ys
106+
107+-- | Internal verion of 'match'
108+--
109+match' :: [PatternComponent] -> [String] -> Maybe [[String]]
110+match' [] [] = Just [] -- An empty match
111+match' [] _ = Nothing -- No match
112+match' _ [] = Nothing -- No match
113+match' (m : ms) (s : ss) = case m of
114+ -- Take one string and one literal, fail on mismatch
115+ Literal l -> if s == l then match' ms ss else Nothing
116+ -- Take one string and one capture
117+ CaptureOne -> fmap ([s] :) $ match' ms ss
118+ -- Take one string, and one or many captures
119+ CaptureMany ->
120+ let take' (i, t) = fmap (i :) $ match' ms t
121+ in msum $ map take' $ splits (s : ss)
122+
123+-- | Create an identifier from a pattern by filling in the captures with a given
124+-- string
125+--
126+-- Example:
127+--
128+-- > fromCapture (parsePattern "tags/*") (parseIdentifier "foo")
129+--
130+-- Result:
131+--
132+-- > "tags/foo"
133+--
134+fromCapture :: Pattern -> Identifier -> Identifier
135+fromCapture pattern = fromCaptures pattern . repeat
136+
137+-- | Simplified version of 'fromCapture' which takes a 'String' instead of an
138+-- 'Identifier'
139+--
140+-- > fromCaptureString (parsePattern "tags/*") "foo"
141+--
142+-- Result:
143+--
144+-- > "tags/foo"
145+--
146+fromCaptureString :: Pattern -> String -> Identifier
147+fromCaptureString pattern = fromCapture pattern . parseIdentifier
148+
149+-- | Create an identifier from a pattern by filling in the captures with the
150+-- given list of strings
151+--
152+fromCaptures :: Pattern -> [Identifier] -> Identifier
153+fromCaptures (Pattern []) _ = mempty
154+fromCaptures (Pattern (m : ms)) [] = case m of
155+ Literal l -> Identifier [l] `mappend` fromCaptures (Pattern ms) []
156+ _ -> error $ "Hakyll.Core.Identifier.Pattern.fromCaptures: "
157+ ++ "identifier list exhausted"
158+fromCaptures (Pattern (m : ms)) ids@(i : is) = case m of
159+ Literal l -> Identifier [l] `mappend` fromCaptures (Pattern ms) ids
160+ _ -> i `mappend` fromCaptures (Pattern ms) is
src/Hakyll/Core/Logger.hsnew file+90
@@ -0,0 +1,90 @@
1+-- | Produce pretty, thread-safe logs
2+--
3+{-# LANGUAGE BangPatterns #-}
4+module Hakyll.Core.Logger
5+ ( Logger
6+ , makeLogger
7+ , flushLogger
8+ , section
9+ , timed
10+ , report
11+ ) where
12+
13+import Control.Monad (forever)
14+import Control.Monad.Trans (MonadIO, liftIO)
15+import Control.Applicative ((<$>), (<*>))
16+import Control.Concurrent (forkIO)
17+import Control.Concurrent.Chan.Strict (Chan, newChan, readChan, writeChan)
18+import Control.Concurrent.MVar.Strict (MVar, newEmptyMVar, takeMVar, putMVar)
19+import Text.Printf (printf)
20+
21+import Data.Time (getCurrentTime, diffUTCTime)
22+
23+-- | Logger structure. Very complicated.
24+--
25+data Logger = Logger
26+ { loggerChan :: Chan (Maybe String) -- Nothing marks the end
27+ , loggerSync :: MVar () -- Used for sync on quit
28+ }
29+
30+-- | Create a new logger
31+--
32+makeLogger :: IO Logger
33+makeLogger = do
34+ logger <- Logger <$> newChan <*> newEmptyMVar
35+ _ <- forkIO $ loggerThread logger
36+ return logger
37+ where
38+ loggerThread logger = forever $ do
39+ msg <- readChan $ loggerChan logger
40+ case msg of
41+ -- Stop: sync
42+ Nothing -> putMVar (loggerSync logger) ()
43+ -- Print and continue
44+ Just m -> putStrLn m
45+
46+-- | Flush the logger (blocks until flushed)
47+--
48+flushLogger :: Logger -> IO ()
49+flushLogger logger = do
50+ writeChan (loggerChan logger) Nothing
51+ () <- takeMVar $ loggerSync logger
52+ return ()
53+
54+-- | Send a raw message to the logger
55+--
56+message :: Logger -> String -> IO ()
57+message logger = writeChan (loggerChan logger) . Just
58+
59+-- | Start a section in the log
60+--
61+section :: MonadIO m
62+ => Logger -- ^ Logger
63+ -> String -- ^ Section name
64+ -> m () -- ^ No result
65+section logger = liftIO . message logger
66+
67+-- | Execute a monadic action and log the duration
68+--
69+timed :: MonadIO m
70+ => Logger -- ^ Logger
71+ -> String -- ^ Message
72+ -> m a -- ^ Action
73+ -> m a -- ^ Timed and logged action
74+timed logger msg action = do
75+ start <- liftIO getCurrentTime
76+ !result <- action
77+ stop <- liftIO getCurrentTime
78+ let diff = fromEnum $ diffUTCTime stop start
79+ ms = diff `div` 10 ^ (9 :: Int)
80+ formatted = printf " [%4dms] %s" ms msg
81+ liftIO $ message logger formatted
82+ return result
83+
84+-- | Log something at the same level as 'timed', but without the timing
85+--
86+report :: MonadIO m
87+ => Logger -- ^ Logger
88+ -> String -- ^ Message
89+ -> m () -- ^ No result
90+report logger msg = liftIO $ message logger $ " [ ] " ++ msg
src/Hakyll/Core/ResourceProvider.hsnew file+75
@@ -0,0 +1,75 @@
1+-- | This module provides an API for resource providers. Resource providers
2+-- allow Hakyll to get content from resources; the type of resource depends on
3+-- the concrete instance.
4+--
5+-- A resource is represented by the 'Resource' type. This is basically just a
6+-- newtype wrapper around 'Identifier' -- but it has an important effect: it
7+-- guarantees that a resource with this identifier can be provided by one or
8+-- more resource providers.
9+--
10+-- Therefore, it is not recommended to read files directly -- you should use the
11+-- provided 'Resource' methods.
12+--
13+module Hakyll.Core.ResourceProvider
14+ ( Resource (..)
15+ , ResourceProvider (..)
16+ , resourceExists
17+ , resourceDigest
18+ , resourceModified
19+ ) where
20+
21+import Control.Monad ((<=<))
22+import Data.Word (Word8)
23+
24+import qualified Data.ByteString.Lazy as LB
25+import OpenSSL.Digest.ByteString.Lazy (digest)
26+import OpenSSL.Digest (MessageDigest (MD5))
27+
28+import Hakyll.Core.Identifier
29+import Hakyll.Core.Store
30+
31+-- | A resource
32+--
33+-- Invariant: the resource specified by the given identifier must exist
34+--
35+newtype Resource = Resource {unResource :: Identifier}
36+ deriving (Eq, Show, Ord)
37+
38+-- | A value responsible for retrieving and listing resources
39+--
40+data ResourceProvider = ResourceProvider
41+ { -- | A list of all resources this provider is able to provide
42+ resourceList :: [Resource]
43+ , -- | Retrieve a certain resource as string
44+ resourceString :: Resource -> IO String
45+ , -- | Retrieve a certain resource as lazy bytestring
46+ resourceLazyByteString :: Resource -> IO LB.ByteString
47+ }
48+
49+-- | Check if a given identifier has a resource
50+--
51+resourceExists :: ResourceProvider -> Identifier -> Bool
52+resourceExists provider = flip elem $ map unResource $ resourceList provider
53+
54+-- | Retrieve a digest for a given resource
55+--
56+resourceDigest :: ResourceProvider -> Resource -> IO [Word8]
57+resourceDigest provider = digest MD5 <=< resourceLazyByteString provider
58+
59+-- | Check if a resource was modified
60+--
61+resourceModified :: ResourceProvider -> Resource -> Store -> IO Bool
62+resourceModified provider resource store = do
63+ -- Get the latest seen digest from the store
64+ lastDigest <- storeGet store itemName $ unResource resource
65+ -- Calculate the digest for the resource
66+ newDigest <- resourceDigest provider resource
67+ -- Check digests
68+ if Just newDigest == lastDigest
69+ -- All is fine, not modified
70+ then return False
71+ -- Resource modified; store new digest
72+ else do storeSet store itemName (unResource resource) newDigest
73+ return True
74+ where
75+ itemName = "Hakyll.Core.ResourceProvider.resourceModified"
src/Hakyll/Core/ResourceProvider/FileResourceProvider.hsnew file+29
@@ -0,0 +1,29 @@
1+-- | A concrete 'ResourceProvider' that gets it's resources from the filesystem
2+--
3+module Hakyll.Core.ResourceProvider.FileResourceProvider
4+ ( fileResourceProvider
5+ ) where
6+
7+import Control.Applicative ((<$>))
8+
9+import qualified Data.ByteString.Lazy as LB
10+
11+import Hakyll.Core.ResourceProvider
12+import Hakyll.Core.Identifier
13+import Hakyll.Core.Util.File
14+import Hakyll.Core.Configuration
15+
16+-- | Create a filesystem-based 'ResourceProvider'
17+--
18+fileResourceProvider :: HakyllConfiguration -> IO ResourceProvider
19+fileResourceProvider configuration = do
20+ -- Retrieve a list of identifiers
21+ list <- map parseIdentifier . filter (not . ignoreFile configuration) <$>
22+ getRecursiveContents False "."
23+
24+ -- Construct a resource provider
25+ return ResourceProvider
26+ { resourceList = map Resource list
27+ , resourceString = readFile . toFilePath . unResource
28+ , resourceLazyByteString = LB.readFile . toFilePath . unResource
29+ }
src/Hakyll/Core/Routes.hsnew file+136
@@ -0,0 +1,136 @@
1+-- | Once a target is compiled, the user usually wants to save it to the disk.
2+-- This is where the 'Routes' type comes in; it determines where a certain
3+-- target should be written.
4+--
5+-- Suppose we have an item @foo\/bar.markdown@. We can render this to
6+-- @foo\/bar.html@ using:
7+--
8+-- > route "foo/bar.markdown" (setExtension ".html")
9+--
10+-- If we do not want to change the extension, we can use 'idRoute', the simplest
11+-- route available:
12+--
13+-- > route "foo/bar.markdown" idRoute
14+--
15+-- That will route @foo\/bar.markdown@ to @foo\/bar.markdown@.
16+--
17+-- Note that the extension says nothing about the content! If you set the
18+-- extension to @.html@, it is your own responsibility to ensure that the
19+-- content is indeed HTML.
20+--
21+-- Finally, some special cases:
22+--
23+-- * If there is no route for an item, this item will not be routed, so it will
24+-- not appear in your site directory.
25+--
26+-- * If an item matches multiple routes, the first rule will be chosen.
27+--
28+module Hakyll.Core.Routes
29+ ( Routes
30+ , runRoutes
31+ , idRoute
32+ , setExtension
33+ , ifMatch
34+ , customRoute
35+ , gsubRoute
36+ , composeRoutes
37+ ) where
38+
39+import Data.Monoid (Monoid, mempty, mappend)
40+import Control.Monad (mplus)
41+import System.FilePath (replaceExtension)
42+
43+import Hakyll.Core.Identifier
44+import Hakyll.Core.Identifier.Pattern
45+import Hakyll.Core.Util.String
46+
47+-- | Type used for a route
48+--
49+newtype Routes = Routes {unRoutes :: Identifier -> Maybe FilePath}
50+
51+instance Monoid Routes where
52+ mempty = Routes $ const Nothing
53+ mappend (Routes f) (Routes g) = Routes $ \id' -> f id' `mplus` g id'
54+
55+-- | Apply a route to an identifier
56+--
57+runRoutes :: Routes -> Identifier -> Maybe FilePath
58+runRoutes = unRoutes
59+
60+-- | A route that uses the identifier as filepath. For example, the target with
61+-- ID @foo\/bar@ will be written to the file @foo\/bar@.
62+--
63+idRoute :: Routes
64+idRoute = Routes $ Just . toFilePath
65+
66+-- | Set (or replace) the extension of a route.
67+--
68+-- Example:
69+--
70+-- > runRoute (setExtension "html") "foo/bar"
71+--
72+-- Result:
73+--
74+-- > Just "foo/bar.html"
75+--
76+-- Example:
77+--
78+-- > runRoute (setExtension "html") "posts/the-art-of-trolling.markdown"
79+--
80+-- Result:
81+--
82+-- > Just "posts/the-art-of-trolling.html"
83+--
84+setExtension :: String -> Routes
85+setExtension extension = Routes $ fmap (`replaceExtension` extension)
86+ . unRoutes idRoute
87+
88+-- | Modify a route: apply the route if the identifier matches the given
89+-- pattern, fail otherwise.
90+--
91+ifMatch :: Pattern -> Routes -> Routes
92+ifMatch pattern (Routes route) = Routes $ \id' ->
93+ if doesMatch pattern id' then route id'
94+ else Nothing
95+
96+-- | Create a custom route. This should almost always be used with 'ifMatch'.
97+--
98+customRoute :: (Identifier -> FilePath) -> Routes
99+customRoute f = Routes $ Just . f
100+
101+-- | Create a gsub route
102+--
103+-- Example:
104+--
105+-- > runRoutes (gsubRoute "rss/" (const "")) "tags/rss/bar.xml"
106+--
107+-- Result:
108+--
109+-- > Just "tags/bar.xml"
110+--
111+gsubRoute :: String -- ^ Pattern
112+ -> (String -> String) -- ^ Replacement
113+ -> Routes -- ^ Resulting route
114+gsubRoute pattern replacement = customRoute $
115+ replaceAll pattern replacement . toFilePath
116+
117+-- | Compose routes so that @f `composeRoutes` g@ is more or less equivalent
118+-- with @f >>> g@.
119+--
120+-- Example:
121+--
122+-- > let routes = gsubRoute "rss/" (const "") `composeRoutes` setExtension "xml"
123+-- > in runRoutes routes "tags/rss/bar"
124+--
125+-- Result:
126+--
127+-- > Just "tags/bar.xml"
128+--
129+-- If the first route given fails, Hakyll will not apply the second route.
130+--
131+composeRoutes :: Routes -- ^ First route to apply
132+ -> Routes -- ^ Second route to apply
133+ -> Routes -- ^ Resulting route
134+composeRoutes (Routes f) (Routes g) = Routes $ \i -> do
135+ p <- f i
136+ g $ parseIdentifier p
src/Hakyll/Core/Rules.hsnew file+161
@@ -0,0 +1,161 @@
1+-- | This module provides a declarative DSL in which the user can specify the
2+-- different rules used to run the compilers.
3+--
4+-- The convention is to just list all items in the 'RulesM' monad, routes and
5+-- compilation rules.
6+--
7+-- A typical usage example would be:
8+--
9+-- > main = hakyll $ do
10+-- > route "posts/*" (setExtension "html")
11+-- > compile "posts/*" someCompiler
12+--
13+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}
14+module Hakyll.Core.Rules
15+ ( RulesM
16+ , Rules
17+ , compile
18+ , create
19+ , route
20+ , metaCompile
21+ , metaCompileWith
22+ ) where
23+
24+import Control.Applicative ((<$>))
25+import Control.Monad.Writer (tell)
26+import Control.Monad.Reader (ask)
27+import Control.Arrow (second, (>>>), arr, (>>^))
28+import Control.Monad.State (get, put)
29+import Data.Monoid (mempty)
30+import qualified Data.Set as S
31+
32+import Data.Typeable (Typeable)
33+import Data.Binary (Binary)
34+
35+import Hakyll.Core.ResourceProvider
36+import Hakyll.Core.Identifier
37+import Hakyll.Core.Identifier.Pattern
38+import Hakyll.Core.Compiler.Internal
39+import Hakyll.Core.Routes
40+import Hakyll.Core.CompiledItem
41+import Hakyll.Core.Writable
42+import Hakyll.Core.Rules.Internal
43+import Hakyll.Core.Util.Arrow
44+
45+-- | Add a route
46+--
47+tellRoute :: Routes -> Rules
48+tellRoute route' = RulesM $ tell $ RuleSet route' mempty mempty
49+
50+-- | Add a number of compilers
51+--
52+tellCompilers :: (Binary a, Typeable a, Writable a)
53+ => [(Identifier, Compiler () a)]
54+ -> Rules
55+tellCompilers compilers = RulesM $ tell $ RuleSet mempty compilers' mempty
56+ where
57+ compilers' = map (second boxCompiler) compilers
58+ boxCompiler = (>>> arr compiledItem >>> arr CompileRule)
59+
60+-- | Add resources
61+--
62+tellResources :: [Resource]
63+ -> Rules
64+tellResources resources = RulesM $ tell $ RuleSet mempty mempty $ S.fromList resources
65+
66+-- | Add a compilation rule to the rules.
67+--
68+-- This instructs all resources matching the given pattern to be compiled using
69+-- the given compiler. When no resources match the given pattern, nothing will
70+-- happen. In this case, you might want to have a look at 'create'.
71+--
72+compile :: (Binary a, Typeable a, Writable a)
73+ => Pattern -> Compiler Resource a -> Rules
74+compile pattern compiler = RulesM $ do
75+ identifiers <- matches pattern . map unResource . resourceList <$> ask
76+ unRulesM $ do
77+ tellCompilers $ flip map identifiers $ \identifier ->
78+ (identifier, constA (Resource identifier) >>> compiler)
79+ tellResources $ map Resource identifiers
80+
81+-- | Add a compilation rule
82+--
83+-- This sets a compiler for the given identifier. No resource is needed, since
84+-- we are creating the item from scratch. This is useful if you want to create a
85+-- page on your site that just takes content from other items -- but has no
86+-- actual content itself.
87+--
88+create :: (Binary a, Typeable a, Writable a)
89+ => Identifier -> Compiler () a -> Rules
90+create identifier compiler = tellCompilers [(identifier, compiler)]
91+
92+-- | Add a route.
93+--
94+-- This adds a route for all items matching the given pattern.
95+--
96+route :: Pattern -> Routes -> Rules
97+route pattern route' = tellRoute $ ifMatch pattern route'
98+
99+-- | Apart from regular compilers, one is also able to specify metacompilers.
100+-- Metacompilers are a special class of compilers: they are compilers which
101+-- produce other compilers.
102+--
103+-- This is needed when the list of compilers depends on something we cannot know
104+-- before actually running other compilers. The most typical example is if we
105+-- have a blogpost using tags.
106+--
107+-- Every post has a collection of tags. For example,
108+--
109+-- > post1: code, haskell
110+-- > post2: code, random
111+--
112+-- Now, we want to create a list of posts for every tag. We cannot write this
113+-- down in our 'Rules' DSL directly, since we don't know what tags the different
114+-- posts will have -- we depend on information that will only be available when
115+-- we are actually compiling the pages.
116+--
117+-- The solution is simple, using 'metaCompile', we can add a compiler that will
118+-- parse the pages and produce the compilers needed for the different tag pages.
119+--
120+-- And indeed, we can see that the first argument to 'metaCompile' is a
121+-- 'Compiler' which produces a list of ('Identifier', 'Compiler') pairs. The
122+-- idea is simple: 'metaCompile' produces a list of compilers, and the
123+-- corresponding identifiers.
124+--
125+-- For simple hakyll systems, it is no need for this construction. More
126+-- formally, it is only needed when the content of one or more items determines
127+-- which items must be rendered.
128+--
129+metaCompile :: (Binary a, Typeable a, Writable a)
130+ => Compiler () [(Identifier, Compiler () a)]
131+ -- ^ Compiler generating the other compilers
132+ -> Rules
133+ -- ^ Resulting rules
134+metaCompile compiler = RulesM $ do
135+ -- Create an identifier from the state
136+ state <- get
137+ let index = rulesMetaCompilerIndex state
138+ id' = fromCaptureString "Hakyll.Core.Rules.metaCompile/*" (show index)
139+
140+ -- Update the state with a new identifier
141+ put $ state {rulesMetaCompilerIndex = index + 1}
142+
143+ -- Fallback to 'metaCompileWith' with now known identifier
144+ unRulesM $ metaCompileWith id' compiler
145+
146+-- | Version of 'metaCompile' that allows you to specify a custom identifier for
147+-- the metacompiler.
148+--
149+metaCompileWith :: (Binary a, Typeable a, Writable a)
150+ => Identifier
151+ -- ^ Identifier for this compiler
152+ -> Compiler () [(Identifier, Compiler () a)]
153+ -- ^ Compiler generating the other compilers
154+ -> Rules
155+ -- ^ Resulting rules
156+metaCompileWith identifier compiler = RulesM $ tell $
157+ RuleSet mempty compilers mempty
158+ where
159+ makeRule = MetaCompileRule . map (second box)
160+ compilers = [(identifier, compiler >>> arr makeRule )]
161+ box = (>>> fromDependency identifier >>^ CompileRule . compiledItem)
src/Hakyll/Core/Rules/Internal.hsnew file+75
@@ -0,0 +1,75 @@
1+-- | Internal rules module for types which are not exposed to the user
2+--
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+module Hakyll.Core.Rules.Internal
5+ ( CompileRule (..)
6+ , RuleSet (..)
7+ , RuleState (..)
8+ , RulesM (..)
9+ , Rules
10+ , runRules
11+ ) where
12+
13+import Control.Applicative (Applicative)
14+import Control.Monad.Writer (WriterT, execWriterT)
15+import Control.Monad.Reader (ReaderT, runReaderT)
16+import Control.Monad.State (State, evalState)
17+import Data.Monoid (Monoid, mempty, mappend)
18+import Data.Set (Set)
19+
20+import Hakyll.Core.ResourceProvider
21+import Hakyll.Core.Identifier
22+import Hakyll.Core.Compiler.Internal
23+import Hakyll.Core.Routes
24+import Hakyll.Core.CompiledItem
25+
26+-- | Output of a compiler rule
27+--
28+-- * The compiler will produce a simple item. This is the most common case.
29+--
30+-- * The compiler will produce more compilers. These new compilers need to be
31+-- added to the runtime if possible, since other items might depend upon them.
32+--
33+data CompileRule = CompileRule CompiledItem
34+ | MetaCompileRule [(Identifier, Compiler () CompileRule)]
35+
36+-- | A collection of rules for the compilation process
37+--
38+data RuleSet = RuleSet
39+ { -- | Routes used in the compilation structure
40+ rulesRoutes :: Routes
41+ , -- | Compilation rules
42+ rulesCompilers :: [(Identifier, Compiler () CompileRule)]
43+ , -- | A list of the used resources
44+ rulesResources :: Set Resource
45+ }
46+
47+instance Monoid RuleSet where
48+ mempty = RuleSet mempty mempty mempty
49+ mappend (RuleSet r1 c1 s1) (RuleSet r2 c2 s2) =
50+ RuleSet (mappend r1 r2) (mappend c1 c2) (mappend s1 s2)
51+
52+-- | Rule state
53+--
54+data RuleState = RuleState
55+ { rulesMetaCompilerIndex :: Int
56+ } deriving (Show)
57+
58+-- | The monad used to compose rules
59+--
60+newtype RulesM a = RulesM
61+ { unRulesM :: ReaderT ResourceProvider (WriterT RuleSet (State RuleState)) a
62+ } deriving (Monad, Functor, Applicative)
63+
64+-- | Simplification of the RulesM type; usually, it will not return any
65+-- result.
66+--
67+type Rules = RulesM ()
68+
69+-- | Run a Rules monad, resulting in a 'RuleSet'
70+--
71+runRules :: Rules -> ResourceProvider -> RuleSet
72+runRules rules provider =
73+ evalState (execWriterT $ runReaderT (unRulesM rules) provider) state
74+ where
75+ state = RuleState {rulesMetaCompilerIndex = 0}
src/Hakyll/Core/Run.hsnew file+207
@@ -0,0 +1,207 @@
1+-- | This is the module which binds it all together
2+--
3+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4+module Hakyll.Core.Run
5+ ( run
6+ ) where
7+
8+import Prelude hiding (reverse)
9+import Control.Monad (filterM)
10+import Control.Monad.Trans (liftIO)
11+import Control.Applicative (Applicative, (<$>))
12+import Control.Monad.Reader (ReaderT, runReaderT, ask)
13+import Control.Monad.State.Strict (StateT, evalStateT, get, modify)
14+import Control.Arrow ((&&&))
15+import qualified Data.Map as M
16+import Data.Monoid (mempty, mappend)
17+import System.FilePath ((</>))
18+import Data.Set (Set)
19+import qualified Data.Set as S
20+
21+import Hakyll.Core.Routes
22+import Hakyll.Core.Identifier
23+import Hakyll.Core.Util.File
24+import Hakyll.Core.Compiler
25+import Hakyll.Core.Compiler.Internal
26+import Hakyll.Core.ResourceProvider
27+import Hakyll.Core.ResourceProvider.FileResourceProvider
28+import Hakyll.Core.Rules.Internal
29+import Hakyll.Core.DirectedGraph
30+import Hakyll.Core.DirectedGraph.DependencySolver
31+import Hakyll.Core.Writable
32+import Hakyll.Core.Store
33+import Hakyll.Core.Configuration
34+import Hakyll.Core.Logger
35+
36+-- | Run all rules needed, return the rule set used
37+--
38+run :: HakyllConfiguration -> Rules -> IO RuleSet
39+run configuration rules = do
40+ logger <- makeLogger
41+
42+ section logger "Initialising"
43+ store <- timed logger "Creating store" $
44+ makeStore $ storeDirectory configuration
45+ provider <- timed logger "Creating provider" $
46+ fileResourceProvider configuration
47+
48+ let ruleSet = runRules rules provider
49+ compilers = rulesCompilers ruleSet
50+
51+ -- Extract the reader/state
52+ reader = unRuntime $ addNewCompilers [] compilers
53+ state' = runReaderT reader $ env logger ruleSet provider store
54+
55+ evalStateT state' state
56+
57+ -- Flush and return
58+ flushLogger logger
59+ return ruleSet
60+ where
61+ env logger ruleSet provider store = RuntimeEnvironment
62+ { hakyllLogger = logger
63+ , hakyllConfiguration = configuration
64+ , hakyllRoutes = rulesRoutes ruleSet
65+ , hakyllResourceProvider = provider
66+ , hakyllStore = store
67+ }
68+
69+ state = RuntimeState
70+ { hakyllModified = S.empty
71+ , hakyllGraph = mempty
72+ }
73+
74+data RuntimeEnvironment = RuntimeEnvironment
75+ { hakyllLogger :: Logger
76+ , hakyllConfiguration :: HakyllConfiguration
77+ , hakyllRoutes :: Routes
78+ , hakyllResourceProvider :: ResourceProvider
79+ , hakyllStore :: Store
80+ }
81+
82+data RuntimeState = RuntimeState
83+ { hakyllModified :: Set Identifier
84+ , hakyllGraph :: DirectedGraph Identifier
85+ }
86+
87+newtype Runtime a = Runtime
88+ { unRuntime :: ReaderT RuntimeEnvironment (StateT RuntimeState IO) a
89+ } deriving (Functor, Applicative, Monad)
90+
91+-- | Return a set of modified identifiers
92+--
93+modified :: ResourceProvider -- ^ Resource provider
94+ -> Store -- ^ Store
95+ -> [Identifier] -- ^ Identifiers to check
96+ -> IO (Set Identifier) -- ^ Modified resources
97+modified provider store ids = fmap S.fromList $ flip filterM ids $ \id' ->
98+ if resourceExists provider id'
99+ then resourceModified provider (Resource id') store
100+ else return False
101+
102+-- | Add a number of compilers and continue using these compilers
103+--
104+addNewCompilers :: [(Identifier, Compiler () CompileRule)]
105+ -- ^ Remaining compilers yet to be run
106+ -> [(Identifier, Compiler () CompileRule)]
107+ -- ^ Compilers to add
108+ -> Runtime ()
109+addNewCompilers oldCompilers newCompilers = Runtime $ do
110+ -- Get some information
111+ logger <- hakyllLogger <$> ask
112+ section logger "Adding new compilers"
113+ provider <- hakyllResourceProvider <$> ask
114+ store <- hakyllStore <$> ask
115+
116+ let -- All compilers
117+ compilers = oldCompilers ++ newCompilers
118+
119+ -- Get all dependencies for the compilers
120+ dependencies = flip map compilers $ \(id', compiler) ->
121+ let deps = runCompilerDependencies compiler id' provider
122+ in (id', deps)
123+
124+ -- Create a compiler map (Id -> Compiler)
125+ compilerMap = M.fromList compilers
126+
127+ -- Create the dependency graph
128+ currentGraph = fromList dependencies
129+
130+ -- Find the old graph and append the new graph to it. This forms the
131+ -- complete graph
132+ completeGraph <- timed logger "Creating graph" $
133+ mappend currentGraph . hakyllGraph <$> get
134+
135+ orderedCompilers <- timed logger "Solving dependencies" $ do
136+ -- Check which items are up-to-date. This only needs to happen for the new
137+ -- compilers
138+ oldModified <- hakyllModified <$> get
139+ newModified <- liftIO $ modified provider store $ map fst newCompilers
140+
141+ let modified' = oldModified `S.union` newModified
142+
143+ -- Find obsolete items. Every item that is reachable from a modified
144+ -- item is considered obsolete. From these obsolete items, we are only
145+ -- interested in ones that are in the current subgraph.
146+ obsolete = S.filter (`member` currentGraph)
147+ $ reachableNodes modified' $ reverse completeGraph
148+
149+ -- Solve the graph and retain only the obsolete items
150+ ordered = filter (`S.member` obsolete) $ solveDependencies currentGraph
151+
152+ -- Update the state
153+ modify $ updateState modified' completeGraph
154+
155+ -- Join the order with the compilers again
156+ return $ map (id &&& (compilerMap M.!)) ordered
157+
158+ -- Now run the ordered list of compilers
159+ unRuntime $ runCompilers orderedCompilers
160+ where
161+ -- Add the modified information for the new compilers
162+ updateState modified' graph state = state
163+ { hakyllModified = modified'
164+ , hakyllGraph = graph
165+ }
166+
167+runCompilers :: [(Identifier, Compiler () CompileRule)]
168+ -- ^ Ordered list of compilers
169+ -> Runtime ()
170+ -- ^ No result
171+runCompilers [] = return ()
172+runCompilers ((id', compiler) : compilers) = Runtime $ do
173+ -- Obtain information
174+ logger <- hakyllLogger <$> ask
175+ routes <- hakyllRoutes <$> ask
176+ provider <- hakyllResourceProvider <$> ask
177+ store <- hakyllStore <$> ask
178+ modified' <- hakyllModified <$> get
179+
180+ section logger $ "Compiling " ++ show id'
181+
182+ let -- Check if the resource was modified
183+ isModified = id' `S.member` modified'
184+
185+ -- Run the compiler
186+ result <- timed logger "Total compile time" $ liftIO $
187+ runCompiler compiler id' provider routes store isModified logger
188+
189+ case result of
190+ -- Compile rule for one item, easy stuff
191+ CompileRule compiled -> do
192+ case runRoutes routes id' of
193+ Nothing -> return ()
194+ Just url -> timed logger ("Routing to " ++ url) $ do
195+ destination <-
196+ destinationDirectory . hakyllConfiguration <$> ask
197+ let path = destination </> url
198+ liftIO $ makeDirectories path
199+ liftIO $ write path compiled
200+
201+ -- Continue for the remaining compilers
202+ unRuntime $ runCompilers compilers
203+
204+ -- Metacompiler, slightly more complicated
205+ MetaCompileRule newCompilers ->
206+ -- Actually I was just kidding, it's not hard at all
207+ unRuntime $ addNewCompilers compilers newCompilers
src/Hakyll/Core/Store.hsnew file+88
@@ -0,0 +1,88 @@
1+-- | A store for stroing and retreiving items
2+--
3+module Hakyll.Core.Store
4+ ( Store
5+ , makeStore
6+ , storeSet
7+ , storeGet
8+ ) where
9+
10+import Control.Concurrent.MVar (MVar, newMVar, readMVar, modifyMVar_)
11+import System.FilePath ((</>))
12+import System.Directory (doesFileExist)
13+import Data.Map (Map)
14+import qualified Data.Map as M
15+
16+import Data.Binary (Binary, encodeFile, decodeFile)
17+import Data.Typeable (Typeable)
18+
19+import Hakyll.Core.CompiledItem
20+import Hakyll.Core.Writable
21+import Hakyll.Core.Identifier
22+import Hakyll.Core.Util.File
23+
24+-- | Data structure used for the store
25+--
26+data Store = Store
27+ { -- | All items are stored on the filesystem
28+ storeDirectory :: FilePath
29+ , -- | And some items are also kept in-memory
30+ storeMap :: MVar (Map FilePath CompiledItem)
31+ }
32+
33+-- | Initialize the store
34+--
35+makeStore :: FilePath -> IO Store
36+makeStore directory = do
37+ mvar <- newMVar M.empty
38+ return Store
39+ { storeDirectory = directory
40+ , storeMap = mvar
41+ }
42+
43+-- | Auxiliary: add an item to the map
44+--
45+addToMap :: (Binary a, Typeable a, Writable a)
46+ => Store -> FilePath -> a -> IO ()
47+addToMap store path value =
48+ modifyMVar_ (storeMap store) $ return . M.insert path (compiledItem value)
49+
50+-- | Create a path
51+--
52+makePath :: Store -> String -> Identifier -> FilePath
53+makePath store name identifier =
54+ storeDirectory store </> name </> toFilePath identifier </> ".hakyllstore"
55+
56+-- | Store an item
57+--
58+storeSet :: (Binary a, Typeable a, Writable a)
59+ => Store -> String -> Identifier -> a -> IO ()
60+storeSet store name identifier value = do
61+ makeDirectories path
62+ encodeFile path value
63+ addToMap store path value
64+ where
65+ path = makePath store name identifier
66+
67+-- | Load an item
68+--
69+storeGet :: (Binary a, Typeable a, Writable a)
70+ => Store -> String -> Identifier -> IO (Maybe a)
71+storeGet store name identifier = do
72+ -- First check the in-memory map
73+ map' <- readMVar $ storeMap store
74+ case M.lookup path map' of
75+ -- Found in the in-memory map
76+ Just c -> return $ Just $ unCompiledItem c
77+ -- Not found in the map, try the filesystem
78+ Nothing -> do
79+ exists <- doesFileExist path
80+ if not exists
81+ -- Not found in the filesystem either
82+ then return Nothing
83+ -- Found in the filesystem
84+ else do v <- decodeFile path
85+ addToMap store path v
86+ return $ Just v
87+ where
88+ path = makePath store name identifier
src/Hakyll/Core/UnixFilter.hsnew file+76
@@ -0,0 +1,76 @@
1+-- | A Compiler that supports unix filters.
2+--
3+module Hakyll.Core.UnixFilter
4+ ( unixFilter
5+ ) where
6+
7+import Control.Concurrent (forkIO)
8+import System.IO (hPutStr, hClose, hGetContents)
9+import System.Posix.Process (executeFile, forkProcess)
10+import System.Posix.IO ( dupTo, createPipe, stdInput
11+ , stdOutput, closeFd, fdToHandle
12+ )
13+
14+import Hakyll.Core.Compiler
15+
16+-- | Use a unix filter as compiler. For example, we could use the 'rev' program
17+-- as a compiler.
18+--
19+-- > rev :: Compiler Resource String
20+-- > rev = getResourceString >>> unixFilter "rev" []
21+--
22+-- A more realistic example: one can use this to call, for example, the sass
23+-- compiler on CSS files. More information about sass can be found here:
24+--
25+-- <http://sass-lang.com/>
26+--
27+-- The code is fairly straightforward, given that we use @.scss@ for sass:
28+--
29+-- > route "style.scss" $ setExtension "css"
30+-- > compile "style.scss" $
31+-- > getResourceString >>> unixFilter "sass" ["-s", "--scss"]
32+-- > >>> arr compressCss
33+--
34+unixFilter :: String -- ^ Program name
35+ -> [String] -- ^ Program args
36+ -> Compiler String String -- ^ Resulting compiler
37+unixFilter programName args =
38+ timedCompiler ("Executing external program " ++ programName) $
39+ unsafeCompiler $ \input -> unixFilterIO programName args input
40+
41+-- | Internally used function
42+--
43+unixFilterIO :: String
44+ -> [String]
45+ -> String
46+ -> IO String
47+unixFilterIO programName args input = do
48+ -- Create pipes
49+ (stdinRead, stdinWrite) <- createPipe
50+ (stdoutRead, stdoutWrite) <- createPipe
51+
52+ -- Fork the child
53+ _ <- forkProcess $ do
54+ -- Copy our pipes over the regular stdin/stdout
55+ _ <- dupTo stdinRead stdInput
56+ _ <- dupTo stdoutWrite stdOutput
57+
58+ -- Close the now unneeded file descriptors in the child
59+ mapM_ closeFd [stdinWrite, stdoutRead, stdinRead, stdoutWrite]
60+
61+ -- Execute the program
62+ _ <- executeFile programName True args Nothing
63+ return ()
64+
65+ -- On the parent side, close the client-side FDs.
66+ mapM_ closeFd [stdinRead, stdoutWrite]
67+
68+ -- Write the input to the child pipe
69+ _ <- forkIO $ do
70+ stdinWriteHandle <- fdToHandle stdinWrite
71+ hPutStr stdinWriteHandle input
72+ hClose stdinWriteHandle
73+
74+ -- Receive the output from the child
75+ stdoutReadHandle <- fdToHandle stdoutRead
76+ hGetContents stdoutReadHandle
src/Hakyll/Core/Util/Arrow.hsnew file+25
@@ -0,0 +1,25 @@
1+-- | Various arrow utility functions
2+--
3+module Hakyll.Core.Util.Arrow
4+ ( constA
5+ , sequenceA
6+ , unitA
7+ ) where
8+
9+import Control.Arrow (Arrow, (&&&), arr, (>>^))
10+
11+constA :: Arrow a
12+ => c
13+ -> a b c
14+constA = arr . const
15+
16+sequenceA :: Arrow a
17+ => [a b c]
18+ -> a b [c]
19+sequenceA = foldl reduce $ constA []
20+ where
21+ reduce la xa = xa &&& la >>^ arr (uncurry (:))
22+
23+unitA :: Arrow a
24+ => a b ()
25+unitA = constA ()
src/Hakyll/Core/Util/File.hsnew file+90
@@ -0,0 +1,90 @@
1+-- | A module containing various file utility functions
2+--
3+module Hakyll.Core.Util.File
4+ ( makeDirectories
5+ , getRecursiveContents
6+ , isFileObsolete
7+ , isFileInternal
8+ ) where
9+
10+import Control.Applicative ((<$>))
11+import System.Time (ClockTime)
12+import Control.Monad (forM, filterM)
13+import Data.List (isPrefixOf)
14+import System.Directory ( createDirectoryIfMissing, doesDirectoryExist
15+ , doesFileExist, getModificationTime
16+ , getDirectoryContents
17+ )
18+import System.FilePath ( normalise, takeDirectory, splitPath
19+ , dropTrailingPathSeparator, (</>)
20+ )
21+
22+import Hakyll.Core.Configuration
23+
24+-- | Given a path to a file, try to make the path writable by making
25+-- all directories on the path.
26+--
27+makeDirectories :: FilePath -> IO ()
28+makeDirectories = createDirectoryIfMissing True . takeDirectory
29+
30+-- | Get all contents of a directory. Note that files starting with a dot (.)
31+-- will be ignored.
32+--
33+getRecursiveContents :: Bool -- ^ Include directories?
34+ -> FilePath -- ^ Directory to search
35+ -> IO [FilePath] -- ^ List of files found
36+getRecursiveContents includeDirs topdir = do
37+ topdirExists <- doesDirectoryExist topdir
38+ if not topdirExists
39+ then return []
40+ else do
41+ names <- filter isProper <$> getDirectoryContents topdir
42+ paths <- forM names $ \name -> do
43+ let path = normalise $ topdir </> name
44+ isDirectory <- doesDirectoryExist path
45+ if isDirectory then getRecursiveContents includeDirs path
46+ else return [path]
47+ return $ if includeDirs then topdir : concat paths
48+ else concat paths
49+ where
50+ isProper = not . (== ".") . take 1
51+
52+-- | Check if a timestamp is obsolete compared to the timestamps of a number of
53+-- files. When they are no files, it is never obsolete.
54+--
55+isObsolete :: ClockTime -- ^ The time to check.
56+ -> [FilePath] -- ^ Dependencies of the cached file.
57+ -> IO Bool
58+isObsolete _ [] = return False
59+isObsolete timeStamp depends = do
60+ depends' <- filterM doesFileExist depends
61+ dependsModified <- mapM getModificationTime depends'
62+ return (timeStamp < maximum dependsModified)
63+
64+-- | Check if a file is obsolete, given it's dependencies. When the file does
65+-- not exist, it is always obsolete. Other wise, it is obsolete if any of it's
66+-- dependencies has a more recent modification time than the file.
67+--
68+isFileObsolete :: FilePath -- ^ The cached file
69+ -> [FilePath] -- ^ Dependencies of the cached file
70+ -> IO Bool
71+isFileObsolete file depends = do
72+ exists <- doesFileExist file
73+ if not exists
74+ then return True
75+ else do timeStamp <- getModificationTime file
76+ isObsolete timeStamp depends
77+
78+-- | Check if a file is meant for Hakyll internal use, i.e. if it is located in
79+-- the destination or store directory
80+--
81+isFileInternal :: HakyllConfiguration -- ^ Configuration
82+ -> FilePath -- ^ File to check
83+ -> Bool -- ^ If the given file is internal
84+isFileInternal configuration file =
85+ any (`isPrefixOf` split file) dirs
86+ where
87+ split = map dropTrailingPathSeparator . splitPath
88+ dirs = map (split . ($ configuration)) [ destinationDirectory
89+ , storeDirectory
90+ ]
src/Hakyll/Core/Util/String.hsnew file+48
@@ -0,0 +1,48 @@
1+-- | Miscellaneous string manipulation functions.
2+--
3+module Hakyll.Core.Util.String
4+ ( trim
5+ , replaceAll
6+ , splitAll
7+ ) where
8+
9+import Data.Char (isSpace)
10+import Data.Maybe (listToMaybe)
11+
12+import Text.Regex.PCRE ((=~~))
13+
14+-- | Trim a string (drop spaces, tabs and newlines at both sides).
15+--
16+trim :: String -> String
17+trim = reverse . trim' . reverse . trim'
18+ where
19+ trim' = dropWhile isSpace
20+
21+-- | A simple (but inefficient) regex replace funcion
22+--
23+replaceAll :: String -- ^ Pattern
24+ -> (String -> String) -- ^ Replacement (called on capture)
25+ -> String -- ^ Source string
26+ -> String -- ^ Result
27+replaceAll pattern f source = replaceAll' source
28+ where
29+ replaceAll' src = case listToMaybe (src =~~ pattern) of
30+ Nothing -> src
31+ Just (o, l) ->
32+ let (before, tmp) = splitAt o src
33+ (capture, after) = splitAt l tmp
34+ in before ++ f capture ++ replaceAll' after
35+
36+-- | A simple regex split function. The resulting list will contain no empty
37+-- strings.
38+--
39+splitAll :: String -- ^ Pattern
40+ -> String -- ^ String to split
41+ -> [String] -- ^ Result
42+splitAll pattern = filter (not . null) . splitAll'
43+ where
44+ splitAll' src = case listToMaybe (src =~~ pattern) of
45+ Nothing -> [src]
46+ Just (o, l) ->
47+ let (before, tmp) = splitAt o src
48+ in before : splitAll' (drop l tmp)
src/Hakyll/Core/Writable.hsnew file+22
@@ -0,0 +1,22 @@
1+-- | Describes writable items; items that can be saved to the disk
2+--
3+{-# LANGUAGE FlexibleInstances #-}
4+module Hakyll.Core.Writable
5+ ( Writable (..)
6+ ) where
7+
8+import Data.Word (Word8)
9+
10+import qualified Data.ByteString as SB
11+
12+-- | Describes an item that can be saved to the disk
13+--
14+class Writable a where
15+ -- | Save an item to the given filepath
16+ write :: FilePath -> a -> IO ()
17+
18+instance Writable [Char] where
19+ write = writeFile
20+
21+instance Writable [Word8] where
22+ write p = SB.writeFile p . SB.pack
src/Hakyll/Main.hsnew file+113
@@ -0,0 +1,113 @@
1+-- | Module providing the main hakyll function and command-line argument parsing
2+--
3+module Hakyll.Main
4+ ( hakyll
5+ , hakyllWith
6+ ) where
7+
8+import Control.Concurrent (forkIO)
9+import Control.Monad (when)
10+import System.Environment (getProgName, getArgs)
11+import System.Directory (doesDirectoryExist, removeDirectoryRecursive)
12+
13+import Hakyll.Core.Configuration
14+import Hakyll.Core.Run
15+import Hakyll.Core.Rules
16+import Hakyll.Core.Rules.Internal
17+import Hakyll.Web.Preview.Poll
18+import Hakyll.Web.Preview.Server
19+
20+-- | This usualy is the function with which the user runs the hakyll compiler
21+--
22+hakyll :: Rules -> IO ()
23+hakyll = hakyllWith defaultHakyllConfiguration
24+
25+-- | A variant of 'hakyll' which allows the user to specify a custom
26+-- configuration
27+--
28+hakyllWith :: HakyllConfiguration -> Rules -> IO ()
29+hakyllWith configuration rules = do
30+ args <- getArgs
31+ case args of
32+ ["build"] -> build configuration rules
33+ ["clean"] -> clean configuration
34+ ["help"] -> help
35+ ["preview"] -> preview configuration rules 8000
36+ ["preview", p] -> preview configuration rules (read p)
37+ ["rebuild"] -> rebuild configuration rules
38+ ["server"] -> server configuration 8000
39+ ["server", p] -> server configuration (read p)
40+ _ -> help
41+
42+-- | Build the site
43+--
44+build :: HakyllConfiguration -> Rules -> IO ()
45+build configuration rules = do
46+ _ <- run configuration rules
47+ return ()
48+
49+-- | Remove the output directories
50+--
51+clean :: HakyllConfiguration -> IO ()
52+clean configuration = do
53+ remove $ destinationDirectory configuration
54+ remove $ storeDirectory configuration
55+ where
56+ remove dir = do
57+ putStrLn $ "Removing " ++ dir ++ "..."
58+ exists <- doesDirectoryExist dir
59+ when exists $ removeDirectoryRecursive dir
60+
61+-- | Show usage information.
62+--
63+help :: IO ()
64+help = do
65+ name <- getProgName
66+ mapM_ putStrLn
67+ [ "ABOUT"
68+ , ""
69+ , "This is a Hakyll site generator program. You should always"
70+ , "run it from the project root directory."
71+ , ""
72+ , "USAGE"
73+ , ""
74+ , name ++ " build Generate the site"
75+ , name ++ " clean Clean up and remove cache"
76+ , name ++ " help Show this message"
77+ , name ++ " preview [port] Run a server and autocompile"
78+ , name ++ " rebuild Clean up and build again"
79+ , name ++ " server [port] Run a local test server"
80+ ]
81+
82+-- | Preview the site
83+--
84+preview :: HakyllConfiguration -> Rules -> Int -> IO ()
85+preview configuration rules port = do
86+ -- Build once, keep the rule set
87+ ruleSet <- run configuration rules
88+
89+ -- Get the resource list and a callback for the preview poll
90+ let resources = rulesResources ruleSet
91+ callback = build configuration rules
92+
93+ -- Fork a thread polling for changes
94+ _ <- forkIO $ previewPoll configuration resources callback
95+
96+ -- Run the server in the main thread
97+ server configuration port
98+
99+-- | Rebuild the site
100+--
101+rebuild :: HakyllConfiguration -> Rules -> IO ()
102+rebuild configuration rules = do
103+ clean configuration
104+ build configuration rules
105+
106+-- | Start a server
107+--
108+server :: HakyllConfiguration -> Int -> IO ()
109+server configuration port = do
110+ let destination = destinationDirectory configuration
111+ staticServer destination preServeHook port
112+ where
113+ preServeHook _ = return ()
src/Text/Hakyll/Internal/CompressCss.hs → src/Hakyll/Web/CompressCss.hsrenamed+23−8
@@ -1,30 +1,45 @@
11-- | Module used for CSS compression. The compression is currently in a simple
2−-- state, but would typically reduce the number of bytes by about 25%.
3−module Text.Hakyll.Internal.CompressCss
4− ( compressCss
2+-- state, but would typically reduce the number of bytes by about 25%.
3+--
4+module Hakyll.Web.CompressCss
5+ ( compressCssCompiler
6+ , compressCss
57 ) where
68
9+import Data.Char (isSpace)
710import Data.List (isPrefixOf)
11+import Control.Arrow ((>>^))
812
9−import Text.Hakyll.Regex (substituteRegex)
13+import Hakyll.Core.Compiler
14+import Hakyll.Core.ResourceProvider
15+import Hakyll.Core.Util.String
16+
17+-- | Compiler form of 'compressCss'
18+--
19+compressCssCompiler :: Compiler Resource String
20+compressCssCompiler = getResourceString >>^ compressCss
1021
1122-- | Compress CSS to speed up your site.
23+--
1224compressCss :: String -> String
1325compressCss = compressSeparators
1426 . stripComments
1527 . compressWhitespace
1628
1729-- | Compresses certain forms of separators.
30+--
1831compressSeparators :: String -> String
19−compressSeparators = substituteRegex "; *}" "}"
20− . substituteRegex " *([{};:]) *" "\\1"
21− . substituteRegex ";;*" ";"
32+compressSeparators = replaceAll "; *}" (const "}")
33+ . replaceAll " *([{};:]) *" (take 1 . dropWhile isSpace)
34+ . replaceAll ";;*" (const ";")
2235
2336-- | Compresses all whitespace.
37+--
2438compressWhitespace :: String -> String
25−compressWhitespace = substituteRegex "[ \t\n][ \t\n]*" " "
39+compressWhitespace = replaceAll "[ \t\n][ \t\n]*" (const " ")
2640
2741-- | Function that strips CSS comments away.
42+--
2843stripComments :: String -> String
2944stripComments [] = []
3045stripComments str
src/Hakyll/Web/Feed.hsnew file+124
@@ -0,0 +1,124 @@
1+-- | A Module that allows easy rendering of RSS feeds.
2+--
3+-- The main rendering functions (@renderRss@, @renderAtom@) all assume that
4+-- you pass the list of items so that the most recent entry in the feed is the
5+-- first item in the list.
6+--
7+-- Also note that the pages should have (at least) the following fields to
8+-- produce a correct feed:
9+--
10+-- - @$title@: Title of the item
11+--
12+-- - @$description@: Description to appear in the feed
13+--
14+-- - @$url@: URL to the item - this is usually set automatically.
15+--
16+-- In addition, the posts should be named according to the rules for
17+-- 'Hakyll.Page.Metadata.renderDateField'.
18+--
19+module Hakyll.Web.Feed
20+ ( FeedConfiguration (..)
21+ , renderRss
22+ , renderAtom
23+ ) where
24+
25+import Prelude hiding (id)
26+import Control.Category (id)
27+import Control.Arrow ((>>>), arr, (&&&))
28+import Control.Monad ((<=<))
29+import Data.Maybe (fromMaybe, listToMaybe)
30+
31+import Hakyll.Core.Compiler
32+import Hakyll.Web.Page
33+import Hakyll.Web.Page.Metadata
34+import Hakyll.Web.Template
35+import Hakyll.Web.Template.Read.Hakyll (readTemplate)
36+import Hakyll.Web.Util.Url
37+
38+import Paths_hakyll
39+
40+-- | This is a data structure to keep the configuration of a feed.
41+data FeedConfiguration = FeedConfiguration
42+ { -- | Title of the feed.
43+ feedTitle :: String
44+ , -- | Description of the feed.
45+ feedDescription :: String
46+ , -- | Name of the feed author.
47+ feedAuthorName :: String
48+ , -- | Absolute root URL of the feed site (e.g. @http://jaspervdj.be@)
49+ feedRoot :: String
50+ }
51+
52+-- | This is an auxiliary function to create a listing that is, in fact, a feed.
53+-- The items should be sorted on date. The @$timestamp@ field should be set.
54+--
55+createFeed :: Template -- ^ Feed template
56+ -> Template -- ^ Item template
57+ -> String -- ^ URL of the feed
58+ -> FeedConfiguration -- ^ Feed configuration
59+ -> [Page String] -- ^ Items to include
60+ -> String -- ^ Resulting feed
61+createFeed feedTemplate itemTemplate url configuration items =
62+ pageBody $ applyTemplate feedTemplate
63+ $ setField "timestamp" timestamp
64+ $ setField "title" (feedTitle configuration)
65+ $ setField "description" (feedDescription configuration)
66+ $ setField "authorName" (feedDescription configuration)
67+ $ setField "root" (feedRoot configuration)
68+ $ setField "url" url
69+ $ fromBody body
70+ where
71+ -- Preprocess items
72+ items' = flip map items $ applyTemplate itemTemplate
73+ . setField "root" (feedRoot configuration)
74+
75+ -- Body: concatenated items
76+ body = concat $ map pageBody items'
77+
78+ -- Take the first timestamp, which should be the most recent
79+ timestamp = fromMaybe "Unknown" $ do
80+ p <- listToMaybe items
81+ return $ getField "timestamp" p
82+
83+
84+-- | Abstract function to render any feed.
85+--
86+renderFeed :: FilePath -- ^ Feed template
87+ -> FilePath -- ^ Item template
88+ -> FeedConfiguration -- ^ Feed configuration
89+ -> Compiler [Page String] String -- ^ Feed compiler
90+renderFeed feedTemplate itemTemplate configuration =
91+ id &&& getRoute >>> renderFeed'
92+ where
93+ -- Arrow rendering the feed from the items and the URL
94+ renderFeed' = unsafeCompiler $ \(items, url) -> do
95+ feedTemplate' <- loadTemplate feedTemplate
96+ itemTemplate' <- loadTemplate itemTemplate
97+ let url' = toUrl $ fromMaybe noUrl url
98+ return $ createFeed feedTemplate' itemTemplate' url' configuration items
99+
100+ -- Auxiliary: load a template from a datafile
101+ loadTemplate = fmap readTemplate . readFile <=< getDataFileName
102+
103+ -- URL is required to have a valid field
104+ noUrl = error "Hakyll.Web.Feed.renderFeed: no route specified"
105+
106+-- | Render an RSS feed with a number of items.
107+--
108+renderRss :: FeedConfiguration -- ^ Feed configuration
109+ -> Compiler [Page String] String -- ^ Feed compiler
110+renderRss configuration = arr (map renderDate)
111+ >>> renderFeed "templates/rss.xml" "templates/rss-item.xml" configuration
112+ where
113+ renderDate = renderDateField "timestamp" "%a, %d %b %Y %H:%M:%S UT"
114+ "No date found."
115+
116+-- | Render an Atom feed with a number of items.
117+--
118+renderAtom :: FeedConfiguration -- ^ Feed configuration
119+ -> Compiler [Page String] String -- ^ Feed compiler
120+renderAtom configuration = arr (map renderDate)
121+ >>> renderFeed "templates/atom.xml" "templates/atom-item.xml" configuration
122+ where
123+ renderDate = renderDateField "timestamp" "%Y-%m-%dT%H:%M:%SZ"
124+ "No date found."
src/Hakyll/Web/FileType.hsnew file+55
@@ -0,0 +1,55 @@
1+-- | A module dealing with common file extensions and associated file types.
2+--
3+module Hakyll.Web.FileType
4+ ( FileType (..)
5+ , fileType
6+ , getFileType
7+ ) where
8+
9+import System.FilePath (takeExtension)
10+import Control.Arrow ((>>^))
11+
12+import Hakyll.Core.Identifier
13+import Hakyll.Core.Compiler
14+
15+-- | Datatype to represent the different file types Hakyll can deal with by
16+-- default
17+--
18+data FileType
19+ = Html
20+ | LaTeX
21+ | LiterateHaskell FileType
22+ | Markdown
23+ | Rst
24+ | PlainText
25+ | Css
26+ | Binary
27+ deriving (Eq, Ord, Show, Read)
28+
29+-- | Get the file type for a certain file. The type is determined by extension.
30+--
31+fileType :: FilePath -> FileType
32+fileType = fileType' . takeExtension
33+ where
34+ fileType' ".htm" = Html
35+ fileType' ".html" = Html
36+ fileType' ".lhs" = LiterateHaskell Markdown
37+ fileType' ".markdown" = Markdown
38+ fileType' ".md" = Markdown
39+ fileType' ".mdn" = Markdown
40+ fileType' ".mdown" = Markdown
41+ fileType' ".mdwn" = Markdown
42+ fileType' ".mkd" = Markdown
43+ fileType' ".mkdwn" = Markdown
44+ fileType' ".page" = Markdown
45+ fileType' ".rst" = Rst
46+ fileType' ".tex" = LaTeX
47+ fileType' ".text" = PlainText
48+ fileType' ".txt" = PlainText
49+ fileType' ".css" = Css
50+ fileType' _ = Binary -- Treat unknown files as binary
51+
52+-- | Get the file type for the current file
53+--
54+getFileType :: Compiler a FileType
55+getFileType = getIdentifier >>^ fileType . toFilePath
src/Hakyll/Web/Page.hsnew file+124
@@ -0,0 +1,124 @@
1+-- | A page is a key-value mapping, representing a page on your site
2+--
3+-- A page is an important concept in Hakyll. It is a key-value mapping, and has
4+-- one field with an arbitrary type. A 'Page' thus consists of
5+--
6+-- * a key-value mapping (of the type @Map String String@);
7+--
8+-- * a value (of the type @a@).
9+--
10+-- Usually, the value will be a 'String' as well, and the value will be the body
11+-- of the page.
12+--
13+-- Pages can be constructed using Haskell, but they are usually parsed from a
14+-- file. The file format for pages is pretty straightforward.
15+--
16+-- > This is a simple page
17+-- > consisting of two lines.
18+--
19+-- This is a valid page with two lines. If we load this in Hakyll, there would
20+-- be no metadata, and the body would be the given text. Let's look at a page
21+-- with some metadata.
22+--
23+-- > ---
24+-- > title: Alice's Adventures in Wonderland
25+-- > author: Lewis Caroll
26+-- > year: 1865
27+-- > ---
28+-- >
29+-- > Chapter I
30+-- > =========
31+-- >
32+-- > Down the Rabbit-Hole
33+-- > --------------------
34+-- >
35+-- > Alice was beginning to get very tired of sitting by her sister on the bank,
36+-- > and of having nothing to do: once or twice she had peeped into the book her
37+-- > sister was reading, but it had no pictures or conversations in it, "and
38+-- > what is the use of a book," thought Alice "without pictures or
39+-- > conversation?"
40+-- >
41+-- > ...
42+--
43+-- As you can see, we construct a metadata header in Hakyll using @---@. Then,
44+-- we simply list all @key: value@ pairs, and end with @---@ again. This page
45+-- contains three metadata fields and a body. The body is given in markdown
46+-- format, which can be easily rendered to HTML by Hakyll, using pandoc.
47+--
48+{-# LANGUAGE DeriveDataTypeable #-}
49+module Hakyll.Web.Page
50+ ( Page (..)
51+ , fromBody
52+ , fromMap
53+ , toMap
54+ , readPageCompiler
55+ , pageCompiler
56+ , addDefaultFields
57+ , sortByBaseName
58+ ) where
59+
60+import Prelude hiding (id)
61+import Control.Category (id)
62+import Control.Arrow (arr, (>>^), (&&&), (>>>))
63+import System.FilePath (takeBaseName, takeDirectory)
64+import qualified Data.Map as M
65+import Data.List (sortBy)
66+import Data.Ord (comparing)
67+
68+import Hakyll.Core.Identifier
69+import Hakyll.Core.Compiler
70+import Hakyll.Core.ResourceProvider
71+import Hakyll.Web.Page.Internal
72+import Hakyll.Web.Page.Read
73+import Hakyll.Web.Page.Metadata
74+import Hakyll.Web.Pandoc
75+import Hakyll.Web.Template
76+import Hakyll.Web.Util.Url
77+
78+-- | Create a page from a body, without metadata
79+--
80+fromBody :: a -> Page a
81+fromBody = Page M.empty
82+
83+-- | Read a page (do not render it)
84+--
85+readPageCompiler :: Compiler Resource (Page String)
86+readPageCompiler = getResourceString >>^ readPage
87+
88+-- | Read a page, add default fields, substitute fields and render using pandoc
89+--
90+pageCompiler :: Compiler Resource (Page String)
91+pageCompiler = cached "Hakyll.Web.Page.pageCompiler" $
92+ readPageCompiler >>> addDefaultFields >>> arr applySelf >>> pageRenderPandoc
93+
94+-- | Add a number of default metadata fields to a page. These fields include:
95+--
96+-- * @$url@
97+--
98+-- * @$category@
99+--
100+-- * @$title@
101+--
102+-- * @$path@
103+--
104+addDefaultFields :: Compiler (Page a) (Page a)
105+addDefaultFields = (getRoute &&& id >>^ uncurry addRoute)
106+ >>> (getIdentifier &&& id >>^ uncurry addIdentifier)
107+ where
108+ -- Add root and url, based on route
109+ addRoute Nothing = id
110+ addRoute (Just r) = setField "url" (toUrl r)
111+
112+ -- Add title and category, based on identifier
113+ addIdentifier i = setField "title" (takeBaseName p)
114+ . setField "category" (takeBaseName $ takeDirectory p)
115+ . setField "path" p
116+ where
117+ p = toFilePath i
118+
119+-- | Sort posts based on the basename of the post. This is equivalent to a
120+-- chronologival sort, because of the @year-month-day-title.extension@ naming
121+-- convention in Hakyll.
122+--
123+sortByBaseName :: [Page a] -> [Page a]
124+sortByBaseName = sortBy $ comparing $ takeBaseName . getField "path"
src/Hakyll/Web/Page/Internal.hsnew file+50
@@ -0,0 +1,50 @@
1+-- | Internal representation of the page datatype
2+--
3+{-# LANGUAGE DeriveDataTypeable #-}
4+module Hakyll.Web.Page.Internal
5+ ( Page (..)
6+ , fromMap
7+ , toMap
8+ ) where
9+
10+import Control.Applicative ((<$>), (<*>))
11+import Data.Monoid (Monoid, mempty, mappend)
12+
13+import Data.Map (Map)
14+import Data.Binary (Binary, get, put)
15+import Data.Typeable (Typeable)
16+import qualified Data.Map as M
17+
18+import Hakyll.Core.Writable
19+
20+-- | Type used to represent pages
21+--
22+data Page a = Page
23+ { pageMetadata :: Map String String
24+ , pageBody :: a
25+ } deriving (Eq, Show, Typeable)
26+
27+instance Monoid a => Monoid (Page a) where
28+ mempty = Page M.empty mempty
29+ mappend (Page m1 b1) (Page m2 b2) =
30+ Page (M.union m1 m2) (mappend b1 b2)
31+
32+instance Functor Page where
33+ fmap f (Page m b) = Page m (f b)
34+
35+instance Binary a => Binary (Page a) where
36+ put (Page m b) = put m >> put b
37+ get = Page <$> get <*> get
38+
39+instance Writable a => Writable (Page a) where
40+ write p (Page _ b) = write p b
41+
42+-- | Create a metadata page, without a body
43+--
44+fromMap :: Monoid a => Map String String -> Page a
45+fromMap m = Page m mempty
46+
47+-- | Convert a page to a map. The body will be placed in the @body@ key.
48+--
49+toMap :: Page String -> Map String String
50+toMap (Page m b) = M.insert "body" b m
src/Hakyll/Web/Page/Metadata.hsnew file+131
@@ -0,0 +1,131 @@
1+-- | Provides various functions to manipulate the metadata fields of a page
2+--
3+module Hakyll.Web.Page.Metadata
4+ ( getField
5+ , getFieldMaybe
6+ , setField
7+ , setFieldA
8+ , renderField
9+ , changeField
10+ , copyField
11+ , renderDateField
12+ , renderDateFieldWith
13+ ) where
14+
15+import Prelude hiding (id)
16+import Control.Category (id)
17+import Control.Arrow (Arrow, (>>>), (***), arr)
18+import Data.List (intercalate)
19+import Data.Maybe (fromMaybe)
20+import Data.Time.Clock (UTCTime)
21+import Data.Time.Format (parseTime, formatTime)
22+import qualified Data.Map as M
23+import System.FilePath (takeFileName)
24+import System.Locale (TimeLocale, defaultTimeLocale)
25+
26+import Hakyll.Web.Page.Internal
27+import Hakyll.Core.Util.String
28+
29+-- | Get a metadata field. If the field does not exist, the empty string is
30+-- returned.
31+--
32+getField :: String -- ^ Key
33+ -> Page a -- ^ Page
34+ -> String -- ^ Value
35+getField key = fromMaybe "" . getFieldMaybe key
36+
37+-- | Get a field in a 'Maybe' wrapper
38+--
39+getFieldMaybe :: String -- ^ Key
40+ -> Page a -- ^ Page
41+ -> Maybe String -- ^ Value, if found
42+getFieldMaybe key = M.lookup key . pageMetadata
43+
44+-- | Add a metadata field. If the field already exists, it is not overwritten.
45+--
46+setField :: String -- ^ Key
47+ -> String -- ^ Value
48+ -> Page a -- ^ Page to add it to
49+ -> Page a -- ^ Resulting page
50+setField k v (Page m b) = Page (M.insertWith (flip const) k v m) b
51+
52+-- | Arrow-based variant of 'setField'. Because of it's type, this function is
53+-- very usable together with the different 'require' functions.
54+--
55+setFieldA :: Arrow a
56+ => String -- ^ Key
57+ -> a x String -- ^ Value arrow
58+ -> a (Page b, x) (Page b) -- ^ Resulting arrow
59+setFieldA k v = id *** v >>> arr (uncurry $ flip $ setField k)
60+
61+-- | Do something with a metadata value, but keep the old value as well. If the
62+-- key given is not present in the metadata, nothing will happen. If the source
63+-- and destination keys are the same, the value will be changed (but you should
64+-- use 'changeField' for this purpose).
65+--
66+renderField :: String -- ^ Key of which the value should be copied
67+ -> String -- ^ Key the value should be copied to
68+ -> (String -> String) -- ^ Function to apply on the value
69+ -> Page a -- ^ Page on which this should be applied
70+ -> Page a -- ^ Resulting page
71+renderField src dst f page = case M.lookup src (pageMetadata page) of
72+ Nothing -> page
73+ Just value -> setField dst (f value) page
74+
75+-- | Change a metadata value.
76+--
77+-- > import Data.Char (toUpper)
78+-- > changeField "title" (map toUpper)
79+--
80+-- Will put the title in UPPERCASE.
81+--
82+changeField :: String -- ^ Key to change.
83+ -> (String -> String) -- ^ Function to apply on the value.
84+ -> Page a -- ^ Page to change
85+ -> Page a -- ^ Resulting page
86+changeField key = renderField key key
87+
88+-- | Make a copy of a metadata field (put the value belonging to a certain key
89+-- under some other key as well)
90+--
91+copyField :: String -- ^ Key to copy
92+ -> String -- ^ Destination to copy to
93+ -> Page a -- ^ Page on which this should be applied
94+ -> Page a -- ^ Resulting page
95+copyField src dst = renderField src dst id
96+
97+-- | When the metadata has a field called @path@ in a
98+-- @folder/yyyy-mm-dd-title.extension@ format (the convention for pages),
99+-- this function can render the date.
100+--
101+-- > renderDate "date" "%B %e, %Y" "Date unknown"
102+--
103+-- Will render something like @January 32, 2010@.
104+--
105+renderDateField :: String -- ^ Key in which the rendered date should be placed
106+ -> String -- ^ Format to use on the date
107+ -> String -- ^ Default value, in case the date cannot be parsed
108+ -> Page a -- ^ Page on which this should be applied
109+ -> Page a -- ^ Resulting page
110+renderDateField = renderDateFieldWith defaultTimeLocale
111+
112+-- | This is an extended version of 'renderDateField' that allows you to
113+-- specify a time locale that is used for outputting the date. For more
114+-- details, see 'renderDateField'.
115+--
116+renderDateFieldWith :: TimeLocale -- ^ Output time locale
117+ -> String -- ^ Destination key
118+ -> String -- ^ Format to use on the date
119+ -> String -- ^ Default value
120+ -> Page a -- ^ Target page
121+ -> Page a -- ^ Resulting page
122+renderDateFieldWith locale key format defaultValue =
123+ renderField "path" key renderDate'
124+ where
125+ renderDate' filePath = fromMaybe defaultValue $ do
126+ let dateString = intercalate "-" $ take 3
127+ $ splitAll "-" $ takeFileName filePath
128+ time <- parseTime defaultTimeLocale
129+ "%Y-%m-%d"
130+ dateString :: Maybe UTCTime
131+ return $ formatTime locale format time
src/Hakyll/Web/Page/Read.hsnew file+60
@@ -0,0 +1,60 @@
1+-- | Module providing a function to parse a page from a file
2+--
3+module Hakyll.Web.Page.Read
4+ ( readPage
5+ ) where
6+
7+import Control.Applicative ((<$>), (<*>))
8+import Control.Arrow (second, (***))
9+import Control.Monad.State (State, get, put, evalState)
10+import Data.List (isPrefixOf)
11+import Data.Map (Map)
12+import qualified Data.Map as M
13+
14+import Hakyll.Web.Page.Internal
15+import Hakyll.Core.Util.String
16+
17+-- | We're using a simple state monad as parser
18+--
19+type LineParser = State [String]
20+
21+-- | Read the metadata section from a page
22+--
23+parseMetadata :: LineParser (Map String String)
24+parseMetadata = get >>= \content -> case content of
25+ -- No lines means no metadata
26+ [] -> return M.empty
27+ -- Check if the file begins with a delimiter
28+ (l : ls) -> if not (isPossibleDelimiter l)
29+ then -- No delimiter means no metadata
30+ return M.empty
31+ else do -- Break the metadata section
32+ let (metadata, rest) = second (drop 1) $ break (== l) ls
33+ -- Put the rest back
34+ put rest
35+ -- Parse the metadata
36+ return $ M.fromList $ map parseMetadata' metadata
37+ where
38+ -- Check if a line can be a delimiter
39+ isPossibleDelimiter = isPrefixOf "---"
40+
41+ -- Parse a "key: value" string to a (key, value) tupple
42+ parseMetadata' = (trim *** trim . drop 1) . break (== ':')
43+
44+-- | Read the body section of a page
45+--
46+parseBody :: LineParser String
47+parseBody = do
48+ body <- get
49+ put []
50+ return $ unlines body
51+
52+-- | Read an entire page
53+--
54+parsePage :: LineParser (Page String)
55+parsePage = Page <$> parseMetadata <*> parseBody
56+
57+-- | Read a page from a string
58+--
59+readPage :: String -> Page String
60+readPage = evalState parsePage . lines
src/Hakyll/Web/Pandoc.hsnew file+110
@@ -0,0 +1,110 @@
1+-- | Module exporting pandoc bindings
2+--
3+module Hakyll.Web.Pandoc
4+ ( -- * The basic building blocks
5+ readPandoc
6+ , readPandocWith
7+ , writePandoc
8+ , writePandocWith
9+
10+ -- * Functions working on pages/compilers
11+ , pageReadPandoc
12+ , pageReadPandocWith
13+ , pageRenderPandoc
14+ , pageRenderPandocWith
15+
16+ -- * Default options
17+ , defaultHakyllParserState
18+ , defaultHakyllWriterOptions
19+ ) where
20+
21+import Prelude hiding (id)
22+import Control.Applicative ((<$>))
23+import Control.Arrow ((>>^), (&&&))
24+import Control.Category (id)
25+
26+import Text.Pandoc
27+
28+import Hakyll.Core.Compiler
29+import Hakyll.Web.FileType
30+import Hakyll.Web.Page.Internal
31+
32+-- | Read a string using pandoc, with the default options
33+--
34+readPandoc :: FileType -- ^ File type, determines how parsing happens
35+ -> String -- ^ String to read
36+ -> Pandoc -- ^ Resulting document
37+readPandoc = readPandocWith defaultHakyllParserState
38+
39+-- | Read a string using pandoc, with the supplied options
40+--
41+readPandocWith :: ParserState -- ^ Parser options
42+ -> FileType -- ^ File type, determines how parsing happens
43+ -> String -- ^ String to read
44+ -> Pandoc -- ^ Resulting document
45+readPandocWith state fileType' = case fileType' of
46+ Html -> readHtml state
47+ LaTeX -> readLaTeX state
48+ LiterateHaskell t -> readPandocWith state {stateLiterateHaskell = True} t
49+ Markdown -> readMarkdown state
50+ Rst -> readRST state
51+ t -> error $
52+ "Hakyll.Web.readPandocWith: I don't know how to read " ++ show t
53+
54+-- | Write a document (as HTML) using pandoc, with the default options
55+--
56+writePandoc :: Pandoc -- ^ Document to write
57+ -> String -- ^ Resulting HTML
58+writePandoc = writePandocWith defaultHakyllWriterOptions
59+
60+-- | Write a document (as HTML) using pandoc, with the supplied options
61+--
62+writePandocWith :: WriterOptions -- ^ Writer options for pandoc
63+ -> Pandoc -- ^ Document to write
64+ -> String -- ^ Resulting HTML
65+writePandocWith = writeHtmlString
66+
67+-- | Read the resource using pandoc
68+--
69+pageReadPandoc :: Compiler (Page String) (Page Pandoc)
70+pageReadPandoc = pageReadPandocWith defaultHakyllParserState
71+
72+-- | Read the resource using pandoc
73+--
74+pageReadPandocWith :: ParserState -> Compiler (Page String) (Page Pandoc)
75+pageReadPandocWith state =
76+ id &&& getFileType >>^ pageReadPandocWith'
77+ where
78+ pageReadPandocWith' (p, t) = readPandocWith state t <$> p
79+
80+-- | Render the resource using pandoc
81+--
82+pageRenderPandoc :: Compiler (Page String) (Page String)
83+pageRenderPandoc =
84+ pageRenderPandocWith defaultHakyllParserState defaultHakyllWriterOptions
85+
86+-- | Render the resource using pandoc
87+--
88+pageRenderPandocWith :: ParserState
89+ -> WriterOptions
90+ -> Compiler (Page String) (Page String)
91+pageRenderPandocWith state options =
92+ pageReadPandocWith state >>^ fmap (writePandocWith options)
93+
94+-- | The default reader options for pandoc parsing in hakyll
95+--
96+defaultHakyllParserState :: ParserState
97+defaultHakyllParserState = defaultParserState
98+ { -- The following option causes pandoc to read smart typography, a nice
99+ -- and free bonus.
100+ stateSmart = True
101+ }
102+
103+-- | The default writer options for pandoc rendering in hakyll
104+--
105+defaultHakyllWriterOptions :: WriterOptions
106+defaultHakyllWriterOptions = defaultWriterOptions
107+ { -- This option causes literate haskell to be written using '>' marks in
108+ -- html, which I think is a good default.
109+ writerLiterateHaskell = True
110+ }
src/Hakyll/Web/Preview/Server.hsnew file+72
@@ -0,0 +1,72 @@
1+-- | Implements a basic static file server for previewing options
2+--
3+{-# LANGUAGE OverloadedStrings #-}
4+module Hakyll.Web.Preview.Server
5+ ( staticServer
6+ ) where
7+
8+import Control.Monad.Trans (liftIO)
9+import Control.Applicative ((<$>))
10+import Codec.Binary.UTF8.String
11+import System.FilePath ((</>))
12+import System.Directory (doesFileExist)
13+
14+import qualified Data.ByteString as SB
15+import Snap.Util.FileServe (serveFile)
16+import Snap.Types (Snap, rqURI, getRequest, writeBS)
17+import Snap.Http.Server ( httpServe, setAccessLog, setErrorLog, addListen
18+ , ConfigListen (..), emptyConfig
19+ )
20+
21+import Hakyll.Core.Util.String (replaceAll)
22+
23+-- | The first file in the list that actually exists is returned
24+--
25+findFile :: [FilePath] -> IO (Maybe FilePath)
26+findFile [] = return Nothing
27+findFile (x : xs) = do
28+ exists <- doesFileExist x
29+ if exists then return (Just x) else findFile xs
30+
31+-- | Serve a given directory
32+--
33+static :: FilePath -- ^ Directory to serve
34+ -> (FilePath -> IO ()) -- ^ Pre-serve hook
35+ -> Snap ()
36+static directory preServe = do
37+ -- Obtain the path
38+ uri <- rqURI <$> getRequest
39+ let filePath = replaceAll "\\?$" (const "") -- Remove trailing ?
40+ $ replaceAll "#[^#]*$" (const "") -- Remove #section
41+ $ replaceAll "^/" (const "") -- Remove leading /
42+ $ decode $ SB.unpack uri
43+
44+ -- Try to find the requested file
45+ r <- liftIO $ findFile $ map (directory </>) $
46+ [ filePath
47+ , filePath </> "index.htm"
48+ , filePath </> "index.html"
49+ ]
50+
51+ case r of
52+ -- Not found, error
53+ Nothing -> writeBS "Not found"
54+ -- Found, serve
55+ Just f -> do
56+ liftIO $ preServe f
57+ serveFile f
58+
59+-- | Main method, runs a static server in the given directory
60+--
61+staticServer :: FilePath -- ^ Directory to serve
62+ -> (FilePath -> IO ()) -- ^ Pre-serve hook
63+ -> Int -- ^ Port to listen on
64+ -> IO () -- ^ Blocks forever
65+staticServer directory preServe port =
66+ httpServe config $ static directory preServe
67+ where
68+ -- Snap server config
69+ config = addListen (ListenHttp "0.0.0.0" port)
70+ $ setAccessLog Nothing
71+ $ setErrorLog Nothing
72+ $ emptyConfig
src/Hakyll/Web/RelativizeUrls.hsnew file+62
@@ -0,0 +1,62 @@
1+-- | This module exposes a function which can relativize URL's on a webpage.
2+--
3+-- This means that one can deploy the resulting site on
4+-- @http:\/\/example.com\/@, but also on @http:\/\/example.com\/some-folder\/@
5+-- without having to change anything (simply copy over the files).
6+--
7+-- To use it, you should use absolute URL's from the site root everywhere. For
8+-- example, use
9+--
10+-- > <img src="/images/lolcat.png" alt="Funny zomgroflcopter" />
11+--
12+-- in a blogpost. When running this through the relativize URL's module, this
13+-- will result in (suppose your blogpost is located at @\/posts\/foo.html@:
14+--
15+-- > <img src="../images/lolcat.png" alt="Funny zomgroflcopter" />
16+--
17+module Hakyll.Web.RelativizeUrls
18+ ( relativizeUrlsCompiler
19+ , relativizeUrls
20+ ) where
21+
22+import Prelude hiding (id)
23+import Control.Category (id)
24+import Control.Arrow ((&&&), (>>^))
25+import Data.List (isPrefixOf)
26+import qualified Data.Set as S
27+
28+import Text.HTML.TagSoup
29+
30+import Hakyll.Core.Compiler
31+import Hakyll.Web.Page
32+import Hakyll.Web.Util.Url
33+
34+-- | Compiler form of 'compressCss' which automatically picks the right root
35+-- path
36+--
37+relativizeUrlsCompiler :: Compiler (Page String) (Page String)
38+relativizeUrlsCompiler = getRoute &&& id >>^ uncurry relativize
39+ where
40+ relativize Nothing = id
41+ relativize (Just r) = fmap (relativizeUrls $ toSiteRoot r)
42+
43+-- | Relativize URL's in HTML
44+--
45+relativizeUrls :: String -- ^ Path to the site root
46+ -> String -- ^ HTML to relativize
47+ -> String -- ^ Resulting HTML
48+relativizeUrls root = renderTags . map relativizeUrls' . parseTags
49+ where
50+ relativizeUrls' (TagOpen s a) = TagOpen s $ map (relativizeUrlsAttrs root) a
51+ relativizeUrls' x = x
52+
53+-- | Relativize URL's in attributes
54+--
55+relativizeUrlsAttrs :: String -- ^ Path to the site root
56+ -> Attribute String -- ^ Attribute to relativize
57+ -> Attribute String -- ^ Resulting attribute
58+relativizeUrlsAttrs root (key, value)
59+ | key `S.member` urls && "/" `isPrefixOf` value = (key, root ++ value)
60+ | otherwise = (key, value)
61+ where
62+ urls = S.fromList ["src", "href"]
src/Hakyll/Web/Tags.hsnew file+180
@@ -0,0 +1,180 @@
1+-- | Module containing some specialized functions to deal with tags.
2+-- This Module follows certain conventions. My advice is to stick with them if
3+-- possible.
4+--
5+-- More concrete: all functions in this module assume that the tags are
6+-- located in the @tags@ field, and separated by commas. An example file
7+-- @foo.markdown@ could look like:
8+--
9+-- > ---
10+-- > author: Philip K. Dick
11+-- > title: Do androids dream of electric sheep?
12+-- > tags: future, science fiction, humanoid
13+-- > ---
14+-- > The novel is set in a post-apocalyptic near future, where the Earth and
15+-- > its populations have been damaged greatly by Nuclear...
16+--
17+-- All the following functions would work with such a format. In addition to
18+-- tags, Hakyll also supports categories. The convention when using categories
19+-- is to place pages in subdirectories.
20+--
21+-- An example, the page @posts\/coding\/2010-01-28-hakyll-categories.markdown@
22+-- Tags or categories are read using the @readTags@ and @readCategory@
23+-- functions. This module only provides functions to work with tags:
24+-- categories are represented as tags. This is perfectly possible: categories
25+-- only have an additional restriction that a page can only have one category
26+-- (instead of multiple tags).
27+--
28+{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, Arrows #-}
29+module Hakyll.Web.Tags
30+ ( Tags (..)
31+ , readTagsWith
32+ , readTags
33+ , readCategory
34+ , renderTagCloud
35+ , renderTagsField
36+ , renderCategoryField
37+ ) where
38+
39+import Prelude hiding (id)
40+import Control.Category (id)
41+import Control.Applicative ((<$>))
42+import Data.Map (Map)
43+import qualified Data.Map as M
44+import Data.List (intersperse)
45+import Control.Arrow (arr, (&&&), (>>>), (***), (<<^), returnA)
46+import Data.Maybe (catMaybes, fromMaybe)
47+import Data.Monoid (mconcat)
48+
49+import Data.Typeable (Typeable)
50+import Data.Binary (Binary, get, put)
51+import Text.Blaze.Renderer.String (renderHtml)
52+import Text.Blaze ((!), toHtml, toValue)
53+import qualified Text.Blaze.Html5 as H
54+import qualified Text.Blaze.Html5.Attributes as A
55+
56+import Hakyll.Web.Page
57+import Hakyll.Web.Page.Metadata
58+import Hakyll.Web.Util.Url
59+import Hakyll.Core.Writable
60+import Hakyll.Core.Identifier
61+import Hakyll.Core.Compiler
62+import Hakyll.Core.Util.String
63+
64+-- | Data about tags
65+--
66+data Tags a = Tags
67+ { tagsMap :: Map String [Page a]
68+ } deriving (Show, Typeable)
69+
70+instance Binary a => Binary (Tags a) where
71+ get = Tags <$> get
72+ put (Tags m) = put m
73+
74+instance Writable (Tags a) where
75+ write _ _ = return ()
76+
77+-- | Obtain tags from a page
78+--
79+getTags :: Page a -> [String]
80+getTags = map trim . splitAll "," . getField "tags"
81+
82+-- | Obtain categories from a page
83+--
84+getCategory :: Page a -> [String]
85+getCategory = return . getField "category"
86+
87+-- | Higher-level function to read tags
88+--
89+readTagsWith :: (Page a -> [String]) -- ^ Function extracting tags from a page
90+ -> [Page a] -- ^ Pages
91+ -> Tags a -- ^ Resulting tags
92+readTagsWith f pages = Tags
93+ { tagsMap = foldl (M.unionWith (++)) M.empty (map readTagsWith' pages)
94+ }
95+ where
96+ -- Create a tag map for one page
97+ readTagsWith' page =
98+ let tags = f page
99+ in M.fromList $ zip tags $ repeat [page]
100+
101+-- | Read a tagmap using the @tags@ metadata field
102+--
103+readTags :: [Page a] -> Tags a
104+readTags = readTagsWith getTags
105+
106+-- | Read a tagmap using the @category@ metadata field
107+--
108+readCategory :: [Page a] -> Tags a
109+readCategory = readTagsWith getCategory
110+
111+-- | Render a tag cloud in HTML
112+--
113+renderTagCloud :: (String -> Identifier) -- ^ Produce a link for a tag
114+ -> Double -- ^ Smallest font size, in percent
115+ -> Double -- ^ Biggest font size, in percent
116+ -> Compiler (Tags a) String -- ^ Tag cloud renderer
117+renderTagCloud makeUrl minSize maxSize = proc (Tags tags) -> do
118+ -- In tags' we create a list: [((tag, route), count)]
119+ tags' <- mapCompiler ((id &&& (getRouteFor <<^ makeUrl)) *** arr length)
120+ -< M.toList tags
121+
122+ let -- Absolute frequencies of the pages
123+ freqs = map snd tags'
124+
125+ -- Find out the relative count of a tag: on a scale from 0 to 1
126+ relative count = (fromIntegral count - min') / (1 + max' - min')
127+
128+ -- Show the relative size of one 'count' in percent
129+ size count =
130+ let size' = floor $ minSize + relative count * (maxSize - minSize)
131+ in show (size' :: Int) ++ "%"
132+
133+ -- The minimum and maximum count found, as doubles
134+ (min', max')
135+ | null freqs = (0, 1)
136+ | otherwise = (minimum &&& maximum) $ map fromIntegral freqs
137+
138+ -- Create a link for one item
139+ makeLink ((tag, url), count) =
140+ H.a ! A.style (toValue $ "font-size: " ++ size count)
141+ ! A.href (toValue $ fromMaybe "/" url)
142+ $ toHtml tag
143+
144+ -- Render and return the HTML
145+ returnA -< renderHtml $ mconcat $ intersperse " " $ map makeLink tags'
146+
147+-- | Render tags with links
148+--
149+renderTagsFieldWith :: (Page a -> [String]) -- ^ Function to get the tags
150+ -> String -- ^ Destination key
151+ -> (String -> Identifier) -- ^ Create a link for a tag
152+ -> Compiler (Page a) (Page a) -- ^ Resulting compiler
153+renderTagsFieldWith tags destination makeUrl =
154+ id &&& arr tags >>> setFieldA destination renderTags
155+ where
156+ -- Compiler creating a comma-separated HTML string for a list of tags
157+ renderTags :: Compiler [String] String
158+ renderTags = arr (map $ id &&& makeUrl)
159+ >>> mapCompiler (id *** getRouteFor)
160+ >>> arr (map $ uncurry renderLink)
161+ >>> arr (renderHtml . mconcat . intersperse ", " . catMaybes)
162+
163+ -- Render one tag link
164+ renderLink _ Nothing = Nothing
165+ renderLink tag (Just filePath) = Just $
166+ H.a ! A.href (toValue $ toUrl filePath) $ toHtml tag
167+
168+-- | Render tags with links
169+--
170+renderTagsField :: String -- ^ Destination key
171+ -> (String -> Identifier) -- ^ Create a link for a tag
172+ -> Compiler (Page a) (Page a) -- ^ Resulting compiler
173+renderTagsField = renderTagsFieldWith getTags
174+
175+-- | Render the category in a link
176+--
177+renderCategoryField :: String -- ^ Destination key
178+ -> (String -> Identifier) -- ^ Create a category link
179+ -> Compiler (Page a) (Page a) -- ^ Resulting compiler
180+renderCategoryField = renderTagsFieldWith getCategory
src/Hakyll/Web/Template.hsnew file+109
@@ -0,0 +1,109 @@
1+-- | This module provides means for reading and applying 'Template's.
2+--
3+-- Templates are tools to convert data (pages) into a string. They are
4+-- perfectly suited for laying out your site.
5+--
6+-- Let's look at an example template:
7+--
8+-- > <html>
9+-- > <head>
10+-- > <title>My crazy homepage - $title$</title>
11+-- > </head>
12+-- > <body>
13+-- > <div id="header">
14+-- > <h1>My crazy homepage - $title$</h1>
15+-- > </div>
16+-- > <div id="content">
17+-- > $body$
18+-- > </div>
19+-- > <div id="footer">
20+-- > By reading this you agree that I now own your soul
21+-- > </div>
22+-- > </body>
23+-- > </html>
24+--
25+-- We can use this template to render a 'Page' which has a body and a @$title$@
26+-- metadata field.
27+--
28+-- As you can see, the format is very simple -- @$key$@ is used to render the
29+-- @$key$@ field from the page, everything else is literally copied. If you want
30+-- to literally insert @\"$key$\"@ into your page (for example, when you're
31+-- writing a Hakyll tutorial) you can use
32+--
33+-- > <p>
34+-- > A literal $$key$$.
35+-- > </p>
36+--
37+-- Because of it's simplicity, these templates can be used for more than HTML:
38+-- you could make, for example, CSS or JS templates as well.
39+--
40+-- In addition to the native format, Hakyll also supports hamlet templates. For
41+-- more information on hamlet templates, please refer to:
42+-- <http://hackage.haskell.org/package/hamlet>.
43+--
44+module Hakyll.Web.Template
45+ ( Template
46+ , applyTemplate
47+ , applySelf
48+ , templateCompiler
49+ , templateCompilerWith
50+ , applyTemplateCompiler
51+ ) where
52+
53+import Control.Arrow
54+import Data.Maybe (fromMaybe)
55+import System.FilePath (takeExtension)
56+import qualified Data.Map as M
57+
58+import Text.Hamlet (HamletSettings, defaultHamletSettings)
59+
60+import Hakyll.Core.Compiler
61+import Hakyll.Core.Identifier
62+import Hakyll.Core.ResourceProvider
63+import Hakyll.Web.Template.Internal
64+import Hakyll.Web.Template.Read
65+import Hakyll.Web.Page.Internal
66+
67+-- | Substitutes @$identifiers@ in the given @Template@ by values from the given
68+-- "Page". When a key is not found, it is left as it is. You can specify
69+-- the characters used to replace escaped dollars (@$$@) here.
70+--
71+applyTemplate :: Template -> Page String -> Page String
72+applyTemplate template page =
73+ fmap (const $ substitute =<< unTemplate template) page
74+ where
75+ map' = toMap page
76+ substitute (Chunk chunk) = chunk
77+ substitute (Key key) = fromMaybe ("$" ++ key ++ "$") $ M.lookup key map'
78+ substitute (Escaped) = "$"
79+
80+-- | Apply a page as it's own template. This is often very useful to fill in
81+-- certain keys like @$root@ and @$url@.
82+--
83+applySelf :: Page String -> Page String
84+applySelf page = applyTemplate (readTemplate $ pageBody page) page
85+
86+-- | Read a template. If the extension of the file we're compiling is
87+-- @.hml@ or @.hamlet@, it will be considered as a Hamlet template, and parsed
88+-- as such.
89+--
90+templateCompiler :: Compiler Resource Template
91+templateCompiler = templateCompilerWith defaultHamletSettings
92+
93+-- | Version of 'templateCompiler' that enables custom settings.
94+--
95+templateCompilerWith :: HamletSettings -> Compiler Resource Template
96+templateCompilerWith settings =
97+ cached "Hakyll.Web.Template.templateCompilerWith" $
98+ getIdentifier &&& getResourceString >>^ uncurry read'
99+ where
100+ read' identifier string =
101+ if takeExtension (toFilePath identifier) `elem` [".hml", ".hamlet"]
102+ -- Hamlet template
103+ then readHamletTemplateWith settings string
104+ -- Hakyll template
105+ else readTemplate string
106+
107+applyTemplateCompiler :: Identifier -- ^ Template
108+ -> Compiler (Page String) (Page String) -- ^ Compiler
109+applyTemplateCompiler identifier = require identifier (flip applyTemplate)
src/Hakyll/Web/Template/Internal.hsnew file+45
@@ -0,0 +1,45 @@
1+-- | Module containing the template data structure
2+--
3+{-# LANGUAGE GeneralizedNewtypeDeriving, DeriveDataTypeable #-}
4+module Hakyll.Web.Template.Internal
5+ ( Template (..)
6+ , TemplateElement (..)
7+ ) where
8+
9+import Control.Applicative ((<$>))
10+
11+import Data.Binary (Binary, get, getWord8, put, putWord8)
12+import Data.Typeable (Typeable)
13+
14+import Hakyll.Core.Writable
15+
16+-- | Datatype used for template substitutions.
17+--
18+newtype Template = Template
19+ { unTemplate :: [TemplateElement]
20+ }
21+ deriving (Show, Eq, Binary, Typeable)
22+
23+instance Writable Template where
24+ -- Writing a template is impossible
25+ write _ _ = return ()
26+
27+-- | Elements of a template.
28+--
29+data TemplateElement
30+ = Chunk String
31+ | Key String
32+ | Escaped
33+ deriving (Show, Eq, Typeable)
34+
35+instance Binary TemplateElement where
36+ put (Chunk string) = putWord8 0 >> put string
37+ put (Key key) = putWord8 1 >> put key
38+ put (Escaped) = putWord8 2
39+
40+ get = getWord8 >>= \tag -> case tag of
41+ 0 -> Chunk <$> get
42+ 1 -> Key <$> get
43+ 2 -> return Escaped
44+ _ -> error $ "Hakyll.Web.Template.Internal: "
45+ ++ "Error reading cached template"
src/Hakyll/Web/Template/Read.hsnew file+10
@@ -0,0 +1,10 @@
1+-- | Re-exports all different template reading modules
2+--
3+module Hakyll.Web.Template.Read
4+ ( readTemplate
5+ , readHamletTemplate
6+ , readHamletTemplateWith
7+ ) where
8+
9+import Hakyll.Web.Template.Read.Hakyll
10+import Hakyll.Web.Template.Read.Hamlet
src/Hakyll/Web/Template/Read/Hakyll.hsnew file+35
@@ -0,0 +1,35 @@
1+-- | Read templates in Hakyll's native format
2+--
3+module Hakyll.Web.Template.Read.Hakyll
4+ ( readTemplate
5+ ) where
6+
7+import Data.List (isPrefixOf)
8+import Data.Char (isAlphaNum)
9+
10+import Hakyll.Web.Template.Internal
11+
12+-- | Construct a @Template@ from a string.
13+--
14+readTemplate :: String -> Template
15+readTemplate = Template . readTemplate'
16+ where
17+ readTemplate' [] = []
18+ readTemplate' string
19+ | "$$" `isPrefixOf` string =
20+ Escaped : readTemplate' (drop 2 string)
21+ | "$" `isPrefixOf` string =
22+ case readKey (drop 1 string) of
23+ Just (key, rest) -> Key key : readTemplate' rest
24+ Nothing -> Chunk "$" : readTemplate' (drop 1 string)
25+ | otherwise =
26+ let (chunk, rest) = break (== '$') string
27+ in Chunk chunk : readTemplate' rest
28+
29+ -- Parse an key into (key, rest) if it's valid, and return
30+ -- Nothing otherwise
31+ readKey string =
32+ let (key, rest) = span isAlphaNum string
33+ in if not (null key) && "$" `isPrefixOf` rest
34+ then Just (key, drop 1 rest)
35+ else Nothing
src/Hakyll/Web/Template/Read/Hamlet.hsnew file+46
@@ -0,0 +1,46 @@
1+-- | Read templates in the hamlet format
2+--
3+{-# LANGUAGE MultiParamTypeClasses #-}
4+module Hakyll.Web.Template.Read.Hamlet
5+ ( readHamletTemplate
6+ , readHamletTemplateWith
7+ ) where
8+
9+import Text.Hamlet (HamletSettings (..), defaultHamletSettings)
10+import Text.Hamlet.RT
11+
12+import Hakyll.Web.Template.Internal
13+
14+-- | Read a hamlet template using the default settings
15+--
16+readHamletTemplate :: String -> Template
17+readHamletTemplate = readHamletTemplateWith defaultHamletSettings
18+
19+-- | Read a hamlet template using the specified settings
20+--
21+readHamletTemplateWith :: HamletSettings -> String -> Template
22+readHamletTemplateWith settings string =
23+ let result = parseHamletRT settings string
24+ in case result of
25+ Just hamlet -> fromHamletRT hamlet
26+ Nothing -> error
27+ "Hakyll.Web.Template.Read.Hamlet.readHamletTemplateWith: \
28+ \Could not parse Hamlet file"
29+
30+-- | Convert a 'HamletRT' to a 'Template'
31+--
32+fromHamletRT :: HamletRT -- ^ Hamlet runtime template
33+ -> Template -- ^ Hakyll template
34+fromHamletRT (HamletRT sd) = Template $ map fromSimpleDoc sd
35+ where
36+ fromSimpleDoc :: SimpleDoc -> TemplateElement
37+ fromSimpleDoc (SDRaw chunk) = Chunk chunk
38+ fromSimpleDoc (SDVar [var]) = Key var
39+ fromSimpleDoc (SDVar _) = error
40+ "Hakyll.Web.Template.Read.Hamlet.fromHamletRT: \
41+ \Hakyll does not support '.' in identifier names when using \
42+ \hamlet templates."
43+ fromSimpleDoc _ = error
44+ "Hakyll.Web.Template.Read.Hamlet.fromHamletRT: \
45+ \Only simple $key$ identifiers are allowed when using hamlet \
46+ \templates."
src/Hakyll/Web/Util/Url.hsnew file+30
@@ -0,0 +1,30 @@
1+-- | Miscellaneous URL manipulation functions.
2+--
3+module Hakyll.Web.Util.Url
4+ ( toUrl
5+ , toSiteRoot
6+ ) where
7+
8+import System.FilePath (splitPath, takeDirectory, joinPath)
9+
10+-- | Convert a filepath to an URL starting from the site root
11+--
12+-- Example:
13+--
14+-- > toUrl "foo/bar.html"
15+--
16+-- Result:
17+--
18+-- > "/foo/bar.html"
19+--
20+toUrl :: FilePath -> String
21+toUrl = ('/' :)
22+
23+-- | Get the relative url to the site root, for a given (absolute) url
24+--
25+toSiteRoot :: String -> String
26+toSiteRoot = emptyException . joinPath . map parent . splitPath . takeDirectory
27+ where
28+ parent = const ".."
29+ emptyException [] = "."
30+ emptyException x = x
src/Network/Hakyll/SimpleServer.hsdeleted−215
@@ -1,215 +0,0 @@
1−-- | Module containing a small, simple http file server for testing and preview
2−-- purposes.
3−module Network.Hakyll.SimpleServer
4− ( simpleServer
5− ) where
6−
7−import Prelude hiding (log)
8−import Control.Monad (forever)
9−import Control.Monad.Reader (ReaderT, runReaderT, ask, liftIO)
10−import Network
11−import System.IO
12−import System.Directory (doesFileExist, doesDirectoryExist)
13−import Control.Concurrent (forkIO)
14−import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
15−import System.FilePath (takeExtension)
16−import qualified Data.Map as M
17−import Data.List (intercalate)
18−
19−import Text.Hakyll.Util
20−import Text.Hakyll.Regex
21−
22−-- | Function to log from a chan.
23−log :: Chan String -> IO ()
24−log logChan = forever (readChan logChan >>= hPutStrLn stderr)
25−
26−-- | General server configuration.
27−data ServerConfig = ServerConfig { documentRoot :: FilePath
28− , portNumber :: PortNumber
29− , logChannel :: Chan String
30− }
31−
32−-- | Custom monad stack.
33−type Server = ReaderT ServerConfig IO
34−
35−-- | Simple representation of a HTTP request.
36−data Request = Request { requestMethod :: String
37− , requestURI :: String
38− , requestVersion :: String
39− } deriving (Ord, Eq)
40−
41−instance Show Request where
42− show request = requestMethod request ++ " "
43− ++ requestURI request ++ " "
44− ++ requestVersion request
45−
46−-- | Read a HTTP request from a 'Handle'. For now, this will ignore the request
47−-- headers and body.
48−readRequest :: Handle -> Server Request
49−readRequest handle = do
50− requestLine <- liftIO $ hGetLine handle
51− let [method, uri, version] = map trim $ splitRegex " " requestLine
52− request = Request { requestMethod = method
53− , requestURI = uri
54− , requestVersion = version
55− }
56− return request
57−
58−-- | Simple representation of the HTTP response we send back.
59−data Response = Response { responseVersion :: String
60− , responseStatusCode :: Int
61− , responsePhrase :: String
62− , responseHeaders :: M.Map String String
63− , responseBody :: String
64− } deriving (Ord, Eq)
65−
66−instance Show Response where
67− show response = responseVersion response ++ " "
68− ++ show (responseStatusCode response) ++ " "
69− ++ responsePhrase response
70−
71−-- | A default response.
72−defaultResponse :: Response
73−defaultResponse = Response { responseVersion = "HTTP/1.1"
74− , responseStatusCode = 0
75− , responsePhrase = ""
76− , responseHeaders = M.empty
77− , responseBody = ""
78− }
79−
80−-- | Create a response for a given HTTP request.
81−createResponse :: Request -> Server Response
82−createResponse request
83− | requestMethod request == "GET" = createGetResponse request
84− | otherwise = return $ createErrorResponse 501 "Not Implemented"
85−
86−-- | Create a simple error response.
87−createErrorResponse :: Int -- ^ Error code.
88− -> String -- ^ Error phrase.
89− -> Response -- ^ Result.
90−createErrorResponse statusCode phrase = defaultResponse
91− { responseStatusCode = statusCode
92− , responsePhrase = phrase
93− , responseHeaders = M.singleton "Content-Type" "text/html"
94− , responseBody =
95− "<html> <head> <title>" ++ show statusCode ++ "</title> </head>"
96− ++ "<body> <h1>" ++ show statusCode ++ "</h1>\n"
97− ++ "<p>" ++ phrase ++ "</p> </body> </html>"
98− }
99−
100−-- | Create a simple get response.
101−createGetResponse :: Request -> Server Response
102−createGetResponse request = do
103− -- Construct the complete fileName of the requested resource.
104− config <- ask
105− let -- Drop everything after a '?'.
106− uri = takeWhile ((/=) '?') $ requestURI request
107− log' = writeChan (logChannel config)
108− isDirectory <- liftIO $ doesDirectoryExist $ documentRoot config ++ uri
109− let fileName =
110− documentRoot config ++ if isDirectory then uri ++ "/index.html"
111− else uri
112−
113− create200 = do
114− h <- openBinaryFile fileName ReadMode
115− contentLength <- hFileSize h
116− body <- hGetContents h
117− let mimeHeader = getMIMEHeader fileName
118− headers = ("Content-Length", show contentLength) : mimeHeader
119− return $ defaultResponse
120− { responseStatusCode = 200
121− , responsePhrase = "OK"
122− , responseHeaders = responseHeaders defaultResponse
123− `M.union` M.fromList headers
124− , responseBody = body
125− }
126−
127− -- Called when an error occurs during the creation of a 200 response.
128− create500 e = do
129− log' $ "Internal Error: " ++ show e
130− return $ createErrorResponse 500 "Internal Server Error"
131−
132− -- Send back the page if found.
133− exists <- liftIO $ doesFileExist fileName
134− if exists
135− then liftIO $ catch create200 create500
136− else do liftIO $ log' $ "Not Found: " ++ fileName
137− return $ createErrorResponse 404 "Not Found"
138−
139−-- | Get the mime header for a certain filename. This is based on the extension
140−-- of the given 'FilePath'.
141−getMIMEHeader :: FilePath -> [(String, String)]
142−getMIMEHeader fileName =
143− case result of (Just x) -> [("Content-Type", x)]
144− Nothing -> []
145− where
146− result = lookup (takeExtension fileName) [ (".css", "text/css")
147− , (".gif", "image/gif")
148− , (".htm", "text/html")
149− , (".html", "text/html")
150− , (".jpeg", "image/jpeg")
151− , (".jpg", "image/jpeg")
152− , (".js", "text/javascript")
153− , (".png", "image/png")
154− , (".txt", "text/plain")
155− , (".xml", "text/xml")
156− ]
157−
158−-- | Respond to an incoming request.
159−respond :: Handle -> Server ()
160−respond handle = do
161− -- Read the request and create a response.
162− request <- readRequest handle
163− response <- createResponse request
164−
165− -- Generate some output.
166− config <- ask
167− liftIO $ writeChan (logChannel config)
168− $ show request ++ " => " ++ show response
169−
170− -- Send the response back to the handle.
171− liftIO $ putResponse response
172− where
173− putResponse response = do hPutStr handle $ intercalate " "
174− [ responseVersion response
175− , show $ responseStatusCode response
176− , responsePhrase response
177− ]
178− hPutStr handle "\r\n"
179− mapM_ putHeader
180− (M.toList $ responseHeaders response)
181− hPutStr handle "\r\n"
182− hPutStr handle $ responseBody response
183− hPutStr handle "\r\n"
184− hClose handle
185−
186− putHeader (key, value) =
187− hPutStr handle $ key ++ ": " ++ value ++ "\r\n"
188−
189−-- | Start a simple http server on the given 'PortNumber', serving the given
190−-- directory.
191−--
192−simpleServer :: PortNumber -- ^ Port to listen on.
193− -> FilePath -- ^ Root directory to serve.
194− -> IO () -- ^ Optional pre-respond action.
195− -> IO ()
196−simpleServer port root preRespond = do
197− -- Channel to send logs to
198− logChan <- newChan
199−
200− let config = ServerConfig { documentRoot = root
201− , portNumber = port
202− , logChannel = logChan
203− }
204−
205− -- When a client connects, respond in a separate thread.
206− listen socket = do (handle, _, _) <- accept socket
207− preRespond
208− forkIO (runReaderT (respond handle) config)
209−
210− -- Handle logging in a separate thread
211− _ <- forkIO (log logChan)
212−
213− writeChan logChan $ "Starting hakyll server on port " ++ show port ++ "..."
214− socket <- listenOn (PortNumber port)
215− forever (listen socket)
src/Text/Hakyll.hsdeleted−185
@@ -1,185 +0,0 @@
1−-- | This is the main Hakyll module, exporting the important @hakyll@ function.
2−--
3−-- Most configurations would use this @hakyll@ function more or less as the
4−-- main function:
5−--
6−-- > main = hakyll $ do
7−-- > directory css "css"
8−-- > directory static "images"
9−--
10−module Text.Hakyll
11− ( defaultHakyllConfiguration
12− , hakyll
13− , hakyllWithConfiguration
14− , runDefaultHakyll
15−
16− , module Text.Hakyll.Context
17− , module Text.Hakyll.ContextManipulations
18− , module Text.Hakyll.CreateContext
19− , module Text.Hakyll.File
20− , module Text.Hakyll.HakyllMonad
21− , module Text.Hakyll.Regex
22− , module Text.Hakyll.Render
23− , module Text.Hakyll.HakyllAction
24− , module Text.Hakyll.Paginate
25− , module Text.Hakyll.Page
26− , module Text.Hakyll.Pandoc
27− , module Text.Hakyll.Util
28− , module Text.Hakyll.Tags
29− , module Text.Hakyll.Feed
30− , module Text.Hakyll.Configurations.Static
31− ) where
32−
33−import Control.Concurrent (forkIO, threadDelay)
34−import Control.Monad.Reader (runReaderT, liftIO, ask)
35−import Control.Monad (when)
36−import Data.Monoid (mempty)
37−import System.Environment (getArgs, getProgName)
38−import System.Directory (doesDirectoryExist, removeDirectoryRecursive)
39−import System.Time (getClockTime)
40−
41−import Text.Pandoc
42−import Text.Hamlet (defaultHamletSettings)
43−
44−import Network.Hakyll.SimpleServer (simpleServer)
45−import Text.Hakyll.Context
46−import Text.Hakyll.ContextManipulations
47−import Text.Hakyll.CreateContext
48−import Text.Hakyll.File
49−import Text.Hakyll.HakyllMonad
50−import Text.Hakyll.Regex
51−import Text.Hakyll.Render
52−import Text.Hakyll.HakyllAction
53−import Text.Hakyll.Paginate
54−import Text.Hakyll.Page
55−import Text.Hakyll.Pandoc
56−import Text.Hakyll.Util
57−import Text.Hakyll.Tags
58−import Text.Hakyll.Feed
59−import Text.Hakyll.Configurations.Static
60−
61−-- | The default reader options for pandoc parsing.
62−--
63−defaultPandocParserState :: ParserState
64−defaultPandocParserState = defaultParserState
65− { -- The following option causes pandoc to read smart typography, a nice
66− -- and free bonus.
67− stateSmart = True
68− }
69−
70−-- | The default writer options for pandoc rendering.
71−--
72−defaultPandocWriterOptions :: WriterOptions
73−defaultPandocWriterOptions = defaultWriterOptions
74− { -- This option causes literate haskell to be written using '>' marks in
75− -- html, which I think is a good default.
76− writerLiterateHaskell = True
77− }
78−
79−-- | The default hakyll configuration.
80−--
81−defaultHakyllConfiguration :: String -- ^ Absolute site URL.
82− -> HakyllConfiguration -- ^ Default config.
83−defaultHakyllConfiguration absoluteUrl' = HakyllConfiguration
84− { absoluteUrl = absoluteUrl'
85− , additionalContext = mempty
86− , siteDirectory = "_site"
87− , cacheDirectory = "_cache"
88− , enableIndexUrl = False
89− , previewMode = BuildOnRequest
90− , pandocParserState = defaultPandocParserState
91− , pandocWriterOptions = defaultPandocWriterOptions
92− , hamletSettings = defaultHamletSettings
93− }
94−
95−-- | Main function to run Hakyll with the default configuration. The
96−-- absolute URL is only used in certain cases, for example RSS feeds et
97−-- cetera.
98−--
99−hakyll :: String -- ^ Absolute URL of your site. Used in certain cases.
100− -> Hakyll () -- ^ You code.
101− -> IO ()
102−hakyll absolute = hakyllWithConfiguration configuration
103− where
104− configuration = defaultHakyllConfiguration absolute
105−
106−-- | Main function to run hakyll with a custom configuration.
107−--
108−hakyllWithConfiguration :: HakyllConfiguration -> Hakyll () -> IO ()
109−hakyllWithConfiguration configuration buildFunction = do
110− args <- getArgs
111− let f = case args of ["build"] -> buildFunction
112− ["clean"] -> clean
113− ["preview", p] -> preview (read p)
114− ["preview"] -> preview defaultPort
115− ["rebuild"] -> clean >> buildFunction
116− ["server", p] -> server (read p) (return ())
117− ["server"] -> server defaultPort (return ())
118− _ -> help
119− runReaderT f configuration
120− where
121− preview port = case previewMode configuration of
122− BuildOnRequest -> server port buildFunction
123− BuildOnInterval -> do
124− let pIO = runReaderT (previewThread buildFunction) configuration
125− _ <- liftIO $ forkIO pIO
126− server port (return ())
127−
128− defaultPort = 8000
129−
130−-- | A preview thread that periodically recompiles the site.
131−--
132−previewThread :: Hakyll () -- ^ Build function
133− -> Hakyll () -- ^ Result
134−previewThread buildFunction = run =<< liftIO getClockTime
135− where
136− delay = 1000000
137− run time = do liftIO $ threadDelay delay
138− contents <- getRecursiveContents "."
139− valid <- isMoreRecent time contents
140− when valid buildFunction
141− run =<< liftIO getClockTime
142−
143−-- | Clean up directories.
144−--
145−clean :: Hakyll ()
146−clean = do askHakyll siteDirectory >>= remove'
147− askHakyll cacheDirectory >>= remove'
148− where
149− remove' dir = liftIO $ do putStrLn $ "Removing " ++ dir ++ "..."
150− exists <- doesDirectoryExist dir
151− when exists $ removeDirectoryRecursive dir
152−
153−-- | Show usage information.
154−--
155−help :: Hakyll ()
156−help = liftIO $ do
157− name <- getProgName
158− putStrLn $ "This is a Hakyll site generator program. You should always\n"
159− ++ "run it from the project root directory.\n"
160− ++ "\n"
161− ++ "Usage:\n"
162− ++ name ++ " build Generate the site.\n"
163− ++ name ++ " clean Clean up and remove cache.\n"
164− ++ name ++ " help Show this message.\n"
165− ++ name ++ " preview [port] Run a server and autocompile.\n"
166− ++ name ++ " rebuild Clean up and build again.\n"
167− ++ name ++ " server [port] Run a local test server.\n"
168−
169−-- | Start a server at the given port number.
170−--
171−server :: Integer -- ^ Port number to serve on.
172− -> Hakyll () -- ^ Pre-respond action.
173− -> Hakyll ()
174−server port preRespond = do
175− configuration <- ask
176− root <- askHakyll siteDirectory
177− let preRespondIO = runReaderT preRespond configuration
178− liftIO $ simpleServer (fromIntegral port) root preRespondIO
179−
180−-- | Run a Hakyll action with default settings. This is mostly aimed at testing
181−-- code.
182−--
183−runDefaultHakyll :: Hakyll a -> IO a
184−runDefaultHakyll f =
185− runReaderT f $ defaultHakyllConfiguration "http://example.com"
src/Text/Hakyll/Configurations/Static.hsdeleted−59
@@ -1,59 +0,0 @@
1−-- | Module for a simple static configuration of a website.
2−--
3−-- The configuration works like this:
4−--
5−-- * The @templates/@ directory should contain one template.
6−--
7−-- * Renderable files in the directory tree are rendered using this template.
8−--
9−-- * The @static/@ directory is copied entirely (if it exists).
10−--
11−-- * All files in the @css/@ directory are compressed.
12−--
13−module Text.Hakyll.Configurations.Static
14− ( staticConfiguration
15− ) where
16−
17−import Control.Applicative ((<$>))
18−import Control.Monad (filterM, forM_)
19−
20−import Text.Hakyll.File ( getRecursiveContents, inDirectory, inHakyllDirectory
21− , directory )
22−import Text.Hakyll.Internal.FileType (isRenderableFile)
23−import Text.Hakyll.HakyllMonad (Hakyll, logHakyll)
24−import Text.Hakyll.Render (renderChain, css, static)
25−import Text.Hakyll.CreateContext (createPage)
26−
27−-- | A simple configuration for an entirely static website.
28−--
29−staticConfiguration :: Hakyll ()
30−staticConfiguration = do
31− -- Find all files not in _site or _cache.
32− files <- filterM isRenderableFile' =<< getRecursiveContents "."
33−
34− -- Find a main template to use
35− mainTemplate <- take 1 <$> getRecursiveContents templateDir
36− logHakyll $ case mainTemplate of [] -> "Using no template"
37− (x : _) -> "Using template " ++ x
38−
39− -- Render all files using this template
40− forM_ files $ renderChain mainTemplate . createPage
41−
42− -- Render a static directory
43− directory static staticDir
44−
45− -- Render a css directory
46− directory css cssDir
47− where
48− -- A file should have a renderable extension and not be in a hakyll
49− -- directory, and not in a special directory.
50− isRenderableFile' file = do
51− inHakyllDirectory' <- inHakyllDirectory file
52− return $ isRenderableFile file
53− && not (any (inDirectory file) [templateDir, cssDir, staticDir])
54− && not inHakyllDirectory'
55−
56− -- Directories
57− templateDir = "templates"
58− cssDir = "css"
59− staticDir = "static"
src/Text/Hakyll/Context.hsdeleted−16
@@ -1,16 +0,0 @@
1−-- | This (quite small) module exports the datatype used for contexts. A
2−-- @Context@ is a simple key-value mapping. You can render these @Context@s
3−-- with templates, and manipulate them in various ways.
4−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
5−module Text.Hakyll.Context
6− ( Context (..)
7− ) where
8−
9−import Data.Monoid (Monoid)
10−import Data.Map (Map)
11−import Data.Binary (Binary)
12−
13−-- | Datatype used for key-value mappings.
14−newtype Context = Context { -- | Extract the context.
15− unContext :: Map String String
16− } deriving (Show, Monoid, Binary)
src/Text/Hakyll/ContextManipulations.hsdeleted−124
@@ -1,124 +0,0 @@
1−-- | This module exports a number of functions that produce @HakyllAction@s to
2−-- manipulate @Context@s.
3−module Text.Hakyll.ContextManipulations
4− ( renderValue
5− , changeValue
6− , changeUrl
7− , copyValue
8− , renderDate
9− , renderDateWithLocale
10− , changeExtension
11− , renderBody
12− , takeBody
13− ) where
14−
15−import Control.Monad (liftM)
16−import Control.Arrow (arr)
17−import System.Locale (TimeLocale, defaultTimeLocale)
18−import System.FilePath (takeFileName, addExtension, dropExtension)
19−import Data.Time.Format (parseTime, formatTime)
20−import Data.Time.Clock (UTCTime)
21−import Data.Maybe (fromMaybe)
22−import qualified Data.Map as M
23−
24−import Text.Hakyll.Regex (substituteRegex)
25−import Text.Hakyll.HakyllAction (HakyllAction (..))
26−import Text.Hakyll.Context (Context (..))
27−
28−-- | Do something with a value in a @Context@, but keep the old value as well.
29−-- If the key given is not present in the @Context@, nothing will happen.
30−--
31−renderValue :: String -- ^ Key of which the value should be copied.
32− -> String -- ^ Key the value should be copied to.
33− -> (String -> String) -- ^ Function to apply on the value.
34− -> HakyllAction Context Context
35−renderValue source destination f = arr $ \(Context context) -> Context $
36− case M.lookup source context of
37− Nothing -> context
38− (Just value) -> M.insert destination (f value) context
39−
40−-- | Change a value in a @Context@.
41−--
42−-- > import Data.Char (toUpper)
43−-- > changeValue "title" (map toUpper)
44−--
45−-- Will put the title in UPPERCASE.
46−changeValue :: String -- ^ Key to change.
47− -> (String -> String) -- ^ Function to apply on the value.
48− -> HakyllAction Context Context
49−changeValue key = renderValue key key
50−
51−-- | Change the URL of a page. This requires a special function, so dependency
52−-- handling can happen correctly.
53−--
54−changeUrl :: (String -> String) -- ^ Function to change URL with.
55− -> HakyllAction Context Context -- ^ Resulting action.
56−changeUrl f = let action = changeValue "url" f
57− in action {actionUrl = Right $ liftM f}
58−
59−-- | Copy a value from one key to another in a @Context@.
60−copyValue :: String -- ^ Source key.
61− -> String -- ^ Destination key.
62− -> HakyllAction Context Context
63−copyValue source destination = renderValue source destination id
64−
65−-- | When the context has a key called @path@ in a
66−-- @folder/yyyy-mm-dd-title.extension@ format (the convention for pages),
67−-- this function can render the date.
68−--
69−-- > renderDate "date" "%B %e, %Y" "Date unknown"
70−--
71−-- Will render something like @January 32, 2010@.
72−--
73−renderDate :: String -- ^ Key in which the rendered date should be placed.
74− -> String -- ^ Format to use on the date.
75− -> String -- ^ Default key, in case the date cannot be parsed.
76− -> HakyllAction Context Context
77−renderDate = renderDateWithLocale defaultTimeLocale
78−
79−-- | This is an extended version of 'renderDate' that allows you to specify a
80−-- time locale that is used for outputting the date. For more details, see
81−-- 'renderDate'.
82−--
83−renderDateWithLocale :: TimeLocale -- ^ Output time locale.
84− -> String -- ^ Destination key.
85− -> String -- ^ Format to use on the date.
86− -> String -- ^ Default key.
87− -> HakyllAction Context Context
88−renderDateWithLocale locale key format defaultValue =
89− renderValue "path" key renderDate'
90− where
91− renderDate' filePath = fromMaybe defaultValue $ do
92− let dateString = substituteRegex "^([0-9]*-[0-9]*-[0-9]*).*" "\\1"
93− (takeFileName filePath)
94− time <- parseTime defaultTimeLocale
95− "%Y-%m-%d"
96− dateString :: Maybe UTCTime
97− return $ formatTime locale format time
98−
99−-- | Change the extension of a file. This is only needed when you want to
100−-- render, for example, mardown to @.php@ files instead of @.html@ files.
101−--
102−-- > changeExtension "php"
103−--
104−-- Will render @test.markdown@ to @test.php@ instead of @test.html@.
105−changeExtension :: String -- ^ Extension to change to.
106− -> HakyllAction Context Context
107−changeExtension extension = changeValue "url" changeExtension'
108− where
109− changeExtension' = flip addExtension extension . dropExtension
110−
111−-- | Change the body of a file using a certain manipulation.
112−--
113−-- > import Data.Char (toUpper)
114−-- > renderBody (map toUpper)
115−--
116−-- Will put the entire body of the page in UPPERCASE.
117−renderBody :: (String -> String)
118− -> HakyllAction Context Context
119−renderBody = renderValue "body" "body"
120−
121−-- | Get the resulting body text from a context
122−--
123−takeBody :: HakyllAction Context String
124−takeBody = arr $ fromMaybe "" . M.lookup "body" . unContext
src/Text/Hakyll/CreateContext.hsdeleted−114
@@ -1,114 +0,0 @@
1−-- | A module that provides different ways to create a @Context@. These
2−-- functions all use the @HakyllAction@ arrow, so they produce values of the
3−-- type @HakyllAction () Context@.
4−module Text.Hakyll.CreateContext
5− ( createPage
6− , createCustomPage
7− , createListing
8− , addField
9− , combine
10− , combineWithUrl
11− ) where
12−
13−import Prelude hiding (id)
14−
15−import qualified Data.Map as M
16−import Control.Arrow (second, arr, (&&&), (***))
17−import Control.Monad (liftM2)
18−import Control.Applicative ((<$>))
19−import Control.Arrow ((>>>))
20−import Control.Category (id)
21−
22−import Text.Hakyll.Context
23−import Text.Hakyll.HakyllAction
24−import Text.Hakyll.Render
25−import Text.Hakyll.Page
26−import Text.Hakyll.Pandoc
27−import Text.Hakyll.Internal.Cache
28−
29−-- | Create a @Context@ from a page file stored on the disk. This is probably
30−-- the most common way to create a @Context@.
31−createPage :: FilePath -> HakyllAction () Context
32−createPage path = cacheAction "pages" $ readPageAction path >>> renderAction
33−
34−-- | Create a custom page @Context@.
35−--
36−-- The association list given maps keys to values for substitution. Note
37−-- that as value, you can either give a @String@ or a
38−-- @HakyllAction () String@. The latter is preferred for more complex data,
39−-- since it allows dependency checking. A @String@ is obviously more simple
40−-- to use in some cases.
41−--
42−createCustomPage :: FilePath
43− -> [(String, Either String (HakyllAction () String))]
44− -> HakyllAction () Context
45−createCustomPage url association = HakyllAction
46− { actionDependencies = dataDependencies
47− , actionUrl = Left $ return url
48− , actionFunction = \_ -> Context . M.fromList <$> assoc'
49− }
50− where
51− mtuple (a, b) = b >>= \b' -> return (a, b')
52− toHakyllString = second (either return runHakyllAction)
53− assoc' = mapM (mtuple . toHakyllString) $ ("url", Left url) : association
54− dataDependencies = map snd association >>= getDependencies
55− getDependencies (Left _) = []
56− getDependencies (Right x) = actionDependencies x
57−
58−-- | A @createCustomPage@ function specialized in creating listings.
59−--
60−-- This function creates a listing of a certain list of @Context@s. Every
61−-- item in the list is created by applying the given template to every
62−-- renderable. You can also specify additional context to be included in the
63−-- @CustomPage@.
64−createListing :: FilePath -- ^ Destination of the page.
65− -> [FilePath] -- ^ Templates to render items with.
66− -> [HakyllAction () Context] -- ^ Renderables in the list.
67− -> [(String, Either String (HakyllAction () String))]
68− -> HakyllAction () Context
69−createListing url templates renderables additional =
70− createCustomPage url context
71− where
72− context = ("body", Right concatenation) : additional
73− concatenation = renderAndConcat templates renderables
74−
75−-- | Add a field to a 'Context'.
76−--
77−addField :: String -- ^ Key
78− -> Either String (HakyllAction () String) -- ^ Value
79− -> HakyllAction Context Context -- ^ Result
80−addField key value = arr (const ()) &&& id
81− >>> value' *** id
82− >>> arr (uncurry insert)
83− where
84− value' = arr (const ()) >>> either (arr . const) id value
85− insert v = Context . M.insert key v . unContext
86−
87−-- | Combine two @Context@s. The url will always be taken from the first
88−-- @Renderable@. Also, if a `$key` is present in both renderables, the
89−-- value from the first @Context@ will be taken as well.
90−--
91−-- You can see this as a this as a @union@ between two mappings.
92−combine :: HakyllAction () Context -> HakyllAction () Context
93− -> HakyllAction () Context
94−combine x y = HakyllAction
95− { actionDependencies = actionDependencies x ++ actionDependencies y
96− , actionUrl = actionUrl x
97− , actionFunction = \_ ->
98− Context <$> liftM2 (M.union) (unContext <$> runHakyllAction x)
99− (unContext <$> runHakyllAction y)
100− }
101−
102−-- | Combine two @Context@s and set a custom URL. This behaves like @combine@,
103−-- except that for the @url@ field, the given URL is always chosen.
104−combineWithUrl :: FilePath
105− -> HakyllAction () Context
106− -> HakyllAction () Context
107− -> HakyllAction () Context
108−combineWithUrl url x y = combine'
109− { actionUrl = Left $ return url
110− , actionFunction = \_ ->
111− Context . M.insert "url" url . unContext <$> runHakyllAction combine'
112− }
113− where
114− combine' = combine x y
src/Text/Hakyll/Feed.hsdeleted−112
@@ -1,112 +0,0 @@
1−-- | A Module that allows easy rendering of RSS feeds. If you use this module,
2−-- you must make sure you set the `absoluteUrl` field in the main Hakyll
3−-- configuration.
4−--
5−-- Apart from that, the main rendering functions (@renderRss@, @renderAtom@)
6−-- all assume that you pass the list of items so that the most recent entry
7−-- in the feed is the first item in the list.
8−--
9−-- Also note that the @Context@s should have (at least) the following
10−-- fields to produce a correct feed:
11−--
12−-- - @$title@: Title of the item.
13−--
14−-- - @$description@: Description to appear in the feed.
15−--
16−-- - @$url@: URL to the item - this is usually set automatically.
17−--
18−-- Furthermore, the feed will be generated, but will be incorrect (it won't
19−-- validate) if an empty list is passed.
20−--
21−module Text.Hakyll.Feed
22− ( FeedConfiguration (..)
23− , renderRss
24− , renderAtom
25− ) where
26−
27−import Control.Arrow ((>>>), second)
28−import Control.Monad.Reader (liftIO)
29−import Data.Maybe (fromMaybe)
30−import qualified Data.Map as M
31−
32−import Text.Hakyll.Context (Context (..))
33−import Text.Hakyll.CreateContext (createListing)
34−import Text.Hakyll.ContextManipulations (renderDate)
35−import Text.Hakyll.HakyllMonad (Hakyll)
36−import Text.Hakyll.Render (render, renderChain)
37−import Text.Hakyll.HakyllAction
38−
39−import Paths_hakyll
40−
41−-- | This is a data structure to keep the configuration of a feed.
42−data FeedConfiguration = FeedConfiguration
43− { -- | Url of the feed (relative to site root). For example, @rss.xml@.
44− feedUrl :: String
45− , -- | Title of the feed.
46− feedTitle :: String
47− , -- | Description of the feed.
48− feedDescription :: String
49− , -- | Name of the feed author.
50− feedAuthorName :: String
51− }
52−
53−-- | This is an auxiliary function to create a listing that is, in fact, a feed.
54−-- The items should be sorted on date.
55−createFeed :: FeedConfiguration -- ^ Feed configuration.
56− -> [HakyllAction () Context] -- ^ Items to include.
57− -> FilePath -- ^ Feed template.
58− -> FilePath -- ^ Item template.
59− -> HakyllAction () Context
60−createFeed configuration renderables template itemTemplate =
61− listing >>> render template
62− where
63− listing = createListing (feedUrl configuration)
64− [itemTemplate] renderables additional
65−
66− additional = map (second $ Left . ($ configuration))
67− [ ("title", feedTitle)
68− , ("description", feedDescription)
69− , ("authorName", feedAuthorName)
70− ] ++ updated
71−
72− -- Take the first timestamp, which should be the most recent.
73− updated = let action = createHakyllAction $ return . fromMaybe "foo"
74− . M.lookup "timestamp" . unContext
75− toTuple r = ("timestamp", Right $ r >>> action)
76− in map toTuple $ take 1 renderables
77−
78−
79−-- | Abstract function to render any feed.
80−renderFeed :: FeedConfiguration -- ^ Feed configuration.
81− -> [HakyllAction () Context] -- ^ Items to include in the feed.
82− -> FilePath -- ^ Feed template.
83− -> FilePath -- ^ Item template.
84− -> Hakyll ()
85−renderFeed configuration renderables template itemTemplate = do
86− template' <- liftIO $ getDataFileName template
87− itemTemplate' <- liftIO $ getDataFileName itemTemplate
88− let renderFeed' = createFeed configuration renderables
89− template' itemTemplate'
90− renderChain [] renderFeed'
91−
92−-- | Render an RSS feed with a number of items.
93−renderRss :: FeedConfiguration -- ^ Feed configuration.
94− -> [HakyllAction () Context] -- ^ Items to include in the feed.
95− -> Hakyll ()
96−renderRss configuration renderables =
97− renderFeed configuration (map (>>> renderRssDate) renderables)
98− "templates/rss.xml" "templates/rss-item.xml"
99− where
100− renderRssDate = renderDate "timestamp" "%a, %d %b %Y %H:%M:%S UT"
101− "No date found."
102−
103−-- | Render an Atom feed with a number of items.
104−renderAtom :: FeedConfiguration -- ^ Feed configuration.
105− -> [HakyllAction () Context] -- ^ Items to include in the feed.
106− -> Hakyll ()
107−renderAtom configuration renderables =
108− renderFeed configuration (map (>>> renderAtomDate) renderables)
109− "templates/atom.xml" "templates/atom-item.xml"
110− where
111− renderAtomDate = renderDate "timestamp" "%Y-%m-%dT%H:%M:%SZ"
112− "No date found."
src/Text/Hakyll/File.hsdeleted−196
@@ -1,196 +0,0 @@
1−-- | A module containing various function for manipulating and examinating
2−-- files and directories.
3−module Text.Hakyll.File
4− ( toDestination
5− , toCache
6− , toUrl
7− , toRoot
8− , inDirectory
9− , inHakyllDirectory
10− , removeSpaces
11− , makeDirectories
12− , getRecursiveContents
13− , sortByBaseName
14− , havingExtension
15− , directory
16− , isMoreRecent
17− , isFileMoreRecent
18− ) where
19−
20−import System.Directory
21−import Control.Applicative ((<$>))
22−import System.FilePath
23−import System.Time (ClockTime)
24−import Control.Monad
25−import Data.List (isPrefixOf, sortBy)
26−import Data.Ord (comparing)
27−import Control.Monad.Reader (liftIO)
28−
29−import Text.Hakyll.HakyllMonad
30−import Text.Hakyll.Internal.FileType (isRenderableFile)
31−
32−-- | Auxiliary function to remove pathSeparators form the start. We don't deal
33−-- with absolute paths here. We also remove $root from the start.
34−removeLeadingSeparator :: FilePath -> FilePath
35−removeLeadingSeparator [] = []
36−removeLeadingSeparator path
37− | head path' `elem` pathSeparators = drop 1 path'
38− | otherwise = path'
39− where
40− path' = if "$root" `isPrefixOf` path then drop 5 path
41− else path
42−
43−-- | Convert a relative URL to a filepath in the destination
44−-- (default: @_site@).
45−toDestination :: FilePath -> Hakyll FilePath
46−toDestination url = do dir <- askHakyll siteDirectory
47− toFilePath dir url
48−
49−-- | Convert a relative URL to a filepath in the cache
50−-- (default: @_cache@).
51−toCache :: FilePath -> Hakyll FilePath
52−toCache path = do dir <- askHakyll cacheDirectory
53− toFilePath dir path
54−
55−-- | Implementation of toDestination/toCache
56−--
57−toFilePath :: String -- ^ Directory (site or cache)
58− -> String -- ^ URL
59− -> Hakyll FilePath -- ^ Resulting file path
60−toFilePath dir url = do
61− enableIndexUrl' <- askHakyll enableIndexUrl
62− let destination = if enableIndexUrl' && separatorEnd
63− then dir </> noSeparator </> "index.html"
64− else dir </> noSeparator
65− return destination
66− where
67− noSeparator = removeLeadingSeparator url
68− separatorEnd = not (null url) && last url == '/'
69−
70−-- | Get the url for a given page. For most extensions, this would be the path
71−-- itself. It's only for rendered extensions (@.markdown@, @.rst@, @.lhs@ this
72−-- function returns a path with a @.html@ extension instead.
73−toUrl :: FilePath -> Hakyll FilePath
74−toUrl path = do enableIndexUrl' <- askHakyll enableIndexUrl
75− -- If the file does not have a renderable extension, like for
76− -- example favicon.ico, we don't have to change it at all.
77− return $ if not (isRenderableFile path)
78− then path
79− -- If index url's are enabled, we create pick it
80− -- unless the page is an index already.
81− else if enableIndexUrl' && not isIndex
82− then indexUrl
83− else withSimpleHtmlExtension
84− where
85− isIndex = dropExtension (takeFileName path) == "index"
86− withSimpleHtmlExtension = flip addExtension ".html" $ dropExtension path
87− indexUrl = dropExtension path ++ "/"
88−
89−
90−-- | Get the relative url to the site root, for a given (absolute) url
91−toRoot :: FilePath -> FilePath
92−toRoot = emptyException . joinPath . map parent . splitPath
93− . takeDirectory . removeLeadingSeparator
94− where
95− parent = const ".."
96− emptyException [] = "."
97− emptyException x = x
98−
99−-- | Check if a file is in a given directory.
100−--
101−inDirectory :: FilePath -- ^ File path
102− -> FilePath -- ^ Directory
103− -> Bool -- ^ Result
104−inDirectory path dir = case splitDirectories path of
105− [] -> False
106− (x : _) -> x == dir
107−
108−-- | Check if a file is in a Hakyll directory. With a Hakyll directory, we mean
109−-- a directory that should be "ignored" such as the @_site@ or @_cache@
110−-- directory.
111−--
112−-- Example:
113−--
114−-- > inHakyllDirectory "_cache/pages/index.html"
115−--
116−-- Result:
117−--
118−-- > True
119−--
120−inHakyllDirectory :: FilePath -> Hakyll Bool
121−inHakyllDirectory path =
122− or <$> mapM (liftM (inDirectory path) . askHakyll)
123− [siteDirectory, cacheDirectory]
124−
125−-- | Swaps spaces for '-'.
126−removeSpaces :: FilePath -> FilePath
127−removeSpaces = map swap
128− where
129− swap ' ' = '-'
130− swap x = x
131−
132−-- | Given a path to a file, try to make the path writable by making
133−-- all directories on the path.
134−makeDirectories :: FilePath -> Hakyll ()
135−makeDirectories path = liftIO $ createDirectoryIfMissing True dir
136− where
137− dir = takeDirectory path
138−
139−-- | Get all contents of a directory. Note that files starting with a dot (.)
140−-- will be ignored.
141−--
142−getRecursiveContents :: FilePath -> Hakyll [FilePath]
143−getRecursiveContents topdir = do
144− topdirExists <- liftIO $ doesDirectoryExist topdir
145− if topdirExists
146− then do names <- liftIO $ getDirectoryContents topdir
147− let properNames = filter isProper names
148− paths <- forM properNames $ \name -> do
149− let path = topdir </> name
150− isDirectory <- liftIO $ doesDirectoryExist path
151− if isDirectory
152− then getRecursiveContents path
153− else return [normalise path]
154− return (concat paths)
155− else return []
156− where
157− isProper = not . (== '.') . head
158−
159−-- | Sort a list of filenames on the basename.
160−sortByBaseName :: [FilePath] -> [FilePath]
161−sortByBaseName = sortBy compareBaseName
162− where
163− compareBaseName = comparing takeFileName
164−
165−-- | A filter that takes all file names with a given extension. Prefix the
166−-- extension with a dot:
167−--
168−-- > havingExtension ".markdown" [ "index.markdown"
169−-- > , "style.css"
170−-- > ] == ["index.markdown"]
171−havingExtension :: String -> [FilePath] -> [FilePath]
172−havingExtension extension = filter ((==) extension . takeExtension)
173−
174−-- | Perform a Hakyll action on every file in a given directory.
175−directory :: (FilePath -> Hakyll ()) -> FilePath -> Hakyll ()
176−directory action dir = getRecursiveContents dir >>= mapM_ action
177−
178−-- | Check if a timestamp is newer then a number of given files.
179−isMoreRecent :: ClockTime -- ^ The time to check.
180− -> [FilePath] -- ^ Dependencies of the cached file.
181− -> Hakyll Bool
182−isMoreRecent _ [] = return True
183−isMoreRecent timeStamp depends = do
184− dependsModified <- liftIO $ mapM getModificationTime depends
185− return (timeStamp >= maximum dependsModified)
186−
187−-- | Check if a file is newer then a number of given files.
188−isFileMoreRecent :: FilePath -- ^ The cached file.
189− -> [FilePath] -- ^ Dependencies of the cached file.
190− -> Hakyll Bool
191−isFileMoreRecent file depends = do
192− exists <- liftIO $ doesFileExist file
193− if not exists
194− then return False
195− else do timeStamp <- liftIO $ getModificationTime file
196− isMoreRecent timeStamp depends
src/Text/Hakyll/HakyllAction.hsdeleted−98
@@ -1,98 +0,0 @@
1−-- | This is the module which exports @HakyllAction@.
2−module Text.Hakyll.HakyllAction
3− ( HakyllAction (..)
4− , createHakyllAction
5− , createSimpleHakyllAction
6− , createFileHakyllAction
7− , chain
8− , runHakyllAction
9− , runHakyllActionIfNeeded
10− ) where
11−
12−import Control.Arrow
13−import Control.Category
14−import Control.Monad ((<=<), unless)
15−import Prelude hiding ((.), id)
16−
17−import Text.Hakyll.File (toDestination, isFileMoreRecent)
18−import Text.Hakyll.HakyllMonad
19−
20−-- | Type used for rendering computations that carry along dependencies.
21−data HakyllAction a b = HakyllAction
22− { -- | Dependencies of the @HakyllAction@.
23− actionDependencies :: [FilePath]
24− , -- | URL pointing to the result of this @HakyllAction@.
25− actionUrl :: Either (Hakyll FilePath)
26− (Hakyll FilePath -> Hakyll FilePath)
27− , -- | The actual render function.
28− actionFunction :: a -> Hakyll b
29− }
30−
31−-- | Create a @HakyllAction@ from a function.
32−createHakyllAction :: (a -> Hakyll b) -- ^ Function to execute.
33− -> HakyllAction a b
34−createHakyllAction f = id { actionFunction = f }
35−
36−-- | Create a @HakyllAction@ from a simple @Hakyll@ value.
37−createSimpleHakyllAction :: Hakyll b -- ^ Hakyll value to pass on.
38− -> HakyllAction () b
39−createSimpleHakyllAction = createHakyllAction . const
40−
41−-- | Create a @HakyllAction@ that operates on one file.
42−createFileHakyllAction :: FilePath -- ^ File to operate on.
43− -> Hakyll b -- ^ Value to pass on.
44− -> HakyllAction () b -- ^ The resulting action.
45−createFileHakyllAction path action = HakyllAction
46− { actionDependencies = [path]
47− , actionUrl = Left $ return path
48− , actionFunction = const action
49− }
50−
51−-- | Run a @HakyllAction@ now.
52−runHakyllAction :: HakyllAction () a -- ^ Render action to run.
53− -> Hakyll a -- ^ Result of the action.
54−runHakyllAction action = actionFunction action ()
55−
56−-- | Run a @HakyllAction@, but only when it is out-of-date. At this point, the
57−-- @actionUrl@ field must be set.
58−runHakyllActionIfNeeded :: HakyllAction () () -- ^ Action to run.
59− -> Hakyll () -- ^ Empty result.
60−runHakyllActionIfNeeded action = do
61− url <- case actionUrl action of
62− Left u -> u
63− Right _ -> error "No url when checking dependencies."
64− destination <- toDestination url
65− valid <- isFileMoreRecent destination $ actionDependencies action
66− unless valid $ do logHakyll $ "Rendering " ++ destination
67− runHakyllAction action
68−
69−-- | Chain a number of @HakyllAction@ computations.
70−chain :: [HakyllAction a a] -- ^ Actions to chain.
71− -> HakyllAction a a -- ^ Resulting action.
72−chain [] = id
73−chain list = foldl1 (>>>) list
74−
75−instance Category HakyllAction where
76− id = HakyllAction
77− { actionDependencies = []
78− , actionUrl = Right id
79− , actionFunction = return
80− }
81−
82− x . y = HakyllAction
83− { actionDependencies = actionDependencies x ++ actionDependencies y
84− , actionUrl = case actionUrl x of
85− Left ux -> Left ux
86− Right fx -> case actionUrl y of
87− Left uy -> Left (fx uy)
88− Right fy -> Right (fx . fy)
89− , actionFunction = actionFunction x <=< actionFunction y
90− }
91−
92−instance Arrow HakyllAction where
93− arr f = id { actionFunction = return . f }
94−
95− first x = x
96− { actionFunction = \(y, z) -> do y' <- actionFunction x y
97− return (y', z)
98− }
src/Text/Hakyll/HakyllMonad.hsdeleted−99
@@ -1,99 +0,0 @@
1−-- | Module describing the Hakyll monad stack.
2−module Text.Hakyll.HakyllMonad
3− ( HakyllConfiguration (..)
4− , PreviewMode (..)
5− , Hakyll
6− , askHakyll
7− , getAdditionalContext
8− , logHakyll
9− , forkHakyllWait
10− , concurrentHakyll
11− ) where
12−
13−import Control.Monad.Trans (liftIO)
14−import Control.Concurrent.MVar (MVar, putMVar, newEmptyMVar, readMVar)
15−import Control.Monad.Reader (ReaderT, ask, runReaderT)
16−import Control.Monad (liftM, forM, forM_)
17−import qualified Data.Map as M
18−import System.IO (hPutStrLn, stderr)
19−
20−import Text.Pandoc (ParserState, WriterOptions)
21−import Text.Hamlet (HamletSettings)
22−
23−import Text.Hakyll.Context (Context (..))
24−
25−-- | Our custom monad stack.
26−--
27−type Hakyll = ReaderT HakyllConfiguration IO
28−
29−-- | Preview mode.
30−--
31−data PreviewMode = BuildOnRequest
32− | BuildOnInterval
33− deriving (Show, Eq, Ord)
34−
35−-- | Hakyll global configuration type.
36−--
37−data HakyllConfiguration = HakyllConfiguration
38− { -- | Absolute URL of the site.
39− absoluteUrl :: String
40− , -- | An additional context to use when rendering. This additional context
41− -- is used globally.
42− additionalContext :: Context
43− , -- | Directory where the site is placed.
44− siteDirectory :: FilePath
45− , -- | Directory for cache files.
46− cacheDirectory :: FilePath
47− , -- | Enable index links.
48− enableIndexUrl :: Bool
49− , -- | The preview mode used
50− previewMode :: PreviewMode
51− , -- | Pandoc parsing options
52− pandocParserState :: ParserState
53− , -- | Pandoc writer options
54− pandocWriterOptions :: WriterOptions
55− , -- | Hamlet settings (if you use hamlet for templates)
56− hamletSettings :: HamletSettings
57− }
58−
59−-- | Simplified @ask@ function for the Hakyll monad stack.
60−--
61−-- Usage would typically be something like:
62−--
63−-- > doSomething :: a -> b -> Hakyll c
64−-- > doSomething arg1 arg2 = do
65−-- > siteDirectory' <- askHakyll siteDirectory
66−-- > ...
67−--
68−askHakyll :: (HakyllConfiguration -> a) -> Hakyll a
69−askHakyll = flip liftM ask
70−
71−-- | Obtain the globally available, additional context.
72−--
73−getAdditionalContext :: HakyllConfiguration -> Context
74−getAdditionalContext configuration =
75− let (Context c) = additionalContext configuration
76− in Context $ M.insert "absolute" (absoluteUrl configuration) c
77−
78−-- | Write some log information.
79−--
80−logHakyll :: String -> Hakyll ()
81−logHakyll = liftIO . hPutStrLn stderr
82−
83−-- | Perform a concurrent hakyll action. Returns an MVar you can wait on
84−--
85−forkHakyllWait :: Hakyll () -> Hakyll (MVar ())
86−forkHakyllWait action = do
87− mvar <- liftIO newEmptyMVar
88− config <- ask
89− liftIO $ do
90− runReaderT action config
91− putMVar mvar ()
92− return mvar
93−
94−-- | Perform a number of concurrent hakyll actions, and waits for them to finish
95−--
96−concurrentHakyll :: [Hakyll ()] -> Hakyll ()
97−concurrentHakyll actions = do
98− mvars <- forM actions forkHakyllWait
99− forM_ mvars (liftIO . readMVar)
src/Text/Hakyll/Internal/Cache.hsdeleted−53
@@ -1,53 +0,0 @@
1−module Text.Hakyll.Internal.Cache
2− ( storeInCache
3− , getFromCache
4− , isCacheMoreRecent
5− , cacheAction
6− ) where
7−
8−import Control.Monad ((<=<))
9−import Control.Monad.Reader (liftIO)
10−import Data.Binary
11−import System.FilePath ((</>))
12−
13−import Text.Hakyll.File
14−import Text.Hakyll.HakyllMonad (Hakyll)
15−import Text.Hakyll.HakyllAction
16−
17−-- | We can store all datatypes instantiating @Binary@ to the cache. The cache
18−-- directory is specified by the @HakyllConfiguration@, usually @_cache@.
19−storeInCache :: (Binary a) => a -> FilePath -> Hakyll ()
20−storeInCache value path = do
21− cachePath <- toCache path
22− makeDirectories cachePath
23− liftIO $ encodeFile cachePath value
24−
25−-- | Get a value from the cache. The filepath given should not be located in the
26−-- cache. This function performs a timestamp check on the filepath and the
27−-- filepath in the cache, and only returns the cached value when it is still
28−-- up-to-date.
29−getFromCache :: (Binary a) => FilePath -> Hakyll a
30−getFromCache = liftIO . decodeFile <=< toCache
31−
32−-- | Check if a file in the cache is more recent than a number of other files.
33−isCacheMoreRecent :: FilePath -> [FilePath] -> Hakyll Bool
34−isCacheMoreRecent file depends = toCache file >>= flip isFileMoreRecent depends
35−
36−-- | Cache an entire arrow
37−--
38−cacheAction :: Binary a
39− => String
40− -> HakyllAction () a
41− -> HakyllAction () a
42−cacheAction key action = action { actionFunction = const cacheFunction }
43− where
44− cacheFunction = do
45− -- Construct a filename
46− fileName <- fmap (key </>) $ either id (const $ return "unknown")
47− $ actionUrl action
48− -- Check the cache
49− cacheOk <- isCacheMoreRecent fileName $ actionDependencies action
50− if cacheOk then getFromCache fileName
51− else do result <- actionFunction action ()
52− storeInCache result fileName
53− return result
src/Text/Hakyll/Internal/FileType.hsdeleted−49
@@ -1,49 +0,0 @@
1−-- | A module dealing with file extensions and associated file types.
2−module Text.Hakyll.Internal.FileType
3− ( FileType (..)
4− , getFileType
5− , isRenderable
6− , isRenderableFile
7− ) where
8−
9−import System.FilePath (takeExtension)
10−
11−-- | Datatype to represent the different file types Hakyll can deal with.
12−data FileType = Html
13− | LaTeX
14− | LiterateHaskellMarkdown
15− | Markdown
16− | ReStructuredText
17− | Text
18− | UnknownFileType
19− deriving (Eq, Ord, Show, Read)
20−
21−-- | Get the file type for a certain file. The type is determined by extension.
22−getFileType :: FilePath -> FileType
23−getFileType = getFileType' . takeExtension
24− where
25− getFileType' ".htm" = Html
26− getFileType' ".html" = Html
27− getFileType' ".lhs" = LiterateHaskellMarkdown
28− getFileType' ".markdown" = Markdown
29− getFileType' ".md" = Markdown
30− getFileType' ".mdn" = Markdown
31− getFileType' ".mdown" = Markdown
32− getFileType' ".mdwn" = Markdown
33− getFileType' ".mkd" = Markdown
34− getFileType' ".mkdwn" = Markdown
35− getFileType' ".page" = Markdown
36− getFileType' ".rst" = ReStructuredText
37− getFileType' ".tex" = LaTeX
38− getFileType' ".text" = Text
39− getFileType' ".txt" = Text
40− getFileType' _ = UnknownFileType
41−
42−-- | Check if a certain @FileType@ is renderable.
43−isRenderable :: FileType -> Bool
44−isRenderable UnknownFileType = False
45−isRenderable _ = True
46−
47−-- | Check if a certain file is renderable.
48−isRenderableFile :: FilePath -> Bool
49−isRenderableFile = isRenderable . getFileType
src/Text/Hakyll/Internal/Template.hsdeleted−86
@@ -1,86 +0,0 @@
1−module Text.Hakyll.Internal.Template
2− ( Template (..)
3− , fromString
4− , readTemplate
5− , substitute
6− , regularSubstitute
7− , finalSubstitute
8− ) where
9−
10−import Control.Arrow ((>>>))
11−import Control.Applicative ((<$>))
12−import Data.List (isPrefixOf)
13−import Data.Char (isAlphaNum)
14−import Data.Maybe (fromMaybe)
15−import System.FilePath ((</>))
16−import qualified Data.Map as M
17−
18−import Text.Hakyll.Context (Context (..))
19−import Text.Hakyll.HakyllMonad (Hakyll)
20−import Text.Hakyll.HakyllAction
21−import Text.Hakyll.Pandoc
22−import Text.Hakyll.Internal.Cache
23−import Text.Hakyll.Page
24−import Text.Hakyll.ContextManipulations
25−import Text.Hakyll.Internal.Template.Template
26−import Text.Hakyll.Internal.Template.Hamlet
27−
28−-- | Construct a @Template@ from a string.
29−--
30−fromString :: String -> Template
31−fromString = Template . fromString'
32− where
33− fromString' [] = []
34− fromString' string
35− | "$$" `isPrefixOf` string =
36− EscapeCharacter : (fromString' $ drop 2 string)
37− | "$" `isPrefixOf` string =
38− let (key, rest) = span isAlphaNum $ drop 1 string
39− in Identifier key : fromString' rest
40− | otherwise =
41− let (chunk, rest) = break (== '$') string
42− in Chunk chunk : fromString' rest
43−
44−-- | Read a @Template@ from a file. This function might fetch the @Template@
45−-- from the cache, if available.
46−readTemplate :: FilePath -> Hakyll Template
47−readTemplate path = do
48− isCacheMoreRecent' <- isCacheMoreRecent fileName [path]
49− if isCacheMoreRecent'
50− then getFromCache fileName
51− else do
52− template <- if isHamletRTFile path
53− then readHamletTemplate
54− else readDefaultTemplate
55− storeInCache template fileName
56− return template
57− where
58− fileName = "templates" </> path
59− readDefaultTemplate = do
60− body <- runHakyllAction $ readPageAction path
61− >>> renderAction
62− >>> takeBody
63− return $ fromString body
64−
65− readHamletTemplate = fromHamletRT <$> readHamletRT path
66−
67−-- | Substitutes @$identifiers@ in the given @Template@ by values from the given
68−-- "Context". When a key is not found, it is left as it is. You can specify
69−-- the characters used to replace escaped dollars (@$$@) here.
70−substitute :: String -> Template -> Context -> String
71−substitute escaper template context = substitute' =<< unTemplate template
72− where
73− substitute' (Chunk chunk) = chunk
74− substitute' (Identifier key) =
75− fromMaybe ('$' : key) $ M.lookup key $ unContext context
76− substitute' (EscapeCharacter) = escaper
77−
78−-- | @substitute@ for use during a chain. This will leave escaped characters as
79−-- they are.
80−regularSubstitute :: Template -> Context -> String
81−regularSubstitute = substitute "$$"
82−
83−-- | @substitute@ for the end of a chain (just before writing). This renders
84−-- escaped characters.
85−finalSubstitute :: Template -> Context -> String
86−finalSubstitute = substitute "$"
src/Text/Hakyll/Internal/Template/Hamlet.hsdeleted−56
@@ -1,56 +0,0 @@
1−-- | Support for Hamlet templates in Hakyll.
2−--
3−module Text.Hakyll.Internal.Template.Hamlet
4− ( isHamletRTFile
5− , readHamletRT
6− , fromHamletRT
7− ) where
8−
9−import Control.Exception (try)
10−import Control.Monad.Trans (liftIO)
11−import System.FilePath (takeExtension)
12−
13−import Text.Hamlet.RT
14−
15−import Text.Hakyll.Internal.Template.Template
16−import Text.Hakyll.HakyllMonad (Hakyll, askHakyll, hamletSettings, logHakyll)
17−
18−-- | Determine if a file is a hamlet template by extension.
19−--
20−isHamletRTFile :: FilePath -> Bool
21−isHamletRTFile fileName = takeExtension fileName `elem` [".hamlet", ".hml"]
22−
23−-- | Read a 'HamletRT' by file name.
24−--
25−readHamletRT :: FilePath -- ^ Filename of the template
26− -> Hakyll HamletRT -- ^ Resulting hamlet template
27−readHamletRT fileName = do
28− settings <- askHakyll hamletSettings
29− string <- liftIO $ readFile fileName
30− result <- liftIO $ try $ parseHamletRT settings string
31− case result of
32− Left (HamletParseException s) -> error' s
33− Left (HamletUnsupportedDocException d) -> error' $ show d
34− Left (HamletRenderException s) -> error' s
35− Right x -> return x
36− where
37− error' s = do
38− logHakyll $ "Parse of hamlet file " ++ fileName ++ " failed."
39− logHakyll s
40− error "Parse failed."
41−
42−-- | Convert a 'HamletRT' to a 'Template'
43−--
44−fromHamletRT :: HamletRT -- ^ Hamlet runtime template
45− -> Template -- ^ Hakyll template
46−fromHamletRT (HamletRT sd) = Template $ map fromSimpleDoc sd
47− where
48− fromSimpleDoc :: SimpleDoc -> TemplateElement
49− fromSimpleDoc (SDRaw chunk) = Chunk chunk
50− fromSimpleDoc (SDVar [var]) = Identifier var
51− fromSimpleDoc (SDVar _) =
52− error "Hakyll does not support '.' in identifier names when using \
53− \hamlet templates."
54− fromSimpleDoc _ =
55− error "Only simple $key$ identifiers are allowed when using hamlet \
56− \templates."
src/Text/Hakyll/Internal/Template/Template.hsdeleted−34
@@ -1,34 +0,0 @@
1−-- | Module containing the template data structure.
2−--
3−{-# LANGUAGE GeneralizedNewtypeDeriving #-}
4−module Text.Hakyll.Internal.Template.Template
5− ( Template (..)
6− , TemplateElement (..)
7− ) where
8−
9−import Control.Applicative ((<$>))
10−
11−import Data.Binary (Binary, get, getWord8, put, putWord8)
12−
13−-- | Datatype used for template substitutions.
14−--
15−newtype Template = Template { unTemplate :: [TemplateElement] }
16− deriving (Show, Eq, Binary)
17−
18−-- | Elements of a template.
19−--
20−data TemplateElement = Chunk String
21− | Identifier String
22− | EscapeCharacter
23− deriving (Show, Eq)
24−
25−instance Binary TemplateElement where
26− put (Chunk string) = putWord8 0 >> put string
27− put (Identifier key) = putWord8 1 >> put key
28− put (EscapeCharacter) = putWord8 2
29−
30− get = getWord8 >>= \tag ->
31− case tag of 0 -> Chunk <$> get
32− 1 -> Identifier <$> get
33− 2 -> return EscapeCharacter
34− _ -> error "Error reading cached template"
src/Text/Hakyll/Page.hsdeleted−108
@@ -1,108 +0,0 @@
1−-- | A module for dealing with @Page@s. This module is mostly internally used.
2−module Text.Hakyll.Page
3− ( PageSection (..)
4− , readPage
5− , readPageAction
6− ) where
7−
8−import Data.List (isPrefixOf)
9−import Data.Char (isSpace)
10−import Control.Monad.Reader (liftIO)
11−import System.FilePath
12−import Control.Monad.State (State, evalState, get, put)
13−
14−import Text.Hakyll.File
15−import Text.Hakyll.HakyllMonad
16−import Text.Hakyll.HakyllAction
17−import Text.Hakyll.Regex (substituteRegex, matchesRegex)
18−import Text.Hakyll.Util (trim)
19−
20−-- | A page is first parsed into a number of page sections. A page section
21−-- consists of:
22−--
23−-- * A key
24−--
25−-- * A value
26−--
27−-- * A 'Bool' flag, indicating if the value is applicable for rendering
28−--
29−data PageSection = PageSection {unPageSection :: (String, String, Bool)}
30− deriving (Show)
31−
32−-- | Split a page into sections.
33−--
34−splitAtDelimiters :: [String] -> State (Maybe String) [[String]]
35−splitAtDelimiters [] = return []
36−splitAtDelimiters ls@(x:xs) = do
37− delimiter <- get
38− if not (isDelimiter delimiter x)
39− then return [ls]
40− else do let proper = takeWhile (== '-') x
41− (content, rest) = break (isDelimiter $ Just proper) xs
42− put $ Just proper
43− rest' <- splitAtDelimiters rest
44− return $ (x : content) : rest'
45− where
46− isDelimiter old = case old of
47− Nothing -> isPossibleDelimiter
48− (Just d) -> (== d) . takeWhile (== '-')
49−
50−-- | Check if the given string is a metadata delimiter.
51−isPossibleDelimiter :: String -> Bool
52−isPossibleDelimiter = isPrefixOf "---"
53−
54−-- | Read one section of a page.
55−--
56−readSection :: Bool -- ^ If this section is the first section in the page.
57− -> [String] -- ^ Lines in the section.
58− -> [PageSection] -- ^ Key-values extracted.
59−readSection _ [] = []
60−readSection isFirst ls
61− | not isDelimiter' = [body ls]
62− | isNamedDelimiter = readSectionMetaData ls
63− | isFirst = readSimpleMetaData (drop 1 ls)
64− | otherwise = [body (drop 1 ls)]
65− where
66− isDelimiter' = isPossibleDelimiter (head ls)
67− isNamedDelimiter = head ls `matchesRegex` "^----* *[a-zA-Z0-9][a-zA-Z0-9]*"
68− body ls' = PageSection ("body", unlines ls', True)
69−
70− readSimpleMetaData = map readPair . filter (not . all isSpace)
71− readPair = trimPair . break (== ':')
72− trimPair (key, value) = PageSection (trim key, trim (drop 1 value), False)
73−
74− readSectionMetaData [] = []
75− readSectionMetaData (header:value) =
76− let key = substituteRegex "[^a-zA-Z0-9]" "" header
77− in [PageSection (key, unlines value, True)]
78−
79−-- | Read a page from a file. Metadata is supported.
80−--
81−readPage :: FilePath -> Hakyll [PageSection]
82−readPage path = do
83− let sectionFunctions = map readSection $ True : repeat False
84−
85− -- Read file.
86− contents <- liftIO $ readFile path
87− url <- toUrl path
88− let sections = evalState (splitAtDelimiters $ lines contents) Nothing
89− sectionsData = concat $ zipWith ($) sectionFunctions sections
90−
91− -- Note that url, path etc. are listed first, which means can be overwritten
92− -- by section data
93− return $ PageSection ("url", url, False)
94− : PageSection ("path", path, False)
95− : PageSection ("title", takeBaseName path, False)
96− : (category ++ sectionsData)
97− where
98− category = let dirs = splitDirectories $ takeDirectory path
99− in [PageSection ("category", last dirs, False) | not (null dirs)]
100−
101−-- | Read a page from a file. Metadata is supported.
102−--
103−readPageAction :: FilePath -> HakyllAction () [PageSection]
104−readPageAction path = HakyllAction
105− { actionDependencies = [path]
106− , actionUrl = Left $ toUrl path
107− , actionFunction = const $ readPage path
108− }
src/Text/Hakyll/Paginate.hsdeleted−94
@@ -1,94 +0,0 @@
1−-- | Module aimed to paginate web pages.
2−--
3−module Text.Hakyll.Paginate
4− ( PaginateConfiguration (..)
5− , defaultPaginateConfiguration
6− , paginate
7− ) where
8−
9−import Control.Applicative ((<$>))
10−
11−import Text.Hakyll.Context (Context)
12−import Text.Hakyll.CreateContext
13−import Text.Hakyll.HakyllAction
14−import Text.Hakyll.Util (link)
15−
16−-- | A configuration for a pagination.
17−--
18−data PaginateConfiguration = PaginateConfiguration
19− { -- | Label for the link to the previous page.
20− previousLabel :: String
21− , -- | Label for the link to the next page.
22− nextLabel :: String
23− , -- | Label for the link to the first page.
24− firstLabel :: String
25− , -- | Label for the link to the last page.
26− lastLabel :: String
27− }
28−
29−-- | A simple default configuration for pagination.
30−--
31−defaultPaginateConfiguration :: PaginateConfiguration
32−defaultPaginateConfiguration = PaginateConfiguration
33− { previousLabel = "Previous"
34− , nextLabel = "Next"
35− , firstLabel = "First"
36− , lastLabel = "Last"
37− }
38−
39−-- | The most important function for pagination. This function operates on a
40−-- list of @Context@s (the pages), and basically just adds fields to them
41−-- by combining them with a custom page.
42−--
43−-- The following metadata fields will be added:
44−--
45−-- - @$previous@: A link to the previous page.
46−--
47−-- - @$next@: A link to the next page.
48−--
49−-- - @$first@: A link to the first page.
50−--
51−-- - @$last@: A link to the last page.
52−--
53−-- - @$index@: 1-based index of the current page.
54−--
55−-- - @$length@: Total number of pages.
56−--
57−-- When @$previous@ or @$next@ are not available, they will be just a label
58−-- without a link. The same goes for when we are on the first or last page for
59−-- @$first@ and @$last@.
60−--
61−paginate :: PaginateConfiguration
62− -> [HakyllAction () Context]
63− -> [HakyllAction () Context]
64−paginate configuration renderables = paginate' Nothing renderables (1 :: Int)
65− where
66− -- Create a link with a given label, taken from the configuration.
67− linkWithLabel f r = Right $ case actionUrl r of
68− Left l -> createSimpleHakyllAction $
69− link (f configuration) . ("$root/" ++) <$> l
70− Right _ -> error "No link found for pagination."
71−
72− -- The main function that creates combined renderables by recursing over
73− -- the list of items.
74− paginate' _ [] _ = []
75− paginate' maybePrev (x:xs) index =
76− let (previous, first) = case maybePrev of
77− (Just r) -> ( linkWithLabel previousLabel r
78− , linkWithLabel firstLabel (head renderables) )
79− Nothing -> ( Left $ previousLabel configuration
80− , Left $ firstLabel configuration )
81− (next, last') = case xs of
82− (n:_) -> ( linkWithLabel nextLabel n
83− , linkWithLabel lastLabel (last renderables) )
84− [] -> ( Left $ nextLabel configuration
85− , Left $ lastLabel configuration )
86− customPage = createCustomPage ""
87− [ ("previous", previous)
88− , ("next", next)
89− , ("first", first)
90− , ("last", last')
91− , ("index", Left $ show index)
92− , ("length", Left $ show $ length renderables)
93− ]
94− in (x `combine` customPage) : paginate' (Just x) xs (index + 1)
src/Text/Hakyll/Pandoc.hsdeleted−57
@@ -1,57 +0,0 @@
1−-- | Module exporting a pandoc arrow
2−--
3−module Text.Hakyll.Pandoc
4− ( renderAction
5− , renderActionWith
6− ) where
7−
8−import Data.Maybe (fromMaybe)
9−import qualified Data.Map as M
10−import Control.Arrow (second, (>>>), arr, (&&&))
11−
12−import Text.Pandoc
13−
14−import Text.Hakyll.Internal.FileType
15−import Text.Hakyll.Page
16−import Text.Hakyll.HakyllMonad
17−import Text.Hakyll.HakyllAction
18−import Text.Hakyll.Context
19−
20−-- | Get a render function for a given extension.
21−--
22−getRenderFunction :: HakyllAction FileType (String -> String)
23−getRenderFunction = createHakyllAction $ \fileType -> case fileType of
24− Html -> return id
25− Text -> return id
26− UnknownFileType -> return id
27− _ -> do parserState <- askHakyll pandocParserState
28− writerOptions <- askHakyll pandocWriterOptions
29− return $ writeHtmlString writerOptions
30− . readFunction fileType (readOptions parserState fileType)
31− where
32− readFunction ReStructuredText = readRST
33− readFunction LaTeX = readLaTeX
34− readFunction Markdown = readMarkdown
35− readFunction LiterateHaskellMarkdown = readMarkdown
36− readFunction t = error $ "Cannot render " ++ show t
37−
38− readOptions options LiterateHaskellMarkdown = options
39− { stateLiterateHaskell = True }
40− readOptions options _ = options
41−
42−-- | An action that renders the list of page sections to a context using pandoc
43−--
44−renderAction :: HakyllAction [PageSection] Context
45−renderAction = (arr id &&& (getFileType' >>> getRenderFunction))
46− >>> renderActionWith
47− where
48− getFileType' = arr $ getFileType . fromMaybe "unknown" . lookup "path"
49− . map (\(x, y, _) -> (x, y)) . map unPageSection
50−
51−-- | An action to render pages, offering just a little more flexibility
52−--
53−renderActionWith :: HakyllAction ([PageSection], String -> String) Context
54−renderActionWith = createHakyllAction $ \(sections, render') -> return $
55− Context $ M.fromList $ map (renderTriple render' . unPageSection) sections
56− where
57− renderTriple render' (k, v, r) = second (if r then render' else id) (k, v)
src/Text/Hakyll/Regex.hsdeleted−77
@@ -1,77 +0,0 @@
1−-- | A module that exports a simple regex interface. This code is mostly copied
2−-- from the regex-compat package at hackage. I decided to write this module
3−-- because I want to abstract the regex package used.
4−module Text.Hakyll.Regex
5− ( splitRegex
6− , substituteRegex
7− , matchesRegex
8− ) where
9−
10−import Text.Regex.TDFA
11−
12−-- | Match a regular expression against a string, returning more information
13−-- about the match.
14−matchRegexAll :: Regex -> String -> Maybe (String, String, String, [String])
15−matchRegexAll = matchM
16−
17−-- | Replaces every occurance of the given regexp with the replacement string.
18−subRegex :: Regex -- ^ Search pattern
19− -> String -- ^ Input string
20− -> String -- ^ Replacement text
21− -> String -- ^ Output string
22−subRegex _ "" _ = ""
23−subRegex regexp inp replacement =
24− let -- bre matches a backslash then capture either a backslash or some digits
25− bre = makeRegex "\\\\(\\\\|[0-9]+)"
26− lookup' _ [] _ = []
27− lookup' [] _ _ = []
28− lookup' match' repl groups =
29− case matchRegexAll bre repl of
30− Nothing -> repl
31− Just (lead, _, trail, bgroups) ->
32− let newval =
33− if head bgroups == "\\"
34− then "\\"
35− else let index :: Int
36− index = read (head bgroups) - 1
37− in if index == -1
38− then match'
39− else groups !! index
40− in lead ++ newval ++ lookup' match' trail groups
41− in case matchRegexAll regexp inp of
42− Nothing -> inp
43− Just (lead, match', trail, groups) ->
44− lead ++ lookup' match' replacement groups
45− ++ subRegex regexp trail replacement
46−
47−-- | Splits a string based on a regular expression. The regular expression
48−-- should identify one delimiter.
49−splitRegex' :: Regex -> String -> [String]
50−splitRegex' _ [] = []
51−splitRegex' delim strIn = loop strIn where
52− loop str = case matchOnceText delim str of
53− Nothing -> [str]
54− Just (firstline, _, remainder) ->
55− if null remainder
56− then [firstline,""]
57− else firstline : loop remainder
58−
59−-- | Split a list at a certain element.
60−splitRegex :: String -> String -> [String]
61−splitRegex pattern = filter (not . null)
62− . splitRegex' (makeRegex pattern)
63−
64−-- | Substitute a regex. Simplified interface. This function performs a global
65−-- substitution.
66−substituteRegex :: String -- ^ Pattern to replace (regex).
67− -> String -- ^ Replacement string.
68− -> String -- ^ Input string.
69− -> String -- ^ Result.
70−substituteRegex pattern replacement string =
71− subRegex (makeRegex pattern) string replacement
72−
73−-- | Simple regex matching.
74−matchesRegex :: String -- ^ Input string.
75− -> String -- ^ Pattern to match.
76− -> Bool
77−matchesRegex = (=~)
src/Text/Hakyll/Render.hsdeleted−126
@@ -1,126 +0,0 @@
1−-- | Module containing rendering functions. All these functions are used to
2−-- render files to the @_site@ directory.
3−module Text.Hakyll.Render
4− ( render
5− , renderAndConcat
6− , renderChain
7− , static
8− , css
9− , writePage
10− ) where
11−
12−import Control.Arrow ((>>>))
13−import Control.Applicative ((<$>))
14−import Control.Monad.Reader (liftIO)
15−import System.Directory (copyFile)
16−import Data.Maybe (fromMaybe)
17−import qualified Data.Map as M
18−
19−import Text.Hakyll.Context (Context (..))
20−import Text.Hakyll.HakyllMonad (Hakyll, askHakyll, getAdditionalContext)
21−import Text.Hakyll.File
22−import Text.Hakyll.HakyllAction
23−import Text.Hakyll.ContextManipulations
24−import Text.Hakyll.Internal.CompressCss
25−import Text.Hakyll.Internal.Template
26−
27−-- | A pure render function - used internally.
28−pureRender :: Template -- ^ Template to use for rendering.
29− -> Context -- ^ Renderable object to render with given template.
30− -> Context -- ^ The body of the result will contain the render.
31−pureRender template (Context c) =
32− -- Ignore $root when substituting here. We will only replace that in the
33− -- final render (just before writing).
34− let contextIgnoringRoot = Context $ M.insert "root" "$root" c
35− body = regularSubstitute template $ contextIgnoringRoot
36− in Context $ M.insert "body" body c
37−
38−-- | This is the most simple render action. You render a @Context@ with a
39−-- template, and get back the result.
40−render :: FilePath -- ^ Template to use for rendering.
41− -> HakyllAction Context Context -- ^ The render computation.
42−render templatePath = HakyllAction
43− { actionDependencies = [templatePath]
44− , actionUrl = Right id
45− , actionFunction = \context ->
46− flip pureRender context <$> readTemplate templatePath
47− }
48−
49−-- | Render each @Context@ with the given templates, then concatenate the
50−-- result. So, basically this function:
51−--
52−-- - Takes every @Context@.
53−--
54−-- - Renders every @Context@ with all given templates. This is comparable
55−-- with a renderChain action.
56−--
57−-- - Concatenates the result and returns that as a @String@.
58−renderAndConcat :: [FilePath]
59− -> [HakyllAction () Context]
60− -> HakyllAction () String
61−renderAndConcat templatePaths renderables = HakyllAction
62− { actionDependencies = renders >>= actionDependencies
63− , actionUrl = Right id
64− , actionFunction = actionFunction'
65− }
66− where
67− render' = chain (map render templatePaths)
68− renders = map (>>> render') renderables
69−
70− actionFunction' _ = do
71− contexts <- mapM (runHakyllAction . (>>> takeBody)) renders
72− return $ concat contexts
73−
74−-- | Chain a render action for a page with a number of templates. This will
75−-- also write the result to the site destination. This is the preferred way
76−-- to do general rendering.
77−--
78−-- > renderChain [ "templates/notice.html"
79−-- > , "templates/default.html"
80−-- > ] $ createPagePath "warning.html"
81−--
82−-- This code will first render @warning.html@ using @templates/notice.html@,
83−-- and will then render the result with @templates/default.html@.
84−renderChain :: [FilePath]
85− -> HakyllAction () Context
86− -> Hakyll ()
87−renderChain templatePaths initial =
88− runHakyllActionIfNeeded renderChainWith'
89− where
90− renderChainWith' = initial >>> chain' >>> writePage
91− chain' = chain $ map render templatePaths
92−
93−-- | Mark a certain file as static, so it will just be copied when the site is
94−-- generated.
95−static :: FilePath -> Hakyll ()
96−static source = runHakyllActionIfNeeded static'
97− where
98− static' = createFileHakyllAction source $ do
99− destination <- toDestination source
100− makeDirectories destination
101− liftIO $ copyFile source destination
102−
103−-- | Render a css file, compressing it.
104−css :: FilePath -> Hakyll ()
105−css source = runHakyllActionIfNeeded css'
106− where
107− css' = createFileHakyllAction source $ do
108− contents <- liftIO $ readFile source
109− destination <- toDestination source
110− makeDirectories destination
111− liftIO $ writeFile destination (compressCss contents)
112−
113−-- | Write a page to the site destination. Final action after render
114−-- chains and such.
115−writePage :: HakyllAction Context ()
116−writePage = createHakyllAction $ \(Context initialContext) -> do
117− additionalContext' <- unContext <$> askHakyll getAdditionalContext
118− let url = fromMaybe (error "No url defined at write time.")
119− (M.lookup "url" initialContext)
120− body = fromMaybe "" (M.lookup "body" initialContext)
121− let context = additionalContext' `M.union` M.singleton "root" (toRoot url)
122− destination <- toDestination url
123− makeDirectories destination
124− -- Substitute $root here, just before writing.
125− liftIO $ writeFile destination $ finalSubstitute (fromString body)
126− (Context context)
src/Text/Hakyll/Tags.hsdeleted−172
@@ -1,172 +0,0 @@
1−-- | Module containing some specialized functions to deal with tags.
2−-- This Module follows certain conventions. Stick with them.
3−--
4−-- More concrete: all functions in this module assume that the tags are
5−-- located in the @tags@ field, and separated by commas. An example file
6−-- @foo.markdown@ could look like:
7−--
8−-- > ---
9−-- > author: Philip K. Dick
10−-- > title: Do androids dream of electric sheep?
11−-- > tags: future, science fiction, humanoid
12−-- > ---
13−-- > The novel is set in a post-apocalyptic near future, where the Earth and
14−-- > its populations have been damaged greatly by Nuclear...
15−--
16−-- All the following functions would work with such a format. In addition to
17−-- tags, Hakyll also supports categories. The convention when using categories
18−-- is to place pages in subdirectories.
19−--
20−-- An example, the page @posts\/coding\/2010-01-28-hakyll-categories.markdown@
21−-- Tags or categories are read using the @readTagMap@ and @readCategoryMap@
22−-- functions. Because categories are implemented using tags - categories can
23−-- be seen as tags, with the restriction that a page can only have one
24−-- category - all functions for tags also work with categories.
25−--
26−-- When reading a @TagMap@ (which is also used for category maps) using the
27−-- @readTagMap@ or @readCategoryMap@ function, you also have to give a unique
28−-- identifier to it. This identifier is simply for caching reasons, so Hakyll
29−-- can tell different maps apart; it has no other use.
30−--
31−module Text.Hakyll.Tags
32− ( TagMap
33− , readTagMap
34− , readCategoryMap
35− , withTagMap
36− , renderTagCloud
37− , renderTagLinks
38− ) where
39−
40−import qualified Data.Map as M
41−import Data.List (intercalate)
42−import Data.Maybe (fromMaybe, maybeToList)
43−import Control.Arrow (second, (>>>))
44−import Control.Applicative ((<$>))
45−import System.FilePath
46−
47−import Text.Blaze.Renderer.String (renderHtml)
48−import Text.Blaze.Html5 ((!), string, stringValue)
49−import qualified Text.Blaze.Html5 as H
50−import qualified Text.Blaze.Html5.Attributes as A
51−
52−import Text.Hakyll.Context (Context (..))
53−import Text.Hakyll.ContextManipulations (changeValue)
54−import Text.Hakyll.CreateContext (createPage)
55−import Text.Hakyll.HakyllMonad (Hakyll)
56−import Text.Hakyll.Regex
57−import Text.Hakyll.HakyllAction
58−import Text.Hakyll.Util
59−import Text.Hakyll.Internal.Cache
60−
61−-- | Type for a tag map.
62−--
63−-- This is a map associating tags or categories to the appropriate pages
64−-- using that tag or category. In the case of categories, each path will only
65−-- appear under one category - this is not the case with tags.
66−type TagMap = M.Map String [HakyllAction () Context]
67−
68−-- | Read a tag map. This is a internally used function that can be used for
69−-- tags as well as for categories.
70−readMap :: (Context -> [String]) -- ^ Function to get tags from a context.
71− -> String -- ^ Unique identifier for the tagmap.
72− -> [FilePath]
73− -> HakyllAction () TagMap
74−readMap getTagsFunction identifier paths = HakyllAction
75− { actionDependencies = paths
76− , actionUrl = Right id
77− , actionFunction = actionFunction'
78− }
79− where
80− fileName = "tagmaps" </> identifier
81−
82− actionFunction' _ = do
83− isCacheMoreRecent' <- isCacheMoreRecent fileName paths
84− assocMap <- if isCacheMoreRecent'
85− then M.fromAscList <$> getFromCache fileName
86− else do assocMap' <- readTagMap'
87− storeInCache (M.toAscList assocMap') fileName
88− return assocMap'
89− return $ M.map (map createPage) assocMap
90−
91− -- TODO: preserve order
92− readTagMap' :: Hakyll (M.Map String [FilePath])
93− readTagMap' = do
94− pairs' <- concat <$> mapM pairs paths
95− return $ M.fromListWith (flip (++)) pairs'
96−
97− -- | Read a page, and return an association list where every tag is
98− -- associated with some paths. Of course, this will always be just one
99− -- @FilePath@ here.
100− pairs :: FilePath -> Hakyll [(String, [FilePath])]
101− pairs path = do
102− context <- runHakyllAction $ createPage path
103− let tags = getTagsFunction context
104− return $ map (\tag -> (tag, [path])) tags
105−
106−-- | Read a @TagMap@, using the @tags@ metadata field.
107−readTagMap :: String -- ^ Unique identifier for the map.
108− -> [FilePath] -- ^ Paths to get tags from.
109− -> HakyllAction () TagMap
110−readTagMap = readMap getTagsFunction
111− where
112− getTagsFunction = map trim . splitRegex ","
113− . fromMaybe [] . M.lookup "tags" . unContext
114−
115−-- | Read a @TagMap@, using the subdirectories the pages are placed in.
116−readCategoryMap :: String -- ^ Unique identifier for the map.
117− -> [FilePath] -- ^ Paths to get tags from.
118− -> HakyllAction () TagMap
119−readCategoryMap = readMap $ maybeToList . M.lookup "category" . unContext
120−
121−-- | Perform a @Hakyll@ action on every item in the tag
122−--
123−withTagMap :: HakyllAction () TagMap
124− -> (String -> [HakyllAction () Context] -> Hakyll ())
125− -> Hakyll ()
126−withTagMap tagMap function = runHakyllAction (tagMap >>> action)
127− where
128− action = createHakyllAction (mapM_ (uncurry function) . M.toList)
129−
130−-- | Render a tag cloud.
131−renderTagCloud :: (String -> String) -- ^ Function to produce an url for a tag.
132− -> Float -- ^ Smallest font size, in percent.
133− -> Float -- ^ Biggest font size, in percent.
134− -> HakyllAction TagMap String
135−renderTagCloud urlFunction minSize maxSize = createHakyllAction renderTagCloud'
136− where
137− renderTagCloud' tagMap =
138− return $ intercalate " " $ map (renderTag tagMap) (tagCount tagMap)
139−
140− renderTag tagMap (tag, count) = renderHtml $
141− H.a ! A.style (stringValue $ "font-size: " ++ sizeTag tagMap count)
142− ! A.href (stringValue $ urlFunction tag)
143− $ string tag
144−
145− sizeTag tagMap count = show (size' :: Int) ++ "%"
146− where
147− size' = floor $ minSize + relative tagMap count * (maxSize - minSize)
148−
149− minCount = minimum . map snd . tagCount
150− maxCount = maximum . map snd . tagCount
151− relative tagMap count = (count - minCount tagMap) /
152− (1 + maxCount tagMap - minCount tagMap)
153−
154− tagCount = map (second $ fromIntegral . length) . M.toList
155−
156−-- | Render all tags to links.
157−--
158−-- On your site, it is nice if you can display the tags on a page, but
159−-- naturally, most people would expect these are clickable.
160−--
161−-- So, this function takes a function to produce an url for a given tag, and
162−-- applies it on all tags.
163−--
164−-- Note that it is your own responsibility to ensure a page with such an url
165−-- exists.
166−renderTagLinks :: (String -> String) -- ^ Function to produce an url for a tag.
167− -> HakyllAction Context Context
168−renderTagLinks urlFunction = changeValue "tags" renderTagLinks'
169− where
170− renderTagLinks' = intercalate ", "
171− . map ((\t -> link t $ urlFunction t) . trim)
172− . splitRegex ","
src/Text/Hakyll/Util.hsdeleted−34
@@ -1,34 +0,0 @@
1−-- | Miscellaneous text manipulation functions.
2−module Text.Hakyll.Util
3− ( trim
4− , stripHtml
5− , link
6− ) where
7−
8−import Data.Char (isSpace)
9−
10−import Text.Blaze.Html5 ((!), string, stringValue, a)
11−import Text.Blaze.Html5.Attributes (href)
12−import Text.Blaze.Renderer.String (renderHtml)
13−
14−-- | Trim a string (drop spaces, tabs and newlines at both sides).
15−trim :: String -> String
16−trim = reverse . trim' . reverse . trim'
17− where
18− trim' = dropWhile isSpace
19−
20−-- | Strip html tags from the given string.
21−stripHtml :: String -> String
22−stripHtml [] = []
23−stripHtml str = let (beforeTag, rest) = break (== '<') str
24− (_, afterTag) = break (== '>') rest
25− in beforeTag ++ stripHtml (drop 1 afterTag)
26−
27−-- | Make a HTML link.
28−--
29−-- > link "foo" "bar.html" == "<a href='bar.html'>foo</a>"
30−link :: String -- ^ Link text.
31− -> String -- ^ Link destination.
32− -> String
33−link text destination = renderHtml $ a ! href (stringValue destination)
34− $ string text
tests/CompressCss.hsdeleted−42
@@ -1,42 +0,0 @@
1−-- | Module testing @Text.Hakyll.Internal.CompressCss@.
2−module CompressCss
3− ( compressCssGroup
4− ) where
5−
6−import qualified Data.Map as M
7−
8−import Data.Binary
9−import Test.Framework (testGroup)
10−import Test.Framework.Providers.HUnit
11−import Test.Framework.Providers.QuickCheck2
12−import Test.HUnit
13−
14−import Text.Hakyll.Internal.CompressCss
15−
16−-- CompressCss test group.
17−compressCssGroup = testGroup "CompressCss"
18− [ testProperty "prop_compressCss_length" prop_compressCss_length
19− , testCase "test_compressCss_1" test_compressCss_1
20− , testCase "test_compressCss_2" test_compressCss_2
21− , testCase "test_compressCss_3" test_compressCss_3
22− , testCase "test_compressCss_4" test_compressCss_4
23− ]
24−
25−-- | Css compression should always decrease the text length.
26−prop_compressCss_length str = length str >= length (compressCss str)
27−
28−-- | compressCss test case 1.
29−test_compressCss_1 = compressCss "a { \n color : red; }" @?= "a{color:red}"
30−
31−-- | compressCss test case 2.
32−test_compressCss_2 = compressCss "img {border :none;;;; }"
33− @?= "img{border:none}"
34−
35−-- | compressCss test case 3.
36−test_compressCss_3 =
37− compressCss "p {font-size : 90%;} h1 {color :white;;; }"
38− @?= "p{font-size:90%}h1{color:white}"
39−
40−-- | compressCss test case 4.
41−test_compressCss_4 = compressCss "a { /* /* red is pretty cool */ color: red; }"
42− @?= "a{color:red}"
tests/File.hsdeleted−62
@@ -1,62 +0,0 @@
1−module File
2− ( fileGroup
3− ) where
4−
5−import qualified Data.Map as M
6−
7−import Control.Applicative ((<$>))
8−import Data.Binary
9−import Test.Framework (testGroup)
10−import Test.Framework.Providers.HUnit
11−import Test.Framework.Providers.QuickCheck2
12−import Test.HUnit
13−import Test.QuickCheck
14−
15−import Text.Hakyll (runDefaultHakyll)
16−import Text.Hakyll.File
17−
18−-- File test group.
19−fileGroup = testGroup "File"
20− [ testCase "test_toRoot_1" test_toRoot_1
21− , testCase "test_toRoot_2" test_toRoot_2
22− , testCase "test_toRoot_3" test_toRoot_3
23− , testCase "test_inHakyllDirectory_1" test_inHakyllDirectory_1
24− , testCase "test_inHakyllDirectory_2" test_inHakyllDirectory_2
25− , testCase "test_inHakyllDirectory_3" test_inHakyllDirectory_3
26− , testCase "test_inHakyllDirectory_4" test_inHakyllDirectory_4
27− , testCase "test_removeSpaces_1" test_removeSpaces_1
28− , testCase "test_removeSpaces_2" test_removeSpaces_2
29− , testCase "test_havingExtension_1" test_havingExtension_1
30− , testCase "test_havingExtension_2" test_havingExtension_2
31− ]
32−
33−
34−-- toRoot test cases
35−test_toRoot_1 = toRoot "/posts/foo.html" @?= ".."
36−test_toRoot_2 = toRoot "posts/foo.html" @?= ".."
37−test_toRoot_3 = toRoot "foo.html" @?= "."
38−
39−-- inHakyllDirectory test cases
40−test_inHakyllDirectory_1 =
41− (runDefaultHakyll $ inHakyllDirectory "_site/foo.html")
42− @? "test_inHakyllDirectory_1"
43−test_inHakyllDirectory_2 =
44− (not <$> (runDefaultHakyll $ inHakyllDirectory "posts/foo.html"))
45− @? "test_inHakyllDirectory_2"
46−test_inHakyllDirectory_3 =
47− (not <$> (runDefaultHakyll $ inHakyllDirectory "index.html"))
48− @? "test_inHakyllDirectory_3"
49−test_inHakyllDirectory_4 =
50− (runDefaultHakyll $ inHakyllDirectory "_cache/index.html")
51− @? "test_inHakyllDirectory_4"
52−
53−-- removeSpaces test cases
54−test_removeSpaces_1 = removeSpaces "$root/tags/random crap.html"
55− @?= "$root/tags/random-crap.html"
56−test_removeSpaces_2 = removeSpaces "another simple example.zip"
57− @?= "another-simple-example.zip"
58−
59−-- Having extension test cases
60−test_havingExtension_1 = havingExtension ".foo" ["file.bar", "file.txt"] @?= []
61−test_havingExtension_2 = havingExtension ".foo" ["file.foo", "file.txt"]
62− @?= ["file.foo"]
tests/Hakyll/Core/DirectedGraph/Tests.hsnew file+36
@@ -0,0 +1,36 @@
1+module Hakyll.Core.DirectedGraph.Tests
2+ ( tests
3+ ) where
4+
5+import Data.Set (Set)
6+import qualified Data.Set as S
7+
8+import Test.Framework
9+import Test.Framework.Providers.HUnit
10+import Test.HUnit hiding (Test)
11+
12+import Hakyll.Core.DirectedGraph
13+import Hakyll.Core.DirectedGraph.DependencySolver
14+
15+tests :: [Test]
16+tests =
17+ [ testCase "solveDependencies [1]" solveDependencies1
18+ ]
19+
20+node :: Ord a => a -> [a] -> (a, Set a)
21+node t n = (t, S.fromList n)
22+
23+testGraph01 :: DirectedGraph Int
24+testGraph01 = fromList
25+ [ node 8 [2, 4, 6]
26+ , node 2 [4, 3]
27+ , node 4 [3]
28+ , node 6 [4]
29+ , node 3 []
30+ ]
31+
32+solveDependencies1 :: Assertion
33+solveDependencies1 = result == [3, 4, 2, 6, 8] || result == [3, 4, 2, 6, 8]
34+ @? "solveDependencies1"
35+ where
36+ result = solveDependencies testGraph01
tests/Hakyll/Core/Identifier/Tests.hsnew file+22
@@ -0,0 +1,22 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Hakyll.Core.Identifier.Tests
3+ ( tests
4+ ) where
5+
6+import Test.Framework
7+import Test.HUnit hiding (Test)
8+
9+import Hakyll.Core.Identifier.Pattern
10+import TestSuite.Util
11+
12+tests :: [Test]
13+tests = fromAssertions "match"
14+ [ Just ["bar"] @=? match "foo/**" "foo/bar"
15+ , Just ["foo/bar"] @=? match "**" "foo/bar"
16+ , Nothing @=? match "*" "foo/bar"
17+ , Just [] @=? match "foo" "foo"
18+ , Just ["foo"] @=? match "*/bar" "foo/bar"
19+ , Just ["foo/bar"] @=? match "**/qux" "foo/bar/qux"
20+ , Just ["foo/bar", "qux"] @=? match "**/*" "foo/bar/qux"
21+ , Just ["foo", "bar/qux"] @=? match "*/**" "foo/bar/qux"
22+ ]
tests/Hakyll/Core/Routes/Tests.hsnew file+24
@@ -0,0 +1,24 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Hakyll.Core.Routes.Tests
3+ ( tests
4+ ) where
5+
6+import Test.Framework
7+import Test.HUnit hiding (Test)
8+
9+import Hakyll.Core.Routes
10+import TestSuite.Util
11+
12+tests :: [Test]
13+tests = fromAssertions "runRoutes"
14+ [ Just "foo.html" @=? runRoutes (setExtension "html") "foo"
15+ , Just "foo.html" @=? runRoutes (setExtension ".html") "foo"
16+ , Just "foo.html" @=? runRoutes (setExtension "html") "foo.markdown"
17+ , Just "foo.html" @=? runRoutes (setExtension ".html") "foo.markdown"
18+
19+ , Just "tags/bar.xml" @=?
20+ runRoutes (gsubRoute "rss/" (const "")) "tags/rss/bar.xml"
21+ , Just "tags/bar.xml" @=?
22+ runRoutes (gsubRoute "rss/" (const "") `composeRoutes`
23+ setExtension "xml") "tags/rss/bar"
24+ ]
tests/Hakyll/Web/Page/Tests.hsnew file+32
@@ -0,0 +1,32 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Hakyll.Web.Page.Tests
3+ ( tests
4+ ) where
5+
6+import Test.Framework
7+import Test.HUnit hiding (Test)
8+
9+import qualified Data.Map as M
10+
11+import Hakyll.Web.Page
12+import Hakyll.Web.Page.Read
13+import TestSuite.Util
14+
15+tests :: [Test]
16+tests = fromAssertions "readPage"
17+ [ Page (M.singleton "foo" "bar") "body\n" @=? readPage
18+ "--- \n\
19+ \foo: bar \n\
20+ \--- \n\
21+ \body"
22+
23+ , Page M.empty "line one\nlijn twee\n" @=? readPage
24+ "line one\n\
25+ \lijn twee"
26+
27+ , Page (M.fromList [("field1", "unos"), ("veld02", "deux")]) "" @=? readPage
28+ "---\n\
29+ \veld02: deux\n\
30+ \field1: unos\n\
31+ \---"
32+ ]
tests/Hakyll/Web/RelativizeUrls/Tests.hsnew file+20
@@ -0,0 +1,20 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Hakyll.Web.RelativizeUrls.Tests
3+ ( tests
4+ ) where
5+
6+import Test.Framework
7+import Test.HUnit hiding (Test)
8+
9+import Hakyll.Web.RelativizeUrls
10+import TestSuite.Util
11+
12+tests :: [Test]
13+tests = fromAssertions "relativizeUrls"
14+ [ "<a href=\"../foo\">bar</a>" @=?
15+ relativizeUrls ".." "<a href=\"/foo\">bar</a>"
16+ , "<img src=\"../../images/lolcat.png\"></img>" @=?
17+ relativizeUrls "../.." "<img src=\"/images/lolcat.png\" />"
18+ , "<a href=\"http://haskell.org\">Haskell</a>" @=?
19+ relativizeUrls "../.." "<a href=\"http://haskell.org\">Haskell</a>"
20+ ]
tests/Hakyll/Web/Template/Tests.hsnew file+44
@@ -0,0 +1,44 @@
1+{-# LANGUAGE OverloadedStrings #-}
2+module Hakyll.Web.Template.Tests
3+ ( tests
4+ ) where
5+
6+import Test.Framework
7+import Test.HUnit hiding (Test)
8+
9+import qualified Data.Map as M
10+
11+import Hakyll.Web.Page
12+import Hakyll.Web.Template
13+import Hakyll.Web.Template.Read
14+import TestSuite.Util
15+
16+tests :: [Test]
17+tests = fromAssertions "applyTemplate"
18+ -- Hakyll templates
19+ [ applyTemplateAssertion readTemplate
20+ "bar" "$foo$" [("foo", "bar")]
21+
22+ , applyTemplateAssertion readTemplate
23+ "$ barqux" "$$ $foo$$bar$" [("foo", "bar"), ("bar", "qux")]
24+
25+ -- Hamlet templates
26+ , applyTemplateAssertion readHamletTemplate
27+ "<head><title>notice</title></head><body>A paragraph</body>"
28+ "<head\n\
29+ \ <title>#{title}\n\
30+ \<body\n\
31+ \ A paragraph\n"
32+ [("title", "notice")]
33+ ]
34+
35+-- | Utility function to create quick template tests
36+--
37+applyTemplateAssertion :: (String -> Template) -- ^ Template parser
38+ -> String -- ^ Expected
39+ -> String -- ^ Template
40+ -> [(String, String)] -- ^ Page
41+ -> Assertion -- ^ Resulting assertion
42+applyTemplateAssertion parser expected template page =
43+ expected @=? pageBody (applyTemplate (parser template)
44+ (fromMap $ M.fromList page))
tests/Main.hsdeleted−22
@@ -1,22 +0,0 @@
1−module Main where
2−
3−import Test.Framework (defaultMain, testGroup)
4−import Test.Framework.Providers.HUnit
5−import Test.Framework.Providers.QuickCheck2
6−
7−import CompressCss
8−import File
9−import Page
10−import Regex
11−import Template
12−import Util
13−
14−-- | Run all tests.
15−main :: IO ()
16−main = defaultMain [ compressCssGroup
17− , fileGroup
18− , pageGroup
19− , regexGroup
20− , templateGroup
21− , utilGroup
22− ]
tests/Page.hsdeleted−92
@@ -1,92 +0,0 @@
1−module Page
2− ( pageGroup
3− ) where
4−
5−import qualified Data.Map as M
6−
7−import Control.Monad.Reader (runReaderT)
8−import Data.Binary
9−import Test.Framework (testGroup)
10−import Test.Framework.Providers.HUnit
11−import Test.Framework.Providers.QuickCheck2
12−import Test.HUnit
13−import System.Directory (getTemporaryDirectory, removeFile)
14−import System.FilePath ((</>))
15−
16−import Text.Hakyll.Page
17−import Text.Hakyll.Pandoc
18−import Text.Hakyll.Context
19−import Text.Hakyll.HakyllAction
20−import Text.Hakyll
21−
22−-- Page test group.
23−pageGroup = testGroup "Page"
24− [ testCase "test_readPage_1" test_readPage_1
25− , testCase "test_readPage_2" test_readPage_2
26− , testCase "test_readPage_3" test_readPage_3
27− , testCase "test_readPage_4" test_readPage_4
28− ]
29−
30−-- | An abstract function to test page reading.
31−test_readPage :: FilePath -- ^ Filename to give to the temporary file.
32− -> String -- ^ Content to put in the file.
33− -> (Context -> Bool) -- ^ Assertion to run on the result Context.
34− -> IO Bool -- ^ Result of the assertion.
35−test_readPage fileName content assertion = do
36− temporaryDir <- getTemporaryDirectory
37− let temporaryFile = temporaryDir </> fileName
38− writeFile temporaryFile content
39− page <- runDefaultHakyll (runHakyllAction $ createPage temporaryFile)
40− removeFile temporaryFile
41− return $ assertion page
42−
43−-- | readPage test case 1.
44−test_readPage_1 = test_readPage fileName content assertion @? "test_readPage_1"
45− where
46− fileName = "test_readPage_1.markdown"
47− content = unlines [ "---"
48− , "author: Eric Cartman"
49− , "---"
50− , "This is a simple test."
51− ]
52− assertion page = M.lookup "author" (unContext page) == Just "Eric Cartman"
53−
54−-- | readPage test case 2.
55−test_readPage_2 = test_readPage fileName content assertion @? "test_readPage_2"
56− where
57− fileName = "test_readPage_2.txt"
58− content = unlines [ "--- someSection"
59− , "This is a section."
60− , "---"
61− , "This is the body."
62− ]
63− assertion page =
64− let m = unContext page
65− in M.lookup "someSection" m == Just "This is a section.\n"
66− && M.lookup "body" m == Just "This is the body.\n"
67−
68−-- | readPage test case 3.
69−test_readPage_3 = test_readPage fileName content assertion @? "test_readPage_3"
70− where
71− fileName = "test_readPage_3.txt"
72− content = unlines [ "No metadata here, sorry."
73− ]
74− assertion page =
75− M.lookup "body" (unContext page) == Just "No metadata here, sorry.\n"
76−
77−-- | readPage test case 4.
78−test_readPage_4 = test_readPage fileName content assertion @? "test_readPage_4"
79− where
80− fileName = "test_readPage_4.txt"
81− content = unlines [ "--- section"
82− , "This is a section."
83− , "---"
84− , "Header"
85− , "------"
86− , "The header is not a separate section."
87− ]
88− assertion page = M.lookup "body" (unContext page) == Just body
89− body = unlines [ "Header"
90− , "------"
91− , "The header is not a separate section."
92− ]
tests/Regex.hsdeleted−24
@@ -1,24 +0,0 @@
1−module Regex
2− ( regexGroup
3− ) where
4−
5−import qualified Data.Map as M
6−
7−import Data.Binary
8−import Test.Framework (testGroup)
9−import Test.Framework.Providers.HUnit
10−import Test.Framework.Providers.QuickCheck2
11−import Test.HUnit
12−import Test.QuickCheck
13−
14−import Text.Hakyll.Regex
15−
16−-- Regex test group.
17−regexGroup = testGroup "Regex"
18− [ testCase "test_splitRegex_1" test_splitRegex_1
19− , testCase "test_splitRegex_2" test_splitRegex_2
20− ]
21−
22−-- Split Regex test cases.
23−test_splitRegex_1 = splitRegex "," "1,2,3" @?= ["1", "2", "3"]
24−test_splitRegex_2 = splitRegex "," ",1,2," @?= ["1", "2"]
tests/Template.hsdeleted−70
@@ -1,70 +0,0 @@
1−module Template
2− ( templateGroup
3− ) where
4−
5−import qualified Data.Map as M
6−import Control.Applicative ((<$>))
7−import Control.Monad (replicateM)
8−import Data.Monoid (mempty)
9−
10−import Data.Binary
11−import Test.Framework (testGroup)
12−import Test.Framework.Providers.HUnit
13−import Test.Framework.Providers.QuickCheck2
14−import Test.HUnit
15−import Test.QuickCheck
16−
17−import Text.Hakyll.Context (Context (..))
18−import Text.Hakyll.Internal.Template
19−import Text.Hakyll.Internal.Template.Template
20−
21−-- Template test group.
22−templateGroup = testGroup "Template"
23− [ testProperty "prop_template_encode_id" prop_template_encode_id
24− , testProperty "prop_substitute_id" prop_substitute_id
25− , testCase "test_substitute_1" test_substitute_1
26− , testCase "test_substitute_2" test_substitute_2
27− ]
28−
29−-- | Generate arbitrary templates from a given length.
30−--
31−instance Arbitrary TemplateElement where
32− arbitrary = oneof
33− -- Random chunk
34− [ Chunk <$> do
35− string <- arbitrary
36− let sanitized = filter (/= '$') string
37− return $ if null sanitized then "foo" else sanitized
38− -- Random identifier
39− , fmap Identifier $
40− choose (5, 10) >>= flip replicateM (choose ('a', 'z'))
41− -- Escape character
42− , return EscapeCharacter
43− ]
44−
45−-- | Make @Template@ testable.
46−instance Arbitrary Template where
47− arbitrary = Template <$> arbitrary
48− shrink = map Template . shrink . unTemplate
49−
50−-- Test encoding/decoding of templates.
51−prop_template_encode_id :: Template -> Bool
52−prop_template_encode_id template = decode (encode template) == template
53−
54−-- Check we get the same sting with empty substitutions.
55−prop_substitute_id string =
56− regularSubstitute (fromString string) mempty == string
57−
58−-- substitute test case 1.
59−test_substitute_1 =
60− finalSubstitute template context @?= "Banana costs $4."
61− where
62− template = fromString "$product costs $$$price."
63− context = Context $ M.fromList [("product", "Banana"), ("price", "4")]
64−
65−-- substitute test case 2.
66−test_substitute_2 =
67− regularSubstitute template context @?= "$$root is a special key."
68− where
69− template = fromString "$$root is a special $thing."
70− context = Context $ M.fromList [("root", "foo"), ("thing", "key")]
tests/TestSuite.hsnew file+26
@@ -0,0 +1,26 @@
1+module TestSuite where
2+
3+import Test.Framework (defaultMain, testGroup)
4+
5+import qualified Hakyll.Core.DirectedGraph.Tests
6+import qualified Hakyll.Core.Identifier.Tests
7+import qualified Hakyll.Core.Routes.Tests
8+import qualified Hakyll.Web.Page.Tests
9+import qualified Hakyll.Web.RelativizeUrls.Tests
10+import qualified Hakyll.Web.Template.Tests
11+
12+main :: IO ()
13+main = defaultMain
14+ [ testGroup "Hakyll.Core.DirectedGraph.Tests"
15+ Hakyll.Core.DirectedGraph.Tests.tests
16+ , testGroup "Hakyll.Core.Identifier.Tests"
17+ Hakyll.Core.Identifier.Tests.tests
18+ , testGroup "Hakyll.Core.Routes.Tests"
19+ Hakyll.Core.Routes.Tests.tests
20+ , testGroup "Hakyll.Web.Page.Tests"
21+ Hakyll.Web.Page.Tests.tests
22+ , testGroup "Hakyll.Web.RelativizeUrls.Tests"
23+ Hakyll.Web.RelativizeUrls.Tests.tests
24+ , testGroup "Hakyll.Web.Template.Tests"
25+ Hakyll.Web.Template.Tests.tests
26+ ]
tests/TestSuite/Util.hsnew file+16
@@ -0,0 +1,16 @@
1+-- | Test utilities
2+--
3+module TestSuite.Util
4+ ( fromAssertions
5+ ) where
6+
7+import Test.Framework
8+import Test.Framework.Providers.HUnit
9+import Test.HUnit hiding (Test)
10+
11+fromAssertions :: String -- ^ Name
12+ -> [Assertion] -- ^ Cases
13+ -> [Test] -- ^ Result tests
14+fromAssertions name = zipWith testCase names
15+ where
16+ names = map (\n -> name ++ " [" ++ show n ++ "]") [1 :: Int ..]
tests/Util.hsdeleted−54
@@ -1,54 +0,0 @@
1−module Util
2− ( utilGroup
3− ) where
4−
5−import Data.Char
6−
7−import Test.QuickCheck
8−import Test.Framework (testGroup)
9−import Test.Framework.Providers.HUnit
10−import Test.Framework.Providers.QuickCheck2
11−import Test.HUnit
12−
13−import Text.Hakyll.Util
14−
15−-- Util test group.
16−utilGroup = testGroup "Util"
17− [ testProperty "prop_trim_length" prop_trim_length
18− , testProperty "prop_trim_id" prop_trim_id
19− , testProperty "prop_stripHtml_length" prop_stripHtml_length
20− , testProperty "prop_stripHtml_id" prop_stripHtml_id
21− , testCase "test_stripHtml_1" test_stripHtml_1
22− , testCase "test_stripHtml_2" test_stripHtml_2
23− , testCase "test_stripHtml_3" test_stripHtml_3
24− , testCase "test_link_1" test_link_1
25− , testCase "test_link_2" test_link_2
26− ]
27−
28−-- Test that a string always becomes shorter when trimmed.
29−prop_trim_length str = length str >= length (trim str)
30−
31−-- Check that a string which does not start or end with a space is not trimmed.
32−prop_trim_id str = (not $ null str) && isAlreadyTrimmed ==> str == (trim str)
33− where
34− isAlreadyTrimmed :: Bool
35− isAlreadyTrimmed = (not $ isSpace $ head str) && (not $ isSpace $ last str)
36−
37−-- Check that a stripped string is shorter.
38−prop_stripHtml_length str = length str >= length (stripHtml str)
39−
40−-- Check that strings without tags remain untouched.
41−prop_stripHtml_id str = (not $ any (`elem` ['>', '<']) str)
42− ==> str == stripHtml str
43−
44−-- Strip Html test cases.
45−test_stripHtml_1 = stripHtml "<b>text</b>" @?= "text"
46−test_stripHtml_2 = stripHtml "text" @?= "text"
47−test_stripHtml_3 =
48− stripHtml "<b>Hakyll</b>, a <i>website</i> generator<img src=\"foo.png\" />"
49− @?= "Hakyll, a website generator"
50−
51−-- Link test cases.
52−test_link_1 = link "foo bar" "/foo/bar.html"
53− @?= "<a href=\"/foo/bar.html\">foo bar</a>"
54−test_link_2 = link "back home" "/" @?= "<a href=\"/\">back home</a>"