on master help sign in
.circleci/config.yml+5−11
since splitsnapshots
unifiedsplit
@@ -6,20 +6,14 @@ jobs:
6 steps:
6 steps:
7 - checkout
7 - checkout
8 - restore_cache:
8 - restore_cache:
9− key: 'v2-hakyll-{{ arch }}-{{ .Branch }}'
9+ key: 'v3-hakyll-{{ arch }}-{{ .Branch }}'
10− - run:
11− name: 'Update cabal indices'
12− command: 'cabal update'
13 - run:
10 - run:
14 # We set jobs to 1 here because that prevents Out-Of-Memory exceptions
11 # We set jobs to 1 here because that prevents Out-Of-Memory exceptions
15 # while compiling dependencies.
12 # while compiling dependencies.
16 name: 'Install dependencies'
13 name: 'Install dependencies'
17− command: '.circleci/tickle.sh cabal install --only-dependencies --enable-tests --jobs=1 --flags="+buildWebsite"'
14+ command: '.circleci/tickle.sh stack build --test --copy-bins --jobs=1'
18− - run:
19− name: 'Build and run tests'
20− command: 'cabal test'
21 - save_cache:
15 - save_cache:
22− key: 'v2-hakyll-{{ arch }}-{{ .Branch }}-{{ .Revision }}'
16+ key: 'v3-hakyll-{{ arch }}-{{ .Branch }}-{{ .Revision }}'
23 paths:
17 paths:
24− - '~/.cabal'
18+ - '~/.stack-work'
25− - '~/.ghc'
19+ - '~/.stack'
CHANGELOG.md+44
@@ -4,6 +4,50 @@ title: Releases
4
4
5# Releases
5# Releases
6
6
7+## Hakyll 4.13.0.0 (2019-08-30)
8+
9+- Improved documentation in many places (contribution by Bergi)
10+- Significantly improve error messages when reading and applying templates
11+ (contribution by Bergi)
12+ * `empty` and `fail` for `Compiler` now fail without or with a message,
13+ allowing for much better debug output
14+ * `renderFeed`, `renderRssWithTemplates` and `renderAtomWithTemplates` now
15+ take `Template` rather than `String` arguments
16+- Add option to specify date in directory structure,e.g.
17+ `posts/2019/05/10/tomorrow.md` (contribution by Taeer Bar-Ya)
18+- Bump QuickCheck to 2.13
19+
20+## Hakyll 4.12.5.2 (2019-05-09)
21+
22+- Bump pandoc to 2.7
23+- Add `srcset` to the list of URL attributes (contribution by c50a326)
24+- Expose `getCategory` in `Hakyll.Web.Tags` (contribution by Ng Wei En)
25+- Fix issue where `published` overwrites the user's context (contribution by
26+ ncaq)
27+
28+## Hakyll 4.12.5.1 (2019-02-03)
29+
30+- Bump pandoc to 2.6
31+- Bump pandoc-citeproc to 0.16
32+
33+## Hakyll 4.12.5.0 (2019-01-12)
34+
35+- Update dependencies (contribution by Hexirp):
36+ * Bump containers to 0.6
37+ * Bump yaml to 0.11
38+ * Bump pandoc to 2.5
39+ * Bump pandoc-citeproc to 0.15
40+ * Bump tasty to 1.2
41+- Correct assertion in unixFilterError test
42+ (contribution by Jim McStanton)
43+- Add renderRssWithTemplates, renderAtomWithTemplates
44+ (contribution by Abhinav Sarkar)
45+- Speed up hashing in cache (contribution by 0xd34df00d)
46+- Update type of fromFilePath to use FilePath instead of String
47+ (contribution by Jim McStanton)
48+- Drop extension when parsing dates in filepaths (contribution by
49+ Gabriel Aumala)
50+
7## Hakyll 4.12.4.0 (2018-08-13)
51## Hakyll 4.12.4.0 (2018-08-13)
8
52
9- Bump yaml to 0.10
53- Bump yaml to 0.10
hakyll.cabal+15−12
@@ -1,5 +1,5 @@
1Name: hakyll
1Name: hakyll
2−Version: 4.12.4.0
2+Version: 4.13.0.0
3
3
4Synopsis: A static website compiler library
4Synopsis: A static website compiler library
5Description:
5Description:
@@ -68,6 +68,7 @@ Extra-source-files:
68 tests/data/partial.html
68 tests/data/partial.html
69 tests/data/partial.html.out
69 tests/data/partial.html.out
70 tests/data/posts/2010-08-26-birthday.md
70 tests/data/posts/2010-08-26-birthday.md
71+ tests/data/posts/2018-09-26.md
71 tests/data/russian.md
72 tests/data/russian.md
72 tests/data/strip.html
73 tests/data/strip.html
73 tests/data/strip.html.out
74 tests/data/strip.html.out
@@ -166,15 +167,17 @@ Library
166 blaze-html >= 0.5 && < 0.10,
167 blaze-html >= 0.5 && < 0.10,
167 blaze-markup >= 0.5.1 && < 0.9,
168 blaze-markup >= 0.5.1 && < 0.9,
168 bytestring >= 0.9 && < 0.11,
169 bytestring >= 0.9 && < 0.11,
169− containers >= 0.3 && < 0.6,
170+ containers >= 0.3 && < 0.7,
170 cryptohash >= 0.7 && < 0.12,
171 cryptohash >= 0.7 && < 0.12,
171 data-default >= 0.4 && < 0.8,
172 data-default >= 0.4 && < 0.8,
172 deepseq >= 1.3 && < 1.5,
173 deepseq >= 1.3 && < 1.5,
173 directory >= 1.0 && < 1.4,
174 directory >= 1.0 && < 1.4,
175+ file-embed >= 0.0.10.1 && < 0.0.12,
174 filepath >= 1.0 && < 1.5,
176 filepath >= 1.0 && < 1.5,
175 lrucache >= 1.1.1 && < 1.3,
177 lrucache >= 1.1.1 && < 1.3,
176 mtl >= 1 && < 2.3,
178 mtl >= 1 && < 2.3,
177 network-uri >= 2.6 && < 2.7,
179 network-uri >= 2.6 && < 2.7,
180+ optparse-applicative >= 0.12 && < 0.15,
178 parsec >= 3.0 && < 3.2,
181 parsec >= 3.0 && < 3.2,
179 process >= 1.6 && < 1.7,
182 process >= 1.6 && < 1.7,
180 random >= 1.0 && < 1.2,
183 random >= 1.0 && < 1.2,
@@ -182,14 +185,13 @@ Library
182 resourcet >= 1.1 && < 1.3,
185 resourcet >= 1.1 && < 1.3,
183 scientific >= 0.3.4 && < 0.4,
186 scientific >= 0.3.4 && < 0.4,
184 tagsoup >= 0.13.1 && < 0.15,
187 tagsoup >= 0.13.1 && < 0.15,
188+ template-haskell >= 2.14 && < 2.15,
185 text >= 0.11 && < 1.3,
189 text >= 0.11 && < 1.3,
186 time >= 1.8 && < 1.10,
190 time >= 1.8 && < 1.10,
187 time-locale-compat >= 0.1 && < 0.2,
191 time-locale-compat >= 0.1 && < 0.2,
188 unordered-containers >= 0.2 && < 0.3,
192 unordered-containers >= 0.2 && < 0.3,
189 vector >= 0.11 && < 0.13,
193 vector >= 0.11 && < 0.13,
190− yaml >= 0.8.11 && < 0.11,
194+ yaml >= 0.8.11 && < 0.12
191− optparse-applicative >= 0.12 && < 0.15,
192− file-embed >= 0.0.10.1 && < 0.0.12
193
195
194 If flag(previewServer)
196 If flag(previewServer)
195 Build-depends:
197 Build-depends:
@@ -227,8 +229,8 @@ Library
227 Other-Modules:
229 Other-Modules:
228 Hakyll.Web.Pandoc.Binary
230 Hakyll.Web.Pandoc.Binary
229 Build-Depends:
231 Build-Depends:
230− pandoc >= 2.0.5 && < 2.3,
232+ pandoc >= 2.0.5 && < 2.8,
231− pandoc-citeproc >= 0.14 && < 0.15
233+ pandoc-citeproc >= 0.14 && < 0.17
232 Cpp-options:
234 Cpp-options:
233 -DUSE_PANDOC
235 -DUSE_PANDOC
234
236
@@ -253,24 +255,25 @@ Test-suite hakyll-tests
253 Hakyll.Web.CompressCss.Tests
255 Hakyll.Web.CompressCss.Tests
254 Hakyll.Web.Html.RelativizeUrls.Tests
256 Hakyll.Web.Html.RelativizeUrls.Tests
255 Hakyll.Web.Html.Tests
257 Hakyll.Web.Html.Tests
258+ Hakyll.Web.Tags.Tests
256 Hakyll.Web.Template.Context.Tests
259 Hakyll.Web.Template.Context.Tests
257 Hakyll.Web.Template.Tests
260 Hakyll.Web.Template.Tests
258 TestSuite.Util
261 TestSuite.Util
259
262
260 Build-Depends:
263 Build-Depends:
261 hakyll,
264 hakyll,
262− QuickCheck >= 2.8 && < 2.13,
265+ QuickCheck >= 2.8 && < 2.14,
263− tasty >= 0.11 && < 1.2,
266+ tasty >= 0.11 && < 1.3,
264 tasty-hunit >= 0.9 && < 0.11,
267 tasty-hunit >= 0.9 && < 0.11,
265 tasty-quickcheck >= 0.8 && < 0.11,
268 tasty-quickcheck >= 0.8 && < 0.11,
266 -- Copy pasted from hakyll dependencies:
269 -- Copy pasted from hakyll dependencies:
267 base >= 4.8 && < 5,
270 base >= 4.8 && < 5,
268 bytestring >= 0.9 && < 0.11,
271 bytestring >= 0.9 && < 0.11,
269− containers >= 0.3 && < 0.6,
272+ containers >= 0.3 && < 0.7,
270 filepath >= 1.0 && < 1.5,
273 filepath >= 1.0 && < 1.5,
271 text >= 0.11 && < 1.3,
274 text >= 0.11 && < 1.3,
272 unordered-containers >= 0.2 && < 0.3,
275 unordered-containers >= 0.2 && < 0.3,
273− yaml >= 0.8.11 && < 0.11
276+ yaml >= 0.8.11 && < 0.12
274
277
275 If flag(previewServer)
278 If flag(previewServer)
276 Cpp-options:
279 Cpp-options:
@@ -321,4 +324,4 @@ Executable hakyll-website
321 base >= 4 && < 5,
324 base >= 4 && < 5,
322 directory >= 1.0 && < 1.4,
325 directory >= 1.0 && < 1.4,
323 filepath >= 1.0 && < 1.5,
326 filepath >= 1.0 && < 1.5,
324− pandoc >= 2.0.5 && < 2.3
327+ pandoc >= 2.0.5 && < 2.8
lib/Hakyll/Core/Compiler.hs+35−2
@@ -24,13 +24,16 @@ module Hakyll.Core.Compiler
24 , cached
24 , cached
25 , unsafeCompiler
25 , unsafeCompiler
26 , debugCompiler
26 , debugCompiler
27+ , noResult
28+ , withErrorMessage
27 ) where
29 ) where
28
30
29
31
30--------------------------------------------------------------------------------
32--------------------------------------------------------------------------------
31−import Control.Monad (when, unless)
33+import Control.Monad (unless, when, (>=>))
32import Data.Binary (Binary)
34import Data.Binary (Binary)
33import Data.ByteString.Lazy (ByteString)
35import Data.ByteString.Lazy (ByteString)
36+import qualified Data.List.NonEmpty as NonEmpty
34import Data.Typeable (Typeable)
37import Data.Typeable (Typeable)
35import System.Environment (getProgName)
38import System.Environment (getProgName)
36import System.FilePath (takeExtension)
39import System.FilePath (takeExtension)
@@ -62,6 +65,7 @@ getUnderlyingExtension = takeExtension . toFilePath <$> getUnderlying
62
65
63
66
64--------------------------------------------------------------------------------
67--------------------------------------------------------------------------------
68+-- | Create an item from the underlying identifier and a given value.
65makeItem :: a -> Compiler (Item a)
69makeItem :: a -> Compiler (Item a)
66makeItem x = do
70makeItem x = do
67 identifier <- getUnderlying
71 identifier <- getUnderlying
@@ -141,6 +145,10 @@ saveSnapshot snapshot item = do
141
145
142
146
143--------------------------------------------------------------------------------
147--------------------------------------------------------------------------------
148+-- | Turn on caching for a compilation value to avoid recomputing it
149+-- on subsequent Hakyll runs.
150+-- The storage key consists of the underlying identifier of the compiled
151+-- ressource and the given name.
144cached :: (Binary a, Typeable a)
152cached :: (Binary a, Typeable a)
145 => String
153 => String
146 -> Compiler a
154 -> Compiler a
@@ -177,12 +185,37 @@ cached name compiler = do
177
185
178
186
179--------------------------------------------------------------------------------
187--------------------------------------------------------------------------------
188+-- | Run an IO computation without dependencies in a Compiler
180unsafeCompiler :: IO a -> Compiler a
189unsafeCompiler :: IO a -> Compiler a
181unsafeCompiler = compilerUnsafeIO
190unsafeCompiler = compilerUnsafeIO
182
191
183
192
184--------------------------------------------------------------------------------
193--------------------------------------------------------------------------------
194+-- | Fail so that it is treated as non-defined in an @\$if()\$@ branching
195+-- "Hakyll.Web.Template" macro, and alternative
196+-- 'Hakyll.Web.Template.Context.Context's are tried
197+--
198+-- @since 4.13.0
199+noResult :: String -> Compiler a
200+noResult = compilerNoResult . return
201+
202+
203+--------------------------------------------------------------------------------
204+-- | Prepend an error line to the error, if there is one. This allows you to
205+-- add helpful context to error messages.
206+--
207+-- @since 4.13.0
208+withErrorMessage :: String -> Compiler a -> Compiler a
209+withErrorMessage x = do
210+ compilerTry >=> either (compilerResult . CompilerError . prepend) return
211+ where
212+ prepend (CompilationFailure es) = CompilationFailure (x `NonEmpty.cons` es)
213+ prepend (CompilationNoResult es) = CompilationNoResult (x : es)
214+
215+
216+--------------------------------------------------------------------------------
185−-- | Compiler for debugging purposes
217+-- | Compiler for debugging purposes.
218+-- Passes a message to the debug logger that is printed in verbose mode.
186debugCompiler :: String -> Compiler ()
219debugCompiler :: String -> Compiler ()
187debugCompiler msg = do
220debugCompiler msg = do
188 logger <- compilerLogger <$> compilerAsk
221 logger <- compilerLogger <$> compilerAsk
lib/Hakyll/Core/Compiler/Internal.hs+126−41
@@ -1,6 +1,7 @@
1--------------------------------------------------------------------------------
1--------------------------------------------------------------------------------
2-- | Internally used compiler module
2-- | Internally used compiler module
3{-# LANGUAGE CPP #-}
3{-# LANGUAGE CPP #-}
4+{-# LANGUAGE DeriveFunctor #-}
4{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE FlexibleInstances #-}
5{-# LANGUAGE GADTs #-}
6{-# LANGUAGE GADTs #-}
6{-# LANGUAGE GeneralizedNewtypeDeriving #-}
7{-# LANGUAGE GeneralizedNewtypeDeriving #-}
@@ -10,19 +11,26 @@ module Hakyll.Core.Compiler.Internal
10 Snapshot
11 Snapshot
11 , CompilerRead (..)
12 , CompilerRead (..)
12 , CompilerWrite (..)
13 , CompilerWrite (..)
14+ , CompilerErrors (..)
13 , CompilerResult (..)
15 , CompilerResult (..)
14 , Compiler (..)
16 , Compiler (..)
15 , runCompiler
17 , runCompiler
16
18
17 -- * Core operations
19 -- * Core operations
20+ , compilerResult
18 , compilerTell
21 , compilerTell
19 , compilerAsk
22 , compilerAsk
23+ , compilerUnsafeIO
24+
25+ -- * Error operations
20 , compilerThrow
26 , compilerThrow
27+ , compilerNoResult
21 , compilerCatch
28 , compilerCatch
22− , compilerResult
29+ , compilerTry
23− , compilerUnsafeIO
30+ , compilerErrorMessages
24
31
25 -- * Utilities
32 -- * Utilities
33+ , compilerDebugEntries
26 , compilerTellDependencies
34 , compilerTellDependencies
27 , compilerTellCacheHits
35 , compilerTellCacheHits
28 ) where
36 ) where
@@ -32,7 +40,9 @@ module Hakyll.Core.Compiler.Internal
32import Control.Applicative (Alternative (..))
40import Control.Applicative (Alternative (..))
33import Control.Exception (SomeException, handle)
41import Control.Exception (SomeException, handle)
34import Control.Monad (forM_)
42import Control.Monad (forM_)
35−import Control.Monad.Except (MonadError (..))
43+import Control.Monad.Except (MonadError (..))
44+import Data.List.NonEmpty (NonEmpty (..))
45+import qualified Data.List.NonEmpty as NonEmpty
36#if MIN_VERSION_base(4,9,0)
46#if MIN_VERSION_base(4,9,0)
37import Data.Semigroup (Semigroup (..))
47import Data.Semigroup (Semigroup (..))
38#endif
48#endif
@@ -45,7 +55,6 @@ import Hakyll.Core.Configuration
45import Hakyll.Core.Dependencies
55import Hakyll.Core.Dependencies
46import Hakyll.Core.Identifier
56import Hakyll.Core.Identifier
47import Hakyll.Core.Identifier.Pattern
57import Hakyll.Core.Identifier.Pattern
48−import Hakyll.Core.Logger (Logger)
49import qualified Hakyll.Core.Logger as Logger
58import qualified Hakyll.Core.Logger as Logger
50import Hakyll.Core.Metadata
59import Hakyll.Core.Metadata
51import Hakyll.Core.Provider
60import Hakyll.Core.Provider
@@ -75,7 +84,7 @@ data CompilerRead = CompilerRead
75 , -- | Compiler store
84 , -- | Compiler store
76 compilerStore :: Store
85 compilerStore :: Store
77 , -- | Logger
86 , -- | Logger
78− compilerLogger :: Logger
87+ compilerLogger :: Logger.Logger
79 }
88 }
80
89
81
90
@@ -104,11 +113,29 @@ instance Monoid CompilerWrite where
104
113
105
114
106--------------------------------------------------------------------------------
115--------------------------------------------------------------------------------
107−data CompilerResult a where
116+-- | Distinguishes reasons in a 'CompilerError'
117+data CompilerErrors a
118+ -- | One or more exceptions occured during compilation
119+ = CompilationFailure (NonEmpty a)
120+ -- | Absence of any result, most notably in template contexts. May still
121+ -- have error messages.
122+ | CompilationNoResult [a]
123+ deriving Functor
124+
125+
126+-- | Unwrap a `CompilerErrors`
127+compilerErrorMessages :: CompilerErrors a -> [a]
128+compilerErrorMessages (CompilationFailure x) = NonEmpty.toList x
129+compilerErrorMessages (CompilationNoResult x) = x
130+
131+
132+--------------------------------------------------------------------------------
133+-- | An intermediate result of a compilation step
134+data CompilerResult a
108− CompilerDone :: a -> CompilerWrite -> CompilerResult a
135+ = CompilerDone a CompilerWrite
109− CompilerSnapshot :: Snapshot -> Compiler a -> CompilerResult a
136+ | CompilerSnapshot Snapshot (Compiler a)
110− CompilerError :: [String] -> CompilerResult a
111− CompilerRequire :: (Identifier, Snapshot) -> Compiler a -> CompilerResult a
137+ | CompilerRequire (Identifier, Snapshot) (Compiler a)
138+ | CompilerError (CompilerErrors String)
112
139
113
140
114--------------------------------------------------------------------------------
141--------------------------------------------------------------------------------
@@ -126,14 +153,14 @@ instance Functor Compiler where
126 return $ case res of
153 return $ case res of
127 CompilerDone x w -> CompilerDone (f x) w
154 CompilerDone x w -> CompilerDone (f x) w
128 CompilerSnapshot s c' -> CompilerSnapshot s (fmap f c')
155 CompilerSnapshot s c' -> CompilerSnapshot s (fmap f c')
129− CompilerError e -> CompilerError e
130 CompilerRequire i c' -> CompilerRequire i (fmap f c')
156 CompilerRequire i c' -> CompilerRequire i (fmap f c')
157+ CompilerError e -> CompilerError e
131 {-# INLINE fmap #-}
158 {-# INLINE fmap #-}
132
159
133
160
134--------------------------------------------------------------------------------
161--------------------------------------------------------------------------------
135instance Monad Compiler where
162instance Monad Compiler where
136− return x = Compiler $ \_ -> return $ CompilerDone x mempty
163+ return x = compilerResult $ CompilerDone x mempty
137 {-# INLINE return #-}
164 {-# INLINE return #-}
138
165
139 Compiler c >>= f = Compiler $ \r -> do
166 Compiler c >>= f = Compiler $ \r -> do
@@ -146,14 +173,14 @@ instance Monad Compiler where
146 CompilerSnapshot s c' -> CompilerSnapshot s $ do
173 CompilerSnapshot s c' -> CompilerSnapshot s $ do
147 compilerTell w -- Save dependencies!
174 compilerTell w -- Save dependencies!
148 c'
175 c'
149− CompilerError e -> CompilerError e
150 CompilerRequire i c' -> CompilerRequire i $ do
176 CompilerRequire i c' -> CompilerRequire i $ do
151 compilerTell w -- Save dependencies!
177 compilerTell w -- Save dependencies!
152 c'
178 c'
179+ CompilerError e -> CompilerError e
153
180
154 CompilerSnapshot s c' -> return $ CompilerSnapshot s (c' >>= f)
181 CompilerSnapshot s c' -> return $ CompilerSnapshot s (c' >>= f)
155− CompilerError e -> return $ CompilerError e
156 CompilerRequire i c' -> return $ CompilerRequire i (c' >>= f)
182 CompilerRequire i c' -> return $ CompilerRequire i (c' >>= f)
183+ CompilerError e -> return $ CompilerError e
157 {-# INLINE (>>=) #-}
184 {-# INLINE (>>=) #-}
158
185
159 fail = compilerThrow . return
186 fail = compilerThrow . return
@@ -170,87 +197,145 @@ instance Applicative Compiler where
170
197
171
198
172--------------------------------------------------------------------------------
199--------------------------------------------------------------------------------
200+-- | Access provided metadata from anywhere
173instance MonadMetadata Compiler where
201instance MonadMetadata Compiler where
174 getMetadata = compilerGetMetadata
202 getMetadata = compilerGetMetadata
175 getMatches = compilerGetMatches
203 getMatches = compilerGetMatches
176
204
177
205
178--------------------------------------------------------------------------------
206--------------------------------------------------------------------------------
207+-- | Compilation may fail with multiple error messages.
208+-- 'catchError' handles errors from 'throwError', 'fail' and 'Hakyll.Core.Compiler.noResult'
179instance MonadError [String] Compiler where
209instance MonadError [String] Compiler where
180− throwError = compilerThrow
210+ throwError = compilerThrow
181− catchError = compilerCatch
211+ catchError c = compilerCatch c . (. compilerErrorMessages)
182
212
183
213
184--------------------------------------------------------------------------------
214--------------------------------------------------------------------------------
215+-- | Like 'unCompiler' but treating IO exceptions as 'CompilerError's
185runCompiler :: Compiler a -> CompilerRead -> IO (CompilerResult a)
216runCompiler :: Compiler a -> CompilerRead -> IO (CompilerResult a)
186runCompiler compiler read' = handle handler $ unCompiler compiler read'
217runCompiler compiler read' = handle handler $ unCompiler compiler read'
187 where
218 where
188 handler :: SomeException -> IO (CompilerResult a)
219 handler :: SomeException -> IO (CompilerResult a)
189− handler e = return $ CompilerError [show e]
220+ handler e = return $ CompilerError $ CompilationFailure $ show e :| []
190
221
191
222
192--------------------------------------------------------------------------------
223--------------------------------------------------------------------------------
224+-- | Trying alternative compilers if the first fails, regardless whether through
225+-- 'fail', 'throwError' or 'Hakyll.Core.Compiler.noResult'.
226+-- Aggregates error messages if all fail.
193instance Alternative Compiler where
227instance Alternative Compiler where
194− empty = compilerThrow []
228+ empty = compilerNoResult []
195− x <|> y = compilerCatch x $ \es -> do
229+ x <|> y = x `compilerCatch` (\rx -> y `compilerCatch` (\ry ->
196− logger <- compilerLogger <$> compilerAsk
230+ case (rx, ry) of
197− forM_ es $ \e -> compilerUnsafeIO $ Logger.debug logger $
231+ (CompilationFailure xs, CompilationFailure ys) ->
232+ compilerThrow $ NonEmpty.toList xs ++ NonEmpty.toList ys
233+ (CompilationFailure xs, CompilationNoResult ys) ->
234+ debug ys >> compilerThrow (NonEmpty.toList xs)
235+ (CompilationNoResult xs, CompilationFailure ys) ->
236+ debug xs >> compilerThrow (NonEmpty.toList ys)
237+ (CompilationNoResult xs, CompilationNoResult ys) -> compilerNoResult $ xs ++ ys
238+ ))
239+ where
198− "Hakyll.Core.Compiler.Internal: Alternative failed: " ++ e
240+ debug = compilerDebugEntries "Hakyll.Core.Compiler.Internal: Alternative fail suppressed"
199− y
200 {-# INLINE (<|>) #-}
241 {-# INLINE (<|>) #-}
201
242
202
243
203--------------------------------------------------------------------------------
244--------------------------------------------------------------------------------
245+-- | Put the result back in a compiler
246+compilerResult :: CompilerResult a -> Compiler a
247+compilerResult x = Compiler $ \_ -> return x
248+{-# INLINE compilerResult #-}
249+
250+
251+--------------------------------------------------------------------------------
252+-- | Get the current environment
204compilerAsk :: Compiler CompilerRead
253compilerAsk :: Compiler CompilerRead
205compilerAsk = Compiler $ \r -> return $ CompilerDone r mempty
254compilerAsk = Compiler $ \r -> return $ CompilerDone r mempty
206{-# INLINE compilerAsk #-}
255{-# INLINE compilerAsk #-}
207
256
208
257
209--------------------------------------------------------------------------------
258--------------------------------------------------------------------------------
259+-- | Put a 'CompilerWrite'
210compilerTell :: CompilerWrite -> Compiler ()
260compilerTell :: CompilerWrite -> Compiler ()
211−compilerTell deps = Compiler $ \_ -> return $ CompilerDone () deps
261+compilerTell = compilerResult . CompilerDone ()
212{-# INLINE compilerTell #-}
262{-# INLINE compilerTell #-}
213
263
214
264
215--------------------------------------------------------------------------------
265--------------------------------------------------------------------------------
266+-- | Run an IO computation without dependencies in a Compiler
267+compilerUnsafeIO :: IO a -> Compiler a
268+compilerUnsafeIO io = Compiler $ \_ -> do
269+ x <- io
270+ return $ CompilerDone x mempty
271+{-# INLINE compilerUnsafeIO #-}
272+
273+
274+--------------------------------------------------------------------------------
275+-- | Throw errors in the 'Compiler'.
276+--
277+-- If no messages are given, this is considered a 'CompilationNoResult' error.
278+-- Otherwise, it is treated as a proper compilation failure.
216compilerThrow :: [String] -> Compiler a
279compilerThrow :: [String] -> Compiler a
217−compilerThrow es = Compiler $ \_ -> return $ CompilerError es
280+compilerThrow = compilerResult . CompilerError .
218−{-# INLINE compilerThrow #-}
281+ maybe (CompilationNoResult []) CompilationFailure .
282+ NonEmpty.nonEmpty
283+
284+-- | Put a 'CompilerError' with multiple messages as 'CompilationNoResult'
285+compilerNoResult :: [String] -> Compiler a
286+compilerNoResult = compilerResult . CompilerError . CompilationNoResult
219
287
220
288
221--------------------------------------------------------------------------------
289--------------------------------------------------------------------------------
222−compilerCatch :: Compiler a -> ([String] -> Compiler a) -> Compiler a
290+-- | Allows to distinguish 'CompilerError's and branch on them with 'Either'
291+--
292+-- prop> compilerTry = (`compilerCatch` return . Left) . fmap Right
293+compilerTry :: Compiler a -> Compiler (Either (CompilerErrors String) a)
294+compilerTry (Compiler x) = Compiler $ \r -> do
295+ res <- x r
296+ case res of
297+ CompilerDone res' w -> return (CompilerDone (Right res') w)
298+ CompilerSnapshot s c -> return (CompilerSnapshot s (compilerTry c))
299+ CompilerRequire i c -> return (CompilerRequire i (compilerTry c))
300+ CompilerError e -> return (CompilerDone (Left e) mempty)
301+{-# INLINE compilerTry #-}
302+
303+
304+--------------------------------------------------------------------------------
305+-- | Allows you to recover from 'CompilerError's.
306+-- Uses the same parameter order as 'catchError' so that it can be used infix.
307+--
308+-- prop> c `compilerCatch` f = compilerTry c >>= either f return
309+compilerCatch :: Compiler a -> (CompilerErrors String -> Compiler a) -> Compiler a
223compilerCatch (Compiler x) f = Compiler $ \r -> do
310compilerCatch (Compiler x) f = Compiler $ \r -> do
224 res <- x r
311 res <- x r
225 case res of
312 case res of
226 CompilerDone res' w -> return (CompilerDone res' w)
313 CompilerDone res' w -> return (CompilerDone res' w)
227 CompilerSnapshot s c -> return (CompilerSnapshot s (compilerCatch c f))
314 CompilerSnapshot s c -> return (CompilerSnapshot s (compilerCatch c f))
228− CompilerError e -> unCompiler (f e) r
229 CompilerRequire i c -> return (CompilerRequire i (compilerCatch c f))
315 CompilerRequire i c -> return (CompilerRequire i (compilerCatch c f))
316+ CompilerError e -> unCompiler (f e) r
230{-# INLINE compilerCatch #-}
317{-# INLINE compilerCatch #-}
231
318
232
319
233--------------------------------------------------------------------------------
320--------------------------------------------------------------------------------
234−-- | Put the result back in a compiler
321+compilerDebugLog :: [String] -> Compiler ()
235−compilerResult :: CompilerResult a -> Compiler a
322+compilerDebugLog ms = do
236−compilerResult x = Compiler $ \_ -> return x
323+ logger <- compilerLogger <$> compilerAsk
237−{-# INLINE compilerResult #-}
324+ compilerUnsafeIO $ forM_ ms $ Logger.debug logger
238−
239
325
240--------------------------------------------------------------------------------
326--------------------------------------------------------------------------------
241−compilerUnsafeIO :: IO a -> Compiler a
327+-- | Pass a list of messages with a heading to the debug logger
242−compilerUnsafeIO io = Compiler $ \_ -> do
328+compilerDebugEntries :: String -> [String] -> Compiler ()
243− x <- io
329+compilerDebugEntries msg = compilerDebugLog . (msg:) . map indent
244− return $ CompilerDone x mempty
330+ where
245−{-# INLINE compilerUnsafeIO #-}
331+ indent = unlines . map (" "++) . lines
246
332
247
333
248--------------------------------------------------------------------------------
334--------------------------------------------------------------------------------
249compilerTellDependencies :: [Dependency] -> Compiler ()
335compilerTellDependencies :: [Dependency] -> Compiler ()
250compilerTellDependencies ds = do
336compilerTellDependencies ds = do
251− logger <- compilerLogger <$> compilerAsk
337+ compilerDebugLog $ map (\d ->
252− forM_ ds $ \d -> compilerUnsafeIO $ Logger.debug logger $
253− "Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d
338+ "Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d) ds
254 compilerTell mempty {compilerDependencies = ds}
339 compilerTell mempty {compilerDependencies = ds}
255{-# INLINE compilerTellDependencies #-}
340{-# INLINE compilerTellDependencies #-}
256
341
lib/Hakyll/Core/Compiler/Require.hs+2
@@ -91,6 +91,7 @@ loadBody id' = loadSnapshotBody id' final
91
91
92
92
93--------------------------------------------------------------------------------
93--------------------------------------------------------------------------------
94+-- | A shortcut for only requiring the body for a specific snapshot of an item
94loadSnapshotBody :: (Binary a, Typeable a)
95loadSnapshotBody :: (Binary a, Typeable a)
95 => Identifier -> Snapshot -> Compiler a
96 => Identifier -> Snapshot -> Compiler a
96loadSnapshotBody id' snapshot = fmap itemBody $ loadSnapshot id' snapshot
97loadSnapshotBody id' snapshot = fmap itemBody $ loadSnapshot id' snapshot
@@ -103,6 +104,7 @@ loadAll pattern = loadAllSnapshots pattern final
103
104
104
105
105--------------------------------------------------------------------------------
106--------------------------------------------------------------------------------
107+-- | Load a specific snapshot for each of dynamic list of items
106loadAllSnapshots :: (Binary a, Typeable a)
108loadAllSnapshots :: (Binary a, Typeable a)
107 => Pattern -> Snapshot -> Compiler [Item a]
109 => Pattern -> Snapshot -> Compiler [Item a]
108loadAllSnapshots pattern snapshot = do
110loadAllSnapshots pattern snapshot = do
lib/Hakyll/Core/Logger.hs+9−3
@@ -18,6 +18,7 @@ import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
18import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)
18import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)
19import Control.Monad (forever)
19import Control.Monad (forever)
20import Control.Monad.Trans (MonadIO, liftIO)
20import Control.Monad.Trans (MonadIO, liftIO)
21+import Data.List (intercalate)
21import Prelude hiding (error)
22import Prelude hiding (error)
22
23
23
24
@@ -79,7 +80,7 @@ string l v m
79
80
80--------------------------------------------------------------------------------
81--------------------------------------------------------------------------------
81error :: MonadIO m => Logger -> String -> m ()
82error :: MonadIO m => Logger -> String -> m ()
82−error l m = string l Error $ " [ERROR] " ++ m
83+error l m = string l Error $ " [ERROR] " ++ indent m
83
84
84
85
85--------------------------------------------------------------------------------
86--------------------------------------------------------------------------------
@@ -89,9 +90,14 @@ header l = string l Message
89
90
90--------------------------------------------------------------------------------
91--------------------------------------------------------------------------------
91message :: MonadIO m => Logger -> String -> m ()
92message :: MonadIO m => Logger -> String -> m ()
92−message l m = string l Message $ " " ++ m
93+message l m = string l Message $ " " ++ indent m
93
94
94
95
95--------------------------------------------------------------------------------
96--------------------------------------------------------------------------------
96debug :: MonadIO m => Logger -> String -> m ()
97debug :: MonadIO m => Logger -> String -> m ()
97−debug l m = string l Debug $ " [DEBUG] " ++ m
98+debug l m = string l Debug $ " [DEBUG] " ++ indent m
99+
100+
101+--------------------------------------------------------------------------------
102+indent :: String -> String
103+indent = intercalate "\n " . lines
lib/Hakyll/Core/Provider/Metadata.hs+3−2
@@ -27,6 +27,7 @@ import Hakyll.Core.Identifier
27import Hakyll.Core.Metadata
27import Hakyll.Core.Metadata
28import Hakyll.Core.Provider.Internal
28import Hakyll.Core.Provider.Internal
29import System.IO as IO
29import System.IO as IO
30+import System.IO.Error (modifyIOError, ioeSetLocation)
30
31
31
32
32--------------------------------------------------------------------------------
33--------------------------------------------------------------------------------
@@ -51,7 +52,7 @@ loadMetadata p identifier = do
51--------------------------------------------------------------------------------
52--------------------------------------------------------------------------------
52loadMetadataHeader :: FilePath -> IO (Metadata, String)
53loadMetadataHeader :: FilePath -> IO (Metadata, String)
53loadMetadataHeader fp = do
54loadMetadataHeader fp = do
54− fileContent <- readFile fp
55+ fileContent <- modifyIOError (`ioeSetLocation` "loadMetadataHeader") $ readFile fp
55 case parsePage fileContent of
56 case parsePage fileContent of
56 Right x -> return x
57 Right x -> return x
57 Left err -> throwIO $ MetadataException fp err
58 Left err -> throwIO $ MetadataException fp err
@@ -60,7 +61,7 @@ loadMetadataHeader fp = do
60--------------------------------------------------------------------------------
61--------------------------------------------------------------------------------
61loadMetadataFile :: FilePath -> IO Metadata
62loadMetadataFile :: FilePath -> IO Metadata
62loadMetadataFile fp = do
63loadMetadataFile fp = do
63− fileContent <- B.readFile fp
64+ fileContent <- modifyIOError (`ioeSetLocation` "loadMetadataFile") $ B.readFile fp
64 let errOrMeta = Yaml.decodeEither' fileContent
65 let errOrMeta = Yaml.decodeEither' fileContent
65 either (fail . show) return errOrMeta
66 either (fail . show) return errOrMeta
66
67
lib/Hakyll/Core/Runtime.hs+3−3
@@ -199,9 +199,9 @@ chase trail id'
199 result <- liftIO $ runCompiler compiler read'
199 result <- liftIO $ runCompiler compiler read'
200 case result of
200 case result of
201 -- Rethrow error
201 -- Rethrow error
202− CompilerError [] -> throwError
202+ CompilerError e -> throwError $ case compilerErrorMessages e of
203− "Compiler failed but no info given, try running with -v?"
203+ [] -> "Compiler failed but no info given, try running with -v?"
204− CompilerError es -> throwError $ intercalate "; " es
204+ es -> intercalate "; " es
205
205
206 -- Signal that a snapshot was saved ->
206 -- Signal that a snapshot was saved ->
207 CompilerSnapshot snapshot c -> do
207 CompilerSnapshot snapshot c -> do
lib/Hakyll/Core/Store.hs+20−21
@@ -16,7 +16,6 @@ module Hakyll.Core.Store
16
16
17
17
18--------------------------------------------------------------------------------
18--------------------------------------------------------------------------------
19−import Control.Exception (IOException, handle)
20import qualified Crypto.Hash.MD5 as MD5
19import qualified Crypto.Hash.MD5 as MD5
21import Data.Binary (Binary, decode, encodeFile)
20import Data.Binary (Binary, decode, encodeFile)
22import qualified Data.ByteString as B
21import qualified Data.ByteString as B
@@ -32,7 +31,8 @@ import System.Directory (createDirectoryIfMissing)
32import System.Directory (doesFileExist, removeFile)
31import System.Directory (doesFileExist, removeFile)
33import System.FilePath ((</>))
32import System.FilePath ((</>))
34import System.IO (IOMode (..), hClose, openFile)
33import System.IO (IOMode (..), hClose, openFile)
35−import Text.Printf (printf)
34+import System.IO.Error (catchIOError, ioeSetFileName,
35+ ioeSetLocation, modifyIOError)
36
36
37
37
38--------------------------------------------------------------------------------
38--------------------------------------------------------------------------------
@@ -85,6 +85,14 @@ new inMemory directory = do
85 where
85 where
86 csize = Just 500
86 csize = Just 500
87
87
88+--------------------------------------------------------------------------------
89+withStore :: Store -> String -> (String -> FilePath -> IO a) -> [String] -> IO a
90+withStore store loc run identifier = modifyIOError handle $ run key path
91+ where
92+ key = hash identifier
93+ path = storeDirectory store </> key
94+ handle e = e `ioeSetFileName` (path ++ " for " ++ intercalate "/" identifier)
95+ `ioeSetLocation` ("Store." ++ loc)
88
96
89--------------------------------------------------------------------------------
97--------------------------------------------------------------------------------
90-- | Auxiliary: add an item to the in-memory cache
98-- | Auxiliary: add an item to the in-memory cache
@@ -125,17 +133,16 @@ cacheDelete (Store _ (Just lru)) key = do
125--------------------------------------------------------------------------------
133--------------------------------------------------------------------------------
126-- | Store an item
134-- | Store an item
127set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()
135set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()
128−set store identifier value = do
136+set store identifier value = withStore store "set" (\key path -> do
129− encodeFile (storeDirectory store </> key) value
137+ encodeFile path value
130 cacheInsert store key value
138 cacheInsert store key value
131− where
132− key = hash identifier
139+ ) identifier
133
140
134
141
135--------------------------------------------------------------------------------
142--------------------------------------------------------------------------------
136-- | Load an item
143-- | Load an item
137get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)
144get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)
138−get store identifier = do
145+get store = withStore store "get" $ \key path -> do
139 -- First check the in-memory map
146 -- First check the in-memory map
140 ref <- cacheLookup store key
147 ref <- cacheLookup store key
141 case ref of
148 case ref of
@@ -147,17 +154,14 @@ get store identifier = do
147 then return NotFound
154 then return NotFound
148 -- Found in the filesystem
155 -- Found in the filesystem
149 else do
156 else do
150− v <- decodeClose
157+ v <- decodeClose path
151 cacheInsert store key v
158 cacheInsert store key v
152 return $ Found v
159 return $ Found v
153 -- Found in the in-memory map (or wrong type), just return
160 -- Found in the in-memory map (or wrong type), just return
154 s -> return s
161 s -> return s
155 where
162 where
156− key = hash identifier
157− path = storeDirectory store </> key
158−
159 -- 'decodeFile' from Data.Binary which closes the file ASAP
163 -- 'decodeFile' from Data.Binary which closes the file ASAP
160− decodeClose = do
164+ decodeClose path = do
161 h <- openFile path ReadMode
165 h <- openFile path ReadMode
162 lbs <- BL.hGetContents h
166 lbs <- BL.hGetContents h
163 BL.length lbs `seq` hClose h
167 BL.length lbs `seq` hClose h
@@ -167,28 +171,23 @@ get store identifier = do
167--------------------------------------------------------------------------------
171--------------------------------------------------------------------------------
168-- | Strict function
172-- | Strict function
169isMember :: Store -> [String] -> IO Bool
173isMember :: Store -> [String] -> IO Bool
170−isMember store identifier = do
174+isMember store = withStore store "isMember" $ \key path -> do
171 inCache <- cacheIsMember store key
175 inCache <- cacheIsMember store key
172 if inCache then return True else doesFileExist path
176 if inCache then return True else doesFileExist path
173− where
174− key = hash identifier
175− path = storeDirectory store </> key
176
177
177
178
178--------------------------------------------------------------------------------
179--------------------------------------------------------------------------------
179-- | Delete an item
180-- | Delete an item
180delete :: Store -> [String] -> IO ()
181delete :: Store -> [String] -> IO ()
181−delete store identifier = do
182+delete store = withStore store "delete" $ \key path -> do
182 cacheDelete store key
183 cacheDelete store key
183− deleteFile $ storeDirectory store </> key
184+ deleteFile path
184− where
185− key = hash identifier
186
185
187
186
188--------------------------------------------------------------------------------
187--------------------------------------------------------------------------------
189-- | Delete a file unless it doesn't exist...
188-- | Delete a file unless it doesn't exist...
190deleteFile :: FilePath -> IO ()
189deleteFile :: FilePath -> IO ()
191−deleteFile = handle (\(_ :: IOException) -> return ()) . removeFile
190+deleteFile = (`catchIOError` \_ -> return ()) . removeFile
192
191
193
192
194--------------------------------------------------------------------------------
193--------------------------------------------------------------------------------
lib/Hakyll/Web/Feed.hs+51−32
@@ -1,4 +1,5 @@
1+{-# LANGUAGE OverloadedStrings #-}
1−{-# LANGUAGE TemplateHaskell #-}
2+{-# LANGUAGE TemplateHaskell #-}
2
3
3--------------------------------------------------------------------------------
4--------------------------------------------------------------------------------
4-- | A Module that allows easy rendering of RSS feeds.
5-- | A Module that allows easy rendering of RSS feeds.
@@ -22,6 +23,8 @@ module Hakyll.Web.Feed
22 ( FeedConfiguration (..)
23 ( FeedConfiguration (..)
23 , renderRss
24 , renderRss
24 , renderAtom
25 , renderAtom
26+ , renderRssWithTemplates
27+ , renderAtomWithTemplates
25 ) where
28 ) where
26
29
27
30
@@ -35,27 +38,26 @@ import Hakyll.Web.Template.List
35
38
36
39
37--------------------------------------------------------------------------------
40--------------------------------------------------------------------------------
38−import Data.FileEmbed (makeRelativeToProject, embedFile)
41+import Data.FileEmbed (makeRelativeToProject)
39−import qualified Data.Text as T
40−import qualified Data.Text.Encoding as T
41
42
42
43
43--------------------------------------------------------------------------------
44--------------------------------------------------------------------------------
44−rssTemplate :: String
45+rssTemplate :: Template
45−rssTemplate = T.unpack $
46+rssTemplate =
46− T.decodeUtf8 $(makeRelativeToProject "data/templates/rss.xml" >>= embedFile)
47+ $(makeRelativeToProject "data/templates/rss.xml" >>= embedTemplate)
47
48
48−rssItemTemplate :: String
49+rssItemTemplate :: Template
49−rssItemTemplate = T.unpack $
50+rssItemTemplate =
50− T.decodeUtf8 $(makeRelativeToProject "data/templates/rss-item.xml" >>= embedFile)
51+ $(makeRelativeToProject "data/templates/rss-item.xml" >>= embedTemplate)
51
52
52−atomTemplate :: String
53+atomTemplate :: Template
53−atomTemplate = T.unpack $
54+atomTemplate =
55+ $(makeRelativeToProject "data/templates/atom.xml" >>= embedTemplate)
56+
57+atomItemTemplate :: Template
58+atomItemTemplate =
54− T.decodeUtf8 $(makeRelativeToProject "data/templates/atom.xml" >>= embedFile)
59+ $(makeRelativeToProject "data/templates/atom-item.xml" >>= embedTemplate)
55
60
56−atomItemTemplate :: String
57−atomItemTemplate = T.unpack $
58− T.decodeUtf8 $(makeRelativeToProject "data/templates/atom-item.xml" >>= embedFile)
59
61
60--------------------------------------------------------------------------------
62--------------------------------------------------------------------------------
61-- | This is a data structure to keep the configuration of a feed.
63-- | This is a data structure to keep the configuration of a feed.
@@ -75,16 +77,13 @@ data FeedConfiguration = FeedConfiguration
75
77
76--------------------------------------------------------------------------------
78--------------------------------------------------------------------------------
77-- | Abstract function to render any feed.
79-- | Abstract function to render any feed.
78−renderFeed :: String -- ^ Default feed template
80+renderFeed :: Template -- ^ Default feed template
79− -> String -- ^ Default item template
81+ -> Template -- ^ Default item template
80 -> FeedConfiguration -- ^ Feed configuration
82 -> FeedConfiguration -- ^ Feed configuration
81 -> Context String -- ^ Context for the items
83 -> Context String -- ^ Context for the items
82 -> [Item String] -- ^ Input items
84 -> [Item String] -- ^ Input items
83 -> Compiler (Item String) -- ^ Resulting item
85 -> Compiler (Item String) -- ^ Resulting item
84−renderFeed defFeed defItem config itemContext items = do
86+renderFeed feedTpl itemTpl config itemContext items = do
85− feedTpl <- readTemplateFile defFeed
86− itemTpl <- readTemplateFile defItem
87−
88 protectedItems <- mapM (applyFilter protectCDATA) items
87 protectedItems <- mapM (applyFilter protectCDATA) items
89 body <- makeItem =<< applyTemplateList itemTpl itemContext' protectedItems
88 body <- makeItem =<< applyTemplateList itemTpl itemContext' protectedItems
90 applyTemplate feedTpl feedContext body
89 applyTemplate feedTpl feedContext body
@@ -118,11 +117,35 @@ renderFeed defFeed defItem config itemContext items = do
118 updatedField = field "updated" $ \_ -> case items of
117 updatedField = field "updated" $ \_ -> case items of
119 [] -> return "Unknown"
118 [] -> return "Unknown"
120 (x : _) -> unContext itemContext' "updated" [] x >>= \cf -> case cf of
119 (x : _) -> unContext itemContext' "updated" [] x >>= \cf -> case cf of
121− ListField _ _ -> fail "Hakyll.Web.Feed.renderFeed: Internal error"
122 StringField s -> return s
120 StringField s -> return s
121+ _ -> fail "Hakyll.Web.Feed.renderFeed: Internal error"
122+
123+--------------------------------------------------------------------------------
124+-- | Render an RSS feed using given templates with a number of items.
125+renderRssWithTemplates ::
126+ Template -- ^ Feed template
127+ -> Template -- ^ Item template
128+ -> FeedConfiguration -- ^ Feed configuration
129+ -> Context String -- ^ Item context
130+ -> [Item String] -- ^ Feed items
131+ -> Compiler (Item String) -- ^ Resulting feed
132+renderRssWithTemplates feedTemplate itemTemplate config context = renderFeed
133+ feedTemplate itemTemplate config
134+ (makeItemContext "%a, %d %b %Y %H:%M:%S UT" context)
123
135
124− readTemplateFile :: String -> Compiler Template
136+
125− readTemplateFile value = pure $ template $ readTemplateElems value
137+--------------------------------------------------------------------------------
138+-- | Render an Atom feed using given templates with a number of items.
139+renderAtomWithTemplates ::
140+ Template -- ^ Feed template
141+ -> Template -- ^ Item template
142+ -> FeedConfiguration -- ^ Feed configuration
143+ -> Context String -- ^ Item context
144+ -> [Item String] -- ^ Feed items
145+ -> Compiler (Item String) -- ^ Resulting feed
146+renderAtomWithTemplates feedTemplate itemTemplate config context = renderFeed
147+ feedTemplate itemTemplate config
148+ (makeItemContext "%Y-%m-%dT%H:%M:%SZ" context)
126
149
127
150
128--------------------------------------------------------------------------------
151--------------------------------------------------------------------------------
@@ -131,9 +154,7 @@ renderRss :: FeedConfiguration -- ^ Feed configuration
131 -> Context String -- ^ Item context
154 -> Context String -- ^ Item context
132 -> [Item String] -- ^ Feed items
155 -> [Item String] -- ^ Feed items
133 -> Compiler (Item String) -- ^ Resulting feed
156 -> Compiler (Item String) -- ^ Resulting feed
134−renderRss config context = renderFeed
135− rssTemplate rssItemTemplate config
157+renderRss = renderRssWithTemplates rssTemplate rssItemTemplate
136− (makeItemContext "%a, %d %b %Y %H:%M:%S UT" context)
137
158
138
159
139--------------------------------------------------------------------------------
160--------------------------------------------------------------------------------
@@ -142,13 +163,11 @@ renderAtom :: FeedConfiguration -- ^ Feed configuration
142 -> Context String -- ^ Item context
163 -> Context String -- ^ Item context
143 -> [Item String] -- ^ Feed items
164 -> [Item String] -- ^ Feed items
144 -> Compiler (Item String) -- ^ Resulting feed
165 -> Compiler (Item String) -- ^ Resulting feed
145−renderAtom config context = renderFeed
146− atomTemplate atomItemTemplate config
166+renderAtom = renderAtomWithTemplates atomTemplate atomItemTemplate
147− (makeItemContext "%Y-%m-%dT%H:%M:%SZ" context)
148
167
149
168
150--------------------------------------------------------------------------------
169--------------------------------------------------------------------------------
151-- | Copies @$updated$@ from @$published$@ if it is not already set.
170-- | Copies @$updated$@ from @$published$@ if it is not already set.
152makeItemContext :: String -> Context a -> Context a
171makeItemContext :: String -> Context a -> Context a
153makeItemContext fmt context = mconcat
172makeItemContext fmt context = mconcat
154− [dateField "published" fmt, context, dateField "updated" fmt]
173+ [context, dateField "published" fmt, dateField "updated" fmt]
lib/Hakyll/Web/Html.hs+1−1
@@ -59,7 +59,7 @@ demoteHeaders = withTags $ \tag -> case tag of
59
59
60--------------------------------------------------------------------------------
60--------------------------------------------------------------------------------
61isUrlAttribute :: String -> Bool
61isUrlAttribute :: String -> Bool
62−isUrlAttribute = (`elem` ["src", "href", "data", "poster"])
62+isUrlAttribute = (`elem` ["src", "href", "data", "poster", "srcset"])
63
63
64
64
65--------------------------------------------------------------------------------
65--------------------------------------------------------------------------------
lib/Hakyll/Web/Tags.hs+2−1
@@ -43,6 +43,7 @@
43module Hakyll.Web.Tags
43module Hakyll.Web.Tags
44 ( Tags (..)
44 ( Tags (..)
45 , getTags
45 , getTags
46+ , getCategory
46 , buildTagsWith
47 , buildTagsWith
47 , buildTags
48 , buildTags
48 , buildCategories
49 , buildCategories
@@ -112,7 +113,7 @@ getTags identifier = do
112
113
113
114
114--------------------------------------------------------------------------------
115--------------------------------------------------------------------------------
115−-- | Obtain categories from a page.
116+-- | Obtain category from a page.
116getCategory :: MonadMetadata m => Identifier -> m [String]
117getCategory :: MonadMetadata m => Identifier -> m [String]
117getCategory = return . return . takeBaseName . takeDirectory . toFilePath
118getCategory = return . return . takeBaseName . takeDirectory . toFilePath
118
119
lib/Hakyll/Web/Template.hs+24−2
@@ -138,19 +138,41 @@
138-- > 3...2...1
138-- > 3...2...1
139-- > </p>
139-- > </p>
140--
140--
141+{-# LANGUAGE TemplateHaskell #-}
141module Hakyll.Web.Template
142module Hakyll.Web.Template
142 ( Template
143 ( Template
143− , template
144− , readTemplateElems
145 , templateBodyCompiler
144 , templateBodyCompiler
146 , templateCompiler
145 , templateCompiler
147 , applyTemplate
146 , applyTemplate
148 , loadAndApplyTemplate
147 , loadAndApplyTemplate
149 , applyAsTemplate
148 , applyAsTemplate
150 , readTemplate
149 , readTemplate
150+ , compileTemplateItem
151 , unsafeReadTemplateFile
151 , unsafeReadTemplateFile
152+ , embedTemplate
152 ) where
153 ) where
153
154
154
155
155--------------------------------------------------------------------------------
156--------------------------------------------------------------------------------
156import Hakyll.Web.Template.Internal
157import Hakyll.Web.Template.Internal
158+
159+
160+--------------------------------------------------------------------------------
161+import Data.FileEmbed (embedFile)
162+import qualified Data.Text as T
163+import qualified Data.Text.Encoding as T
164+import Language.Haskell.TH (Exp, Q)
165+
166+
167+--------------------------------------------------------------------------------
168+-- | Embed template allows you embed a template within the Haskell binary.
169+-- Example:
170+--
171+-- > myTemplate :: Template
172+-- > myTemplate = $(embedTemplate "test.html")
173+embedTemplate :: FilePath -> Q Exp
174+embedTemplate filePath = [|
175+ let source = T.unpack $ T.decodeUtf8 $(embedFile filePath) in
176+ case parseTemplateElemsFile filePath source of
177+ Left err -> error err
178+ Right tpl -> template filePath tpl |]
lib/Hakyll/Web/Template/Context.hs+121−31
@@ -1,6 +1,27 @@
1+-- | This module provides 'Context's which are used to expand expressions in
2+-- templates and allow for arbitrary customisation.
3+--
4+-- 'Template's define a small expression DSL which consists of strings,
5+-- identifiers and function application. There is no type system, every value is
6+-- a string and on the top level they get substituted verbatim into the page.
7+--
8+-- For example, you can build a context that contains
9+--
10+-- > … <> functionField "concat" (const . concat) <> …
11+--
12+-- which will allow you to use the @concat@ identifier as a function that takes
13+-- arbitrarily many stings and concatenates them to a new string:
14+--
15+-- > $partial(concat("templates/categories/", category))$
16+--
17+-- This will evaluate the @category@ field in the context, then prepend he path,
18+-- and include the referenced file as a template.
19+
20+
1--------------------------------------------------------------------------------
21--------------------------------------------------------------------------------
2{-# LANGUAGE CPP #-}
22{-# LANGUAGE CPP #-}
3{-# LANGUAGE ExistentialQuantification #-}
23{-# LANGUAGE ExistentialQuantification #-}
24+{-# LANGUAGE RankNTypes #-}
4module Hakyll.Web.Template.Context
25module Hakyll.Web.Template.Context
5 ( ContextField (..)
26 ( ContextField (..)
6 , Context (..)
27 , Context (..)
@@ -28,13 +49,14 @@ module Hakyll.Web.Template.Context
28 , teaserField
49 , teaserField
29 , teaserFieldWithSeparator
50 , teaserFieldWithSeparator
30 , missingField
51 , missingField
52+ , bindItem
31 ) where
53 ) where
32
54
33
55
34--------------------------------------------------------------------------------
56--------------------------------------------------------------------------------
35import Control.Applicative (Alternative (..))
57import Control.Applicative (Alternative (..))
36import Control.Monad (msum)
58import Control.Monad (msum)
37−import Data.List (intercalate)
59+import Data.List (intercalate, tails)
38#if MIN_VERSION_base(4,9,0)
60#if MIN_VERSION_base(4,9,0)
39import Data.Semigroup (Semigroup (..))
61import Data.Semigroup (Semigroup (..))
40#endif
62#endif
@@ -50,13 +72,16 @@ import Hakyll.Core.Metadata
50import Hakyll.Core.Provider
72import Hakyll.Core.Provider
51import Hakyll.Core.Util.String (needlePrefix, splitAll)
73import Hakyll.Core.Util.String (needlePrefix, splitAll)
52import Hakyll.Web.Html
74import Hakyll.Web.Html
75+import Prelude hiding (id)
53−import System.FilePath (splitDirectories, takeBaseName, dropExtension)
76+import System.FilePath (dropExtension, splitDirectories,
77+ takeBaseName)
54
78
55
79
56--------------------------------------------------------------------------------
80--------------------------------------------------------------------------------
57-- | Mostly for internal usage
81-- | Mostly for internal usage
58data ContextField
82data ContextField
83+ = EmptyField
59− = StringField String
84+ | StringField String
60 | forall a. ListField (Context a) [Item a]
85 | forall a. ListField (Context a) [Item a]
61
86
62
87
@@ -81,6 +106,8 @@ newtype Context a = Context
81
106
82
107
83--------------------------------------------------------------------------------
108--------------------------------------------------------------------------------
109+-- | Tries to find a key in the left context,
110+-- or when that fails in the right context.
84#if MIN_VERSION_base(4,9,0)
111#if MIN_VERSION_base(4,9,0)
85instance Semigroup (Context a) where
112instance Semigroup (Context a) where
86 (<>) (Context f) (Context g) = Context $ \k a i -> f k a i <|> g k a i
113 (<>) (Context f) (Context g) = Context $ \k a i -> f k a i <|> g k a i
@@ -97,64 +124,105 @@ instance Monoid (Context a) where
97
124
98--------------------------------------------------------------------------------
125--------------------------------------------------------------------------------
99field' :: String -> (Item a -> Compiler ContextField) -> Context a
126field' :: String -> (Item a -> Compiler ContextField) -> Context a
100−field' key value = Context $ \k _ i -> if k == key then value i else empty
127+field' key value = Context $ \k _ i ->
128+ if k == key
129+ then value i
130+ else noResult $ "Tried field " ++ key
101
131
102
132
103--------------------------------------------------------------------------------
133--------------------------------------------------------------------------------
104−-- | Constructs a new field in the 'Context.'
134+-- | Constructs a new field for a 'Context'.
135+-- If the key matches, the compiler is run and its result is substituted in the
136+-- template.
137+--
138+-- If the compiler fails, the field will be considered non-existent
139+-- in an @$if()$@ macro or ultimately break the template application
140+-- (unless the key is found in another context when using '<>').
141+-- Use 'empty' or 'noResult' for intentional failures of fields used in
142+-- @$if()$@, to distinguish them from exceptions thrown with 'fail'.
105field
143field
106 :: String -- ^ Key
144 :: String -- ^ Key
107 -> (Item a -> Compiler String) -- ^ Function that constructs a value based
145 -> (Item a -> Compiler String) -- ^ Function that constructs a value based
108− -- on the item
146+ -- on the item (e.g. accessing metadata)
109 -> Context a
147 -> Context a
110field key value = field' key (fmap StringField . value)
148field key value = field' key (fmap StringField . value)
111
149
112
150
113--------------------------------------------------------------------------------
151--------------------------------------------------------------------------------
114-- | Creates a 'field' to use with the @$if()$@ template macro.
152-- | Creates a 'field' to use with the @$if()$@ template macro.
153+-- Attempting to substitute the field into the template will cause an error.
115boolField
154boolField
116 :: String
155 :: String
117 -> (Item a -> Bool)
156 -> (Item a -> Bool)
118 -> Context a
157 -> Context a
119−boolField name f = field name (\i -> if f i
158+boolField name f = field' name (\i -> if f i
120− then pure (error $ unwords ["no string value for bool field:",name])
159+ then return EmptyField
121− else empty)
160+ else noResult $ "Field " ++ name ++ " is false")
122
161
123
162
124--------------------------------------------------------------------------------
163--------------------------------------------------------------------------------
125−-- | Creates a 'field' that does not depend on the 'Item'
164+-- | Creates a 'field' that does not depend on the 'Item' but always yields
165+-- the same string
126−constField :: String -> String -> Context a
166+constField :: String -- ^ Key
167+ -> String -- ^ Value
168+ -> Context a
127constField key = field key . const . return
169constField key = field key . const . return
128
170
129
171
130--------------------------------------------------------------------------------
172--------------------------------------------------------------------------------
173+-- | Creates a list field to be consumed by a @$for(…)$@ expression.
174+-- The compiler returns multiple items which are rendered in the loop body
175+-- with the supplied context.
131listField :: String -> Context a -> Compiler [Item a] -> Context b
176listField :: String -> Context a -> Compiler [Item a] -> Context b
132listField key c xs = listFieldWith key c (const xs)
177listField key c xs = listFieldWith key c (const xs)
133
178
134
179
135--------------------------------------------------------------------------------
180--------------------------------------------------------------------------------
181+-- | Produces a new 'Context' which has list field 'key'. All fields from
182+-- 'c' are also accessible from the produced context.
183+-- Be careful when doing @listFieldWith k ca f <> cb@ as any fields in @ca@
184+-- will override fields in @cb@ with the same name.
185+-- Creates a list field like 'listField', but supplies the current page
186+-- to the compiler.
136listFieldWith
187listFieldWith
137 :: String -> Context a -> (Item b -> Compiler [Item a]) -> Context b
188 :: String -> Context a -> (Item b -> Compiler [Item a]) -> Context b
138listFieldWith key c f = field' key $ fmap (ListField c) . f
189listFieldWith key c f = field' key $ fmap (ListField c) . f
139
190
140
191
141--------------------------------------------------------------------------------
192--------------------------------------------------------------------------------
193+-- | Creates a variadic function field.
194+--
195+-- The function will be called with the dynamically evaluated string arguments
196+-- from the template as well as the page that is currently rendered.
197+functionField :: String -- ^ Key
142−functionField :: String -> ([String] -> Item a -> Compiler String) -> Context a
198+ -> ([String] -> Item a -> Compiler String) -- ^ Function
199+ -> Context a
143functionField name value = Context $ \k args i ->
200functionField name value = Context $ \k args i ->
144 if k == name
201 if k == name
145 then StringField <$> value args i
202 then StringField <$> value args i
146− else empty
203+ else noResult $ "Tried function field " ++ name
147
204
148
205
149--------------------------------------------------------------------------------
206--------------------------------------------------------------------------------
207+-- | Transform the respective string results of all fields in a context.
208+-- For example,
209+--
210+-- > mapContext (++"c") (constField "x" "a" <> constField "y" "b")
211+--
212+-- is equivalent to
213+--
214+-- > constField "x" "ac" <> constField "y" "bc"
215+--
150mapContext :: (String -> String) -> Context a -> Context a
216mapContext :: (String -> String) -> Context a -> Context a
151mapContext f (Context c) = Context $ \k a i -> do
217mapContext f (Context c) = Context $ \k a i -> do
152 fld <- c k a i
218 fld <- c k a i
153 case fld of
219 case fld of
220+ EmptyField -> wrongType "boolField"
154 StringField str -> return $ StringField (f str)
221 StringField str -> return $ StringField (f str)
155− ListField _ _ -> fail $
222+ _ -> wrongType "ListField"
223+ where
156− "Hakyll.Web.Template.Context.mapContext: " ++
224+ wrongType typ = fail $ "Hakyll.Web.Template.Context.mapContext: " ++
157− "can't map over a ListField!"
225+ "can't map over a " ++ typ ++ "!"
158
226
159--------------------------------------------------------------------------------
227--------------------------------------------------------------------------------
160-- | A context that allows snippet inclusion. In processed file, use as:
228-- | A context that allows snippet inclusion. In processed file, use as:
@@ -163,15 +231,15 @@ mapContext f (Context c) = Context $ \k a i -> do
163-- > $snippet("path/to/snippet/")$
231-- > $snippet("path/to/snippet/")$
164-- > ...
232-- > ...
165--
233--
166−-- The contents of the included file will not be interpolated.
234+-- The contents of the included file will not be interpolated like @partial@
235+-- does it.
167--
236--
168snippetField :: Context String
237snippetField :: Context String
169snippetField = functionField "snippet" f
238snippetField = functionField "snippet" f
170 where
239 where
171 f [contentsPath] _ = loadBody (fromFilePath contentsPath)
240 f [contentsPath] _ = loadBody (fromFilePath contentsPath)
172− f _ i = error $
241+ f [] _ = fail "No argument to function 'snippet()'"
173− "Too many arguments to function 'snippet()' in item " ++
242+ f _ _ = fail "Too many arguments to function 'snippet()'"
174− show (itemIdentifier i)
175
243
176--------------------------------------------------------------------------------
244--------------------------------------------------------------------------------
177-- | A context that contains (in that order)
245-- | A context that contains (in that order)
@@ -191,8 +259,7 @@ defaultContext =
191 metadataField `mappend`
259 metadataField `mappend`
192 urlField "url" `mappend`
260 urlField "url" `mappend`
193 pathField "path" `mappend`
261 pathField "path" `mappend`
194− titleField "title" `mappend`
262+ titleField "title"
195− missingField
196
263
197
264
198--------------------------------------------------------------------------------
265--------------------------------------------------------------------------------
@@ -210,15 +277,20 @@ bodyField key = field key $ return . itemBody
210-- | Map any field to its metadata value, if present
277-- | Map any field to its metadata value, if present
211metadataField :: Context a
278metadataField :: Context a
212metadataField = Context $ \k _ i -> do
279metadataField = Context $ \k _ i -> do
213− value <- getMetadataField (itemIdentifier i) k
280+ let id = itemIdentifier i
281+ empty' = noResult $ "No '" ++ k ++ "' field in metadata " ++
282+ "of item " ++ show id
283+ value <- getMetadataField id k
214− maybe empty (return . StringField) value
284+ maybe empty' (return . StringField) value
215
285
216
286
217--------------------------------------------------------------------------------
287--------------------------------------------------------------------------------
218-- | Absolute url to the resulting item
288-- | Absolute url to the resulting item
219urlField :: String -> Context a
289urlField :: String -> Context a
220−urlField key = field key $
290+urlField key = field key $ \i -> do
291+ let id = itemIdentifier i
292+ empty' = fail $ "No route url found for item " ++ show id
221− fmap (maybe empty toUrl) . getRoute . itemIdentifier
293+ fmap (maybe empty' toUrl) $ getRoute id
222
294
223
295
224--------------------------------------------------------------------------------
296--------------------------------------------------------------------------------
@@ -269,6 +341,12 @@ titleField = mapContext takeBaseName . pathField
269-- that begins with @yyyy-mm-dd@ . For example:
341-- that begins with @yyyy-mm-dd@ . For example:
270-- @folder//yyyy-mm-dd-title//dist//main.extension@ .
342-- @folder//yyyy-mm-dd-title//dist//main.extension@ .
271-- In case of multiple matches, the rightmost one is used.
343-- In case of multiple matches, the rightmost one is used.
344+--
345+-- As another alternative, if none of the above matches, and the file has a
346+-- path which contains nested directories specifying a date, then that date
347+-- will be used. In other words, if the path is of the form
348+-- @**//yyyy//mm//dd//**//main.extension@ .
349+-- As above, in case of multiple matches, the rightmost one is used.
272
350
273dateField :: String -- ^ Key in which the rendered date should be placed
351dateField :: String -- ^ Key in which the rendered date should be placed
274 -> String -- ^ Format to use on the date
352 -> String -- ^ Format to use on the date
@@ -279,7 +357,7 @@ dateField = dateFieldWith defaultTimeLocale
279--------------------------------------------------------------------------------
357--------------------------------------------------------------------------------
280-- | This is an extended version of 'dateField' that allows you to
358-- | This is an extended version of 'dateField' that allows you to
281-- specify a time locale that is used for outputting the date. For more
359-- specify a time locale that is used for outputting the date. For more
282−-- details, see 'dateField'.
360+-- details, see 'dateField' and 'formatTime'.
283dateFieldWith :: TimeLocale -- ^ Output time locale
361dateFieldWith :: TimeLocale -- ^ Output time locale
284 -> String -- ^ Destination key
362 -> String -- ^ Destination key
285 -> String -- ^ Format to use on the date
363 -> String -- ^ Format to use on the date
@@ -305,7 +383,8 @@ getItemUTC locale id' = do
305 maybe empty' return $ msum $
383 maybe empty' return $ msum $
306 [tryField "published" fmt | fmt <- formats] ++
384 [tryField "published" fmt | fmt <- formats] ++
307 [tryField "date" fmt | fmt <- formats] ++
385 [tryField "date" fmt | fmt <- formats] ++
308− [parseTime' "%Y-%m-%d" $ intercalate "-" $ take 3 $ splitAll "-" fnCand | fnCand <- reverse paths]
386+ [parseTime' "%Y-%m-%d" $ intercalate "-" $ take 3 $ splitAll "-" fnCand | fnCand <- reverse paths] ++
387+ [parseTime' "%Y-%m-%d" $ intercalate "-" $ fnCand | fnCand <- map (take 3) $ reverse . tails $ paths]
309 where
388 where
310 empty' = fail $ "Hakyll.Web.Template.Context.getItemUTC: " ++
389 empty' = fail $ "Hakyll.Web.Template.Context.getItemUTC: " ++
311 "could not parse time for " ++ show id'
390 "could not parse time for " ++ show id'
@@ -333,6 +412,7 @@ getItemModificationTime identifier = do
333
412
334
413
335--------------------------------------------------------------------------------
414--------------------------------------------------------------------------------
415+-- | Creates a field with the last modification date of the underlying item.
336modificationTimeField :: String -- ^ Key
416modificationTimeField :: String -- ^ Key
337 -> String -- ^ Format
417 -> String -- ^ Format
338 -> Context a -- ^ Resulting context
418 -> Context a -- ^ Resulting context
@@ -340,6 +420,8 @@ modificationTimeField = modificationTimeFieldWith defaultTimeLocale
340
420
341
421
342--------------------------------------------------------------------------------
422--------------------------------------------------------------------------------
423+-- | Creates a field with the last modification date of the underlying item
424+-- in a custom localisation format (see 'formatTime').
343modificationTimeFieldWith :: TimeLocale -- ^ Time output locale
425modificationTimeFieldWith :: TimeLocale -- ^ Time output locale
344 -> String -- ^ Key
426 -> String -- ^ Key
345 -> String -- ^ Format
427 -> String -- ^ Format
@@ -378,10 +460,11 @@ teaserFieldWithSeparator separator key snapshot = field key $ \item -> do
378
460
379
461
380--------------------------------------------------------------------------------
462--------------------------------------------------------------------------------
463+-- | Constantly reports any field as missing. Mostly for internal usage,
464+-- it is the last choice in every context used in a template application.
381missingField :: Context a
465missingField :: Context a
382−missingField = Context $ \k _ i -> fail $
466+missingField = Context $ \k _ _ -> noResult $
383− "Missing field $" ++ k ++ "$ in context for item " ++
467+ "Missing field '" ++ k ++ "' in context"
384− show (itemIdentifier i)
385
468
386parseTimeM :: Bool -> TimeLocale -> String -> String -> Maybe UTCTime
469parseTimeM :: Bool -> TimeLocale -> String -> String -> Maybe UTCTime
387#if MIN_VERSION_time(1,5,0)
470#if MIN_VERSION_time(1,5,0)
@@ -389,3 +472,10 @@ parseTimeM = TF.parseTimeM
389#else
472#else
390parseTimeM _ = TF.parseTime
473parseTimeM _ = TF.parseTime
391#endif
474#endif
475+
476+--------------------------------------------------------------------------------
477+
478+-- | Binds an 'Item' to a given 'Context', allowing it to be combined with any
479+-- other 'Context' of any type.
480+bindItem :: Context a -> Item a -> forall b. Context b
481+bindItem (Context ctx) ia = Context $ \k args _ -> ctx k args ia
lib/Hakyll/Web/Template/Internal.hs+103−44
@@ -1,15 +1,18 @@
1+{-# LANGUAGE DeriveAnyClass #-}
2+{-# LANGUAGE DeriveGeneric #-}
1{-# LANGUAGE GeneralizedNewtypeDeriving #-}
3{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2{-# LANGUAGE ScopedTypeVariables #-}
4{-# LANGUAGE ScopedTypeVariables #-}
5+
3module Hakyll.Web.Template.Internal
6module Hakyll.Web.Template.Internal
4 ( Template (..)
7 ( Template (..)
5 , template
8 , template
6 , templateBodyCompiler
9 , templateBodyCompiler
7 , templateCompiler
10 , templateCompiler
8 , applyTemplate
11 , applyTemplate
9− , applyTemplate'
10 , loadAndApplyTemplate
12 , loadAndApplyTemplate
11 , applyAsTemplate
13 , applyAsTemplate
12 , readTemplate
14 , readTemplate
15+ , compileTemplateItem
13 , unsafeReadTemplateFile
16 , unsafeReadTemplateFile
14
17
15 , module Hakyll.Web.Template.Internal.Element
18 , module Hakyll.Web.Template.Internal.Element
@@ -18,16 +21,19 @@ module Hakyll.Web.Template.Internal
18
21
19
22
20--------------------------------------------------------------------------------
23--------------------------------------------------------------------------------
21−import Control.Monad.Except (MonadError (..))
24+import Control.Monad (forM)
25+import Control.Monad.Except (MonadError (..), catchError)
22import Data.Binary (Binary)
26import Data.Binary (Binary)
23import Data.List (intercalate)
27import Data.List (intercalate)
28+import qualified Data.List.NonEmpty as NonEmpty
24import Data.Typeable (Typeable)
29import Data.Typeable (Typeable)
25import GHC.Exts (IsString (..))
30import GHC.Exts (IsString (..))
26−import Prelude hiding (id)
31+import GHC.Generics (Generic)
27
32
28
33
29--------------------------------------------------------------------------------
34--------------------------------------------------------------------------------
30import Hakyll.Core.Compiler
35import Hakyll.Core.Compiler
36+import Hakyll.Core.Compiler.Internal
31import Hakyll.Core.Identifier
37import Hakyll.Core.Identifier
32import Hakyll.Core.Item
38import Hakyll.Core.Item
33import Hakyll.Core.Writable
39import Hakyll.Core.Writable
@@ -38,9 +44,10 @@ import Hakyll.Web.Template.Internal.Trim
38
44
39--------------------------------------------------------------------------------
45--------------------------------------------------------------------------------
40-- | Datatype used for template substitutions.
46-- | Datatype used for template substitutions.
41−newtype Template = Template
47+data Template = Template
42− { unTemplate :: [TemplateElement]
48+ { tplElements :: [TemplateElement]
49+ , tplOrigin :: FilePath -- Only for error messages.
43− } deriving (Show, Eq, Binary, Typeable)
50+ } deriving (Show, Eq, Generic, Binary, Typeable)
44
51
45
52
46--------------------------------------------------------------------------------
53--------------------------------------------------------------------------------
@@ -56,39 +63,68 @@ instance IsString Template where
56
63
57--------------------------------------------------------------------------------
64--------------------------------------------------------------------------------
58-- | Wrap the constructor to ensure trim is called.
65-- | Wrap the constructor to ensure trim is called.
59−template :: [TemplateElement] -> Template
66+template :: FilePath -> [TemplateElement] -> Template
60−template = Template . trim
67+template p = flip Template p . trim
61
68
62
69
63--------------------------------------------------------------------------------
70--------------------------------------------------------------------------------
71+-- | Parse a string into a template.
72+-- You should prefer 'compileTemplateItem' over this.
64readTemplate :: String -> Template
73readTemplate :: String -> Template
65−readTemplate = Template . trim . readTemplateElems
74+readTemplate = either error (template origin) . parseTemplateElemsFile origin
75+ where
76+ origin = "{literal}"
77+{-# DEPRECATED readTemplate "Use templateCompiler instead" #-}
78+
79+--------------------------------------------------------------------------------
80+-- | Parse an item body into a template.
81+-- Provides useful error messages in the 'Compiler' monad.
82+compileTemplateItem :: Item String -> Compiler Template
83+compileTemplateItem item = let file = itemIdentifier item
84+ in compileTemplateFile file (itemBody item)
85+
86+--------------------------------------------------------------------------------
87+compileTemplateFile :: Identifier -> String -> Compiler Template
88+compileTemplateFile file = either fail (return . template origin)
89+ . parseTemplateElemsFile origin
90+ where
91+ origin = show file
66
92
67--------------------------------------------------------------------------------
93--------------------------------------------------------------------------------
68-- | Read a template, without metadata header
94-- | Read a template, without metadata header
69templateBodyCompiler :: Compiler (Item Template)
95templateBodyCompiler :: Compiler (Item Template)
70templateBodyCompiler = cached "Hakyll.Web.Template.templateBodyCompiler" $ do
96templateBodyCompiler = cached "Hakyll.Web.Template.templateBodyCompiler" $ do
71 item <- getResourceBody
97 item <- getResourceBody
72− file <- getResourceFilePath
98+ file <- getUnderlying
73− return $ fmap (template . readTemplateElemsFile file) item
99+ withItemBody (compileTemplateFile file) item
74
100
75--------------------------------------------------------------------------------
101--------------------------------------------------------------------------------
76-- | Read complete file contents as a template
102-- | Read complete file contents as a template
77templateCompiler :: Compiler (Item Template)
103templateCompiler :: Compiler (Item Template)
78templateCompiler = cached "Hakyll.Web.Template.templateCompiler" $ do
104templateCompiler = cached "Hakyll.Web.Template.templateCompiler" $ do
79 item <- getResourceString
105 item <- getResourceString
80− file <- getResourceFilePath
106+ file <- getUnderlying
81− return $ fmap (template . readTemplateElemsFile file) item
107+ withItemBody (compileTemplateFile file) item
82
108
83
109
84--------------------------------------------------------------------------------
110--------------------------------------------------------------------------------
111+-- | Interpolate template expressions from context values in a page
85applyTemplate :: Template -- ^ Template
112applyTemplate :: Template -- ^ Template
86 -> Context a -- ^ Context
113 -> Context a -- ^ Context
87 -> Item a -- ^ Page
114 -> Item a -- ^ Page
88 -> Compiler (Item String) -- ^ Resulting item
115 -> Compiler (Item String) -- ^ Resulting item
89applyTemplate tpl context item = do
116applyTemplate tpl context item = do
90− body <- applyTemplate' (unTemplate tpl) context item
117+ body <- applyTemplate' (tplElements tpl) context item `catchError` handler
91 return $ itemSetBody body item
118 return $ itemSetBody body item
119+ where
120+ tplName = tplOrigin tpl
121+ itemName = show $ itemIdentifier item
122+ handler es = fail $ "Hakyll.Web.Template.applyTemplate: Failed to " ++
123+ (if tplName == itemName
124+ then "interpolate template in item " ++ itemName
125+ else "apply template " ++ tplName ++ " to item " ++ itemName) ++
126+ ":\n" ++ intercalate ",\n" es
127+
92
128
93
129
94--------------------------------------------------------------------------------
130--------------------------------------------------------------------------------
@@ -105,9 +141,6 @@ applyTemplate' tes context x = go tes
105
141
106 go = fmap concat . mapM applyElem
142 go = fmap concat . mapM applyElem
107
143
108− trimError = error $ "Hakyll.Web.Template.applyTemplate: template not " ++
109− "fully trimmed."
110−
111 ---------------------------------------------------------------------------
144 ---------------------------------------------------------------------------
112
145
113 applyElem :: TemplateElement -> Compiler String
146 applyElem :: TemplateElement -> Compiler String
@@ -118,29 +151,43 @@ applyTemplate' tes context x = go tes
118
151
119 applyElem (Chunk c) = return c
152 applyElem (Chunk c) = return c
120
153
121− applyElem (Expr e) = applyExpr e >>= getString e
154+ applyElem (Expr e) = withErrorMessage evalMsg (applyStringExpr typeMsg e)
155+ where
156+ evalMsg = "In expr '$" ++ show e ++ "$'"
157+ typeMsg = "expr '$" ++ show e ++ "$'"
122
158
123 applyElem Escaped = return "$"
159 applyElem Escaped = return "$"
124
160
125− applyElem (If e t mf) = (applyExpr e >> go t) `catchError` handler
161+ applyElem (If e t mf) = compilerTry (applyExpr e) >>= handle
126 where
162 where
127− handler _ = case mf of
163+ f = maybe (return "") go mf
128− Nothing -> return ""
164+ handle (Right _) = go t
129− Just f -> go f
165+ handle (Left (CompilationNoResult _)) = f
130−
166+ handle (Left (CompilationFailure es)) = debug (NonEmpty.toList es) >> f
167+ debug = compilerDebugEntries ("Hakyll.Web.Template.applyTemplate: " ++
168+ "[ERROR] in 'if' condition on expr '" ++ show e ++ "':")
169+
131− applyElem (For e b s) = applyExpr e >>= \cf -> case cf of
170+ applyElem (For e b s) = withErrorMessage headMsg (applyExpr e) >>= \cf -> case cf of
132− StringField _ -> fail $
171+ EmptyField -> expected "list" "boolean" typeMsg
133− "Hakyll.Web.Template.applyTemplateWith: expected ListField but " ++
172+ StringField _ -> expected "list" "string" typeMsg
134− "got StringField for expr " ++ show e
135− ListField c xs -> do
173+ ListField c xs -> withErrorMessage bodyMsg $ do
136 sep <- maybe (return "") go s
174 sep <- maybe (return "") go s
137− bs <- mapM (applyTemplate' b c) xs
175+ bs <- forM xs $ applyTemplate' b $ c <> bindItem context x
138 return $ intercalate sep bs
176 return $ intercalate sep bs
139−
177+ where
178+ headMsg = "In expr '$for(" ++ show e ++ ")$'"
179+ typeMsg = "loop expr '" ++ show e ++ "'"
180+ bodyMsg = "In loop context of '$for(" ++ show e ++ ")$'"
181+
140− applyElem (Partial e) = do
182+ applyElem (Partial e) = withErrorMessage headMsg $
141− p <- applyExpr e >>= getString e
183+ applyStringExpr typeMsg e >>= \p ->
184+ withErrorMessage inclMsg $ do
142− Template tpl' <- loadBody (fromFilePath p)
185+ tpl' <- loadBody (fromFilePath p)
143− applyTemplate' tpl' context x
186+ itemBody <$> applyTemplate tpl' context x
187+ where
188+ headMsg = "In expr '$partial(" ++ show e ++ ")$'"
189+ typeMsg = "partial expr '" ++ show e ++ "'"
190+ inclMsg = "In inclusion of '$partial(" ++ show e ++ ")$'"
144
191
145 ---------------------------------------------------------------------------
192 ---------------------------------------------------------------------------
146
193
@@ -149,17 +196,29 @@ applyTemplate' tes context x = go tes
149 applyExpr (Ident (TemplateKey k)) = context' k [] x
196 applyExpr (Ident (TemplateKey k)) = context' k [] x
150
197
151 applyExpr (Call (TemplateKey k) args) = do
198 applyExpr (Call (TemplateKey k) args) = do
152− args' <- mapM (\e -> applyExpr e >>= getString e) args
199+ args' <- mapM (\e -> applyStringExpr (typeMsg e) e) args
153 context' k args' x
200 context' k args' x
201+ where
202+ typeMsg e = "argument '" ++ show e ++ "'"
154
203
155 applyExpr (StringLiteral s) = return (StringField s)
204 applyExpr (StringLiteral s) = return (StringField s)
156
205
157 ----------------------------------------------------------------------------
206 ----------------------------------------------------------------------------
158
207
208+ applyStringExpr :: String -> TemplateExpr -> Compiler String
209+ applyStringExpr msg expr =
210+ applyExpr expr >>= getString
211+ where
212+ getString EmptyField = expected "string" "boolean" msg
159− getString _ (StringField s) = return s
213+ getString (StringField s) = return s
160− getString e (ListField _ _) = fail $
214+ getString (ListField _ _) = expected "string" "list" msg
161− "Hakyll.Web.Template.applyTemplateWith: expected StringField but " ++
215+
162− "got ListField for expr " ++ show e
216+ expected typ act expr = fail $ unwords ["Hakyll.Web.Template.applyTemplate:",
217+ "expected", typ, "but got", act, "for", expr]
218+
219+ -- expected to never happen with all templates constructed by 'template'
220+ trimError = fail $
221+ "Hakyll.Web.Template.applyTemplate: template not fully trimmed."
163
222
164
223
165--------------------------------------------------------------------------------
224--------------------------------------------------------------------------------
@@ -189,14 +248,14 @@ loadAndApplyTemplate identifier context item = do
189applyAsTemplate :: Context String -- ^ Context
248applyAsTemplate :: Context String -- ^ Context
190 -> Item String -- ^ Item and template
249 -> Item String -- ^ Item and template
191 -> Compiler (Item String) -- ^ Resulting item
250 -> Compiler (Item String) -- ^ Resulting item
192−applyAsTemplate context item =
251+applyAsTemplate context item = do
193− let tpl = template $ readTemplateElemsFile file (itemBody item)
252+ tpl <- compileTemplateItem item
194− file = toFilePath $ itemIdentifier item
195− in applyTemplate tpl context item
253+ applyTemplate tpl context item
196
254
197
255
198--------------------------------------------------------------------------------
256--------------------------------------------------------------------------------
199unsafeReadTemplateFile :: FilePath -> Compiler Template
257unsafeReadTemplateFile :: FilePath -> Compiler Template
200unsafeReadTemplateFile file = do
258unsafeReadTemplateFile file = do
201 tpl <- unsafeCompiler $ readFile file
259 tpl <- unsafeCompiler $ readFile file
202− pure $ template $ readTemplateElemsFile file tpl
260+ compileTemplateFile (fromFilePath file) tpl
261+{-# DEPRECATED unsafeReadTemplateFile "Use templateCompiler" #-}
lib/Hakyll/Web/Template/Internal/Element.hs+7−14
@@ -7,14 +7,14 @@ module Hakyll.Web.Template.Internal.Element
7 , TemplateExpr (..)
7 , TemplateExpr (..)
8 , TemplateElement (..)
8 , TemplateElement (..)
9 , templateElems
9 , templateElems
10− , readTemplateElems
10+ , parseTemplateElemsFile
11− , readTemplateElemsFile
12 ) where
11 ) where
13
12
14
13
15--------------------------------------------------------------------------------
14--------------------------------------------------------------------------------
16−import Control.Applicative ((<|>))
15+import Control.Applicative ((<|>), (<*))
17import Control.Monad (void)
16import Control.Monad (void)
17+import Control.Arrow (left)
18import Data.Binary (Binary, get, getWord8, put, putWord8)
18import Data.Binary (Binary, get, getWord8, put, putWord8)
19import Data.List (intercalate)
19import Data.List (intercalate)
20import Data.Maybe (isJust)
20import Data.Maybe (isJust)
@@ -107,17 +107,10 @@ instance Binary TemplateExpr where
107 2 -> StringLiteral <$> get
107 2 -> StringLiteral <$> get
108 _ -> error "Hakyll.Web.Template.Internal: Error reading cached template"
108 _ -> error "Hakyll.Web.Template.Internal: Error reading cached template"
109
109
110−
111−--------------------------------------------------------------------------------
112−readTemplateElems :: String -> [TemplateElement]
113−readTemplateElems = readTemplateElemsFile "{literal}"
114−
115−
116--------------------------------------------------------------------------------
110--------------------------------------------------------------------------------
117−readTemplateElemsFile :: FilePath -> String -> [TemplateElement]
111+parseTemplateElemsFile :: FilePath -> String -> Either String [TemplateElement]
118−readTemplateElemsFile file input = case P.parse templateElems file input of
119− Left err -> error $ "Cannot parse template: " ++ show err
112+parseTemplateElemsFile file = left (\e -> "Cannot parse template " ++ show e)
120− Right t -> t
113+ . P.parse (templateElems <* P.eof) file
121
114
122
115
123--------------------------------------------------------------------------------
116--------------------------------------------------------------------------------
@@ -167,7 +160,7 @@ trimOpen = do
167--------------------------------------------------------------------------------
160--------------------------------------------------------------------------------
168trimClose :: P.Parser Bool
161trimClose :: P.Parser Bool
169trimClose = do
162trimClose = do
170− trimIfR <- P.optionMaybe $ P.try (P.char '-')
163+ trimIfR <- P.optionMaybe $ (P.char '-')
171 void $ P.char '$'
164 void $ P.char '$'
172 pure $ isJust trimIfR
165 pure $ isJust trimIfR
173
166
stack.yaml+9−10
@@ -1,4 +1,4 @@
1−resolver: nightly-2018-08-29
1+resolver: lts-13.29
2save-hackage-creds: false
2save-hackage-creds: false
3
3
4flags:
4flags:
@@ -23,12 +23,11 @@ build:
23 haddock-deps: false
23 haddock-deps: false
24
24
25extra-deps:
25extra-deps:
26−- file-embed-0.0.11
26+- 'cmark-gfm-0.2.0'
27−- QuickCheck-2.12.4
27+- 'hslua-module-system-0.2.1'
28−- yaml-0.10.1.1
28+- 'ipynb-0.1'
29−
29+- 'lrucache-1.2.0.1'
30−- pandoc-2.2.3.2
30+- 'pandoc-2.7.3'
31−- pandoc-citeproc-0.14.3.1
31+- 'pandoc-citeproc-0.16.2'
32−- HsYAML-0.1.1.2
32+- 'skylighting-0.8.2'
33−- aeson-1.3.1.1
33+- 'skylighting-core-0.8.2'
34−- rfc5051-0.1.0.3
tests/Hakyll/Core/UnixFilter/Tests.hs+6−14
@@ -6,18 +6,16 @@ module Hakyll.Core.UnixFilter.Tests
6
6
7
7
8--------------------------------------------------------------------------------
8--------------------------------------------------------------------------------
9−import Data.List (isInfixOf)
10−import Test.Tasty (TestTree, testGroup)
9+import Test.Tasty (TestTree, testGroup)
11−import Test.Tasty.HUnit (testCase)
10+import Test.Tasty.HUnit (testCase)
12−import qualified Test.Tasty.HUnit as H
11+import qualified Test.Tasty.HUnit as H
13
12
14
13
15--------------------------------------------------------------------------------
14--------------------------------------------------------------------------------
16import Hakyll.Core.Compiler
15import Hakyll.Core.Compiler
17−import Hakyll.Core.Compiler.Internal
16+import Hakyll.Core.Identifier
18import Hakyll.Core.Item
17import Hakyll.Core.Item
19import Hakyll.Core.UnixFilter
18import Hakyll.Core.UnixFilter
20−import Hakyll.Core.Identifier
21import TestSuite.Util
19import TestSuite.Util
22
20
23
21
@@ -51,10 +49,7 @@ unixFilterFalse :: H.Assertion
51unixFilterFalse = do
49unixFilterFalse = do
52 store <- newTestStore
50 store <- newTestStore
53 provider <- newTestProvider store
51 provider <- newTestProvider store
54− result <- testCompiler store provider testMarkdown compiler
52+ testCompilerError store provider testMarkdown compiler "exit code"
55− case result of
56− CompilerError es -> True H.@=? any ("exit code" `isInfixOf`) es
57− _ -> H.assertFailure "Expecting CompilerError"
58 cleanTestEnv
53 cleanTestEnv
59 where
54 where
60 compiler = getResourceString >>= withItemBody (unixFilter "false" [])
55 compiler = getResourceString >>= withItemBody (unixFilter "false" [])
@@ -65,10 +60,7 @@ unixFilterError :: H.Assertion
65unixFilterError = do
60unixFilterError = do
66 store <- newTestStore
61 store <- newTestStore
67 provider <- newTestProvider store
62 provider <- newTestProvider store
68− result <- testCompiler store provider testMarkdown compiler
63+ testCompilerError store provider testMarkdown compiler "option"
69− case result of
70− CompilerError es -> True H.@=? any ("invalid option" `isInfixOf`) es
71− _ -> H.assertFailure "Expecting CompilerError"
72 cleanTestEnv
64 cleanTestEnv
73 where
65 where
74 compiler = getResourceString >>= withItemBody (unixFilter "head" ["-#"])
66 compiler = getResourceString >>= withItemBody (unixFilter "head" ["-#"])
tests/Hakyll/Web/Template/Context/Tests.hs+36−3
@@ -13,16 +13,19 @@ import Test.Tasty.HUnit (Assertion, testCase, (@=?))
13--------------------------------------------------------------------------------
13--------------------------------------------------------------------------------
14import Hakyll.Core.Compiler
14import Hakyll.Core.Compiler
15import Hakyll.Core.Identifier
15import Hakyll.Core.Identifier
16+import Hakyll.Core.Item
16import Hakyll.Core.Provider
17import Hakyll.Core.Provider
17import Hakyll.Core.Store (Store)
18import Hakyll.Core.Store (Store)
18import Hakyll.Web.Template.Context
19import Hakyll.Web.Template.Context
20+import Hakyll.Web.Template.Internal
19import TestSuite.Util
21import TestSuite.Util
20
22
21
23
22--------------------------------------------------------------------------------
24--------------------------------------------------------------------------------
23tests :: TestTree
25tests :: TestTree
24−tests = testGroup "Hakyll.Core.Template.Context.Tests"
26+tests = testGroup "Hakyll.Web.Template.Context.Tests"
25 [ testCase "testDateField" testDateField
27 [ testCase "testDateField" testDateField
28+ , testCase "testOuerLoopContextAccess" testOuerLoopContextAccess
26 ]
29 ]
27
30
28
31
@@ -45,6 +48,11 @@ testDateField = do
45 "posts/2018-09-26.md" "date" $
48 "posts/2018-09-26.md" "date" $
46 dateField "date" "%B %e, %Y"
49 dateField "date" "%B %e, %Y"
47 date3 @=? "September 26, 2018"
50 date3 @=? "September 26, 2018"
51+
52+ date4 <- testContextDone store provider
53+ "posts/2019/05/10/tomorrow.md" "date" $
54+ dateField "date" "%B %e, %Y"
55+ date4 @=? "May 10, 2019"
48 cleanTestEnv
56 cleanTestEnv
49
57
50
58
@@ -57,6 +65,31 @@ testContextDone store provider identifier key context =
57 cf <- unContext context key [] item
65 cf <- unContext context key [] item
58 case cf of
66 case cf of
59 StringField str -> return str
67 StringField str -> return str
60− ListField _ _ -> error $
68+ _ -> error $
61 "Hakyll.Web.Template.Context.Tests.testContextDone: " ++
69 "Hakyll.Web.Template.Context.Tests.testContextDone: " ++
62− "Didn't expect ListField"
70+ "expected StringField"
71+
72+--------------------------------------------------------------------------------
73+
74+testOuerLoopContextAccess :: Assertion
75+testOuerLoopContextAccess = do
76+ store <- newTestStore
77+ provider <- newTestProvider store
78+ test store provider ctx "baz"
79+ test store provider (ctx' <> ctx) "not baz"
80+ test store provider (ctx <> ctx') "baz"
81+
82+ cleanTestEnv
83+ where
84+ tpl = readTemplate "$for(foo)$$for(bar)$$qux$$endfor$$endfor$"
85+ ctx = mconcat [
86+ field "qux" $ const $ return "baz"
87+ , listField "foo" (listField "bar" mempty $ return [mockItem])
88+ $ return [mockItem]
89+ ]
90+ ctx' = field "qux" $ const $ return "not baz"
91+ mockItem = Item "" ()
92+ test store provider context str = do
93+ str' <- testCompilerDone store provider ""
94+ $ applyTemplate tpl context mockItem
95+ str @=? itemBody str'
tests/Hakyll/Web/Template/Tests.hs+49−17
@@ -1,5 +1,6 @@
1--------------------------------------------------------------------------------
1--------------------------------------------------------------------------------
2{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE TemplateHaskell #-}
3module Hakyll.Web.Template.Tests
4module Hakyll.Web.Template.Tests
4 ( tests
5 ( tests
5 ) where
6 ) where
@@ -7,9 +8,10 @@ module Hakyll.Web.Template.Tests
7
8
8--------------------------------------------------------------------------------
9--------------------------------------------------------------------------------
9import Test.Tasty (TestTree, testGroup)
10import Test.Tasty (TestTree, testGroup)
10−import Test.Tasty.HUnit (Assertion, testCase, (@=?),
11+import Test.Tasty.HUnit (Assertion, assertBool, testCase,
11− (@?=))
12+ (@=?), (@?=))
12
13
14+import Data.Either (isLeft)
13
15
14--------------------------------------------------------------------------------
16--------------------------------------------------------------------------------
15import Hakyll.Core.Compiler
17import Hakyll.Core.Compiler
@@ -25,20 +27,20 @@ import TestSuite.Util
25
27
26--------------------------------------------------------------------------------
28--------------------------------------------------------------------------------
27tests :: TestTree
29tests :: TestTree
28−tests = testGroup "Hakyll.Core.Template.Tests" $ concat
30+tests = testGroup "Hakyll.Web.Template.Tests" $ concat
29 [ [ testCase "case01" $ test ("template.html.out", "template.html", "example.md")
31 [ [ testCase "case01" $ test ("template.html.out", "template.html", "example.md")
30 , testCase "case02" $ test ("strip.html.out", "strip.html", "example.md")
32 , testCase "case02" $ test ("strip.html.out", "strip.html", "example.md")
31 , testCase "case03" $ test ("just-meta.html.out", "just-meta.html", "example.md")
33 , testCase "case03" $ test ("just-meta.html.out", "just-meta.html", "example.md")
32 , testCase "applyJoinTemplateList" testApplyJoinTemplateList
34 , testCase "applyJoinTemplateList" testApplyJoinTemplateList
33 ]
35 ]
34
36
35− , fromAssertions "readTemplate"
37+ , fromAssertions "parseTemplate"
36− [ [Chunk "Hello ", Expr (Call "guest" [])]
38+ [ Right [Chunk "Hello ", Expr (Call "guest" [])]
37− @=? readTemplateElems "Hello $guest()$"
39+ @=? parse "Hello $guest()$"
38− , [If (Call "a" [StringLiteral "bar"]) [Chunk "foo"] Nothing]
40+ , Right [If (Call "a" [StringLiteral "bar"]) [Chunk "foo"] Nothing]
39− @=? readTemplateElems "$if(a(\"bar\"))$foo$endif$"
41+ @=? parse "$if(a(\"bar\"))$foo$endif$"
40 -- 'If' trim check.
42 -- 'If' trim check.
41− , [ TrimL
43+ , Right [ TrimL
42 , If (Ident (TemplateKey "body"))
44 , If (Ident (TemplateKey "body"))
43 [ TrimR
45 [ TrimR
44 , Chunk "\n"
46 , Chunk "\n"
@@ -54,29 +56,39 @@ tests = testGroup "Hakyll.Core.Template.Tests" $ concat
54 ])
56 ])
55 , TrimR
57 , TrimR
56 ]
58 ]
57− @=? readTemplateElems "$-if(body)-$\n$body$\n$-else-$\n$body$\n$-endif-$"
59+ @=? parse "$-if(body)-$\n$body$\n$-else-$\n$body$\n$-endif-$"
58 -- 'For' trim check.
60 -- 'For' trim check.
59− , [ TrimL
61+ , Right [ TrimL
60 , For (Ident (TemplateKey "authors"))
62 , For (Ident (TemplateKey "authors"))
61 [TrimR, Chunk "\n body \n", TrimL]
63 [TrimR, Chunk "\n body \n", TrimL]
62 Nothing
64 Nothing
63 , TrimR
65 , TrimR
64 ]
66 ]
65− @=? readTemplateElems "$-for(authors)-$\n body \n$-endfor-$"
67+ @=? parse "$-for(authors)-$\n body \n$-endfor-$"
66 -- 'Partial' trim check.
68 -- 'Partial' trim check.
67− , [ TrimL
69+ , Right [ TrimL
68 , Partial (StringLiteral "path")
70 , Partial (StringLiteral "path")
69 , TrimR
71 , TrimR
70 ]
72 ]
71− @=? readTemplateElems "$-partial(\"path\")-$"
73+ @=? parse "$-partial(\"path\")-$"
72 -- 'Expr' trim check.
74 -- 'Expr' trim check.
73− , [ TrimL
75+ , Right [ TrimL
74 , Expr (Ident (TemplateKey "foo"))
76 , Expr (Ident (TemplateKey "foo"))
75 , TrimR
77 , TrimR
76 ]
78 ]
77− @=? readTemplateElems "$-foo-$"
79+ @=? parse "$-foo-$"
80+ -- fail on incomplete template.
81+ , assertBool "did not yield error" $ isLeft $
82+ parse "a$b"
83+ -- fail on mismatched template syntax.
84+ , assertBool "did not fail to parse" $ isLeft $
85+ parse "$for(xs)$\n <p>foo</p>\n$endif$"
78 ]
86 ]
87+
88+ , [testCase "embeddedTemplate" testEmbeddedTemplate]
79 ]
89 ]
90+ where
91+ parse = parseTemplateElemsFile ""
80
92
81
93
82--------------------------------------------------------------------------------
94--------------------------------------------------------------------------------
@@ -113,6 +125,8 @@ testApplyJoinTemplateList :: Assertion
113testApplyJoinTemplateList = do
125testApplyJoinTemplateList = do
114 store <- newTestStore
126 store <- newTestStore
115 provider <- newTestProvider store
127 provider <- newTestProvider store
128+ tpl <- testCompilerDone store provider "tpl" $
129+ compileTemplateItem (Item "tpl" "<b>$body$</b>")
116 str <- testCompilerDone store provider "item3" $
130 str <- testCompilerDone store provider "item3" $
117 applyJoinTemplateList ", " tpl defaultContext [i1, i2]
131 applyJoinTemplateList ", " tpl defaultContext [i1, i2]
118
132
@@ -121,4 +135,22 @@ testApplyJoinTemplateList = do
121 where
135 where
122 i1 = Item "item1" "Hello"
136 i1 = Item "item1" "Hello"
123 i2 = Item "item2" "World"
137 i2 = Item "item2" "World"
124− tpl = readTemplate "<b>$body$</b>"
138+
139+
140+--------------------------------------------------------------------------------
141+embeddedTemplate :: Template
142+embeddedTemplate = $(embedTemplate "tests/data/embed.html")
143+
144+--------------------------------------------------------------------------------
145+testEmbeddedTemplate :: Assertion
146+testEmbeddedTemplate = do
147+ store <- newTestStore
148+ provider <- newTestProvider store
149+ str <- testCompilerDone store provider "item3" $
150+ applyTemplate embeddedTemplate defaultContext item
151+
152+ itemBody str @?= "<p>Hello, world</p>\n"
153+ cleanTestEnv
154+ where
155+ item = Item "item1" "Hello, world"
156+
tests/TestSuite.hs+2
@@ -28,6 +28,7 @@ import qualified Hakyll.Web.Pandoc.FileType.Tests
28#endif
28#endif
29import qualified Hakyll.Web.Template.Context.Tests
29import qualified Hakyll.Web.Template.Context.Tests
30import qualified Hakyll.Web.Template.Tests
30import qualified Hakyll.Web.Template.Tests
31+import qualified Hakyll.Web.Tags.Tests
31
32
32
33
33--------------------------------------------------------------------------------
34--------------------------------------------------------------------------------
@@ -49,6 +50,7 @@ main = defaultMain $ testGroup "Hakyll"
49#ifdef USE_PANDOC
50#ifdef USE_PANDOC
50 , Hakyll.Web.Pandoc.FileType.Tests.tests
51 , Hakyll.Web.Pandoc.FileType.Tests.tests
51#endif
52#endif
53+ , Hakyll.Web.Tags.Tests.tests
52 , Hakyll.Web.Template.Context.Tests.tests
54 , Hakyll.Web.Template.Context.Tests.tests
53 , Hakyll.Web.Template.Tests.tests
55 , Hakyll.Web.Template.Tests.tests
54 ]
56 ]
tests/TestSuite/Util.hs+11−2
@@ -6,6 +6,7 @@ module TestSuite.Util
6 , newTestProvider
6 , newTestProvider
7 , testCompiler
7 , testCompiler
8 , testCompilerDone
8 , testCompilerDone
9+ , testCompilerError
9 , testConfiguration
10 , testConfiguration
10 , cleanTestEnv
11 , cleanTestEnv
11 , renderParagraphs
12 , renderParagraphs
@@ -13,7 +14,7 @@ module TestSuite.Util
13
14
14
15
15--------------------------------------------------------------------------------
16--------------------------------------------------------------------------------
16−import Data.List (intercalate)
17+import Data.List (intercalate, isInfixOf)
17import Data.Monoid (mempty)
18import Data.Monoid (mempty)
18import qualified Data.Set as S
19import qualified Data.Set as S
19import Test.Tasty
20import Test.Tasty
@@ -80,13 +81,21 @@ testCompilerDone store provider underlying compiler = do
80 CompilerDone x _ -> return x
81 CompilerDone x _ -> return x
81 CompilerError e -> fail $
82 CompilerError e -> fail $
82 "TestSuite.Util.testCompilerDone: compiler " ++ show underlying ++
83 "TestSuite.Util.testCompilerDone: compiler " ++ show underlying ++
83− " threw: " ++ intercalate "; " e
84+ " threw: " ++ intercalate "; " (compilerErrorMessages e)
84 CompilerRequire i _ -> fail $
85 CompilerRequire i _ -> fail $
85 "TestSuite.Util.testCompilerDone: compiler " ++ show underlying ++
86 "TestSuite.Util.testCompilerDone: compiler " ++ show underlying ++
86 " requires: " ++ show i
87 " requires: " ++ show i
87 CompilerSnapshot _ _ -> fail
88 CompilerSnapshot _ _ -> fail
88 "TestSuite.Util.testCompilerDone: unexpected CompilerSnapshot"
89 "TestSuite.Util.testCompilerDone: unexpected CompilerSnapshot"
89
90
91+testCompilerError :: Store -> Provider -> Identifier -> Compiler a -> String -> IO ()
92+testCompilerError store provider underlying compiler expectedMessage = do
93+ result <- testCompiler store provider underlying compiler
94+ case result of
95+ CompilerError e ->
96+ any (expectedMessage `isInfixOf`) (compilerErrorMessages e) @?
97+ "Expecting '" ++ expectedMessage ++ "' error"
98+ _ -> assertFailure "Expecting CompilerError"
90
99
91--------------------------------------------------------------------------------
100--------------------------------------------------------------------------------
92testConfiguration :: Configuration
101testConfiguration :: Configuration
tests/data/embed.htmlnew file+1
@@ -0,0 +1 @@
1+<p>$body$</p>
tests/data/posts/2019/05/10/tomorrow.mdnew file+1
@@ -0,0 +1 @@
1+This day hasn't happened yet (as of writing this).
web/css/syntax.css+61−18
@@ -1,18 +1,61 @@
1−/* Generated by pandoc. */
1+a.sourceLine { display: inline-block; line-height: 1.25; }
2−table.sourceCode, tr.sourceCode, td.lineNumbers, td.sourceCode, table.sourceCode pre
2+a.sourceLine { pointer-events: none; color: inherit; text-decoration: inherit; }
3− { margin: 0; padding: 0; border: 0; vertical-align: baseline; border: none; }
3+a.sourceLine:empty { height: 1.2em; }
4−td.lineNumbers { border-right: 1px solid #AAAAAA; text-align: right; color: #AAAAAA; padding-right: 5px; padding-left: 5px; }
4+.sourceCode { overflow: visible; }
5−td.sourceCode { padding-left: 5px; }
5+code.sourceCode { white-space: pre; position: relative; }
6−.sourceCode span.kw { color: #007020; font-weight: bold; }
6+div.sourceCode { margin: 1em 0; }
7+pre.sourceCode { margin: 0; }
8+@media screen {
9+div.sourceCode { overflow: auto; }
10+}
11+@media print {
12+code.sourceCode { white-space: pre-wrap; }
13+a.sourceLine { text-indent: -1em; padding-left: 1em; }
14+}
15+pre.numberSource a.sourceLine
16+ { position: relative; left: -4em; }
17+pre.numberSource a.sourceLine::before
18+ { content: attr(title);
19+ position: relative; left: -1em; text-align: right; vertical-align: baseline;
20+ border: none; pointer-events: all; display: inline-block;
21+ -webkit-touch-callout: none; -webkit-user-select: none;
22+ -khtml-user-select: none; -moz-user-select: none;
23+ -ms-user-select: none; user-select: none;
24+ padding: 0 4px; width: 4em;
25+ color: #aaaaaa;
26+ }
27+pre.numberSource { margin-left: 3em; border-left: 1px solid #aaaaaa; padding-left: 4px; }
28+div.sourceCode
29+ { }
30+@media screen {
31+a.sourceLine::before { text-decoration: underline; }
32+}
33+code span.al { color: #ff0000; font-weight: bold; } /* Alert */
34+code span.an { color: #60a0b0; font-weight: bold; font-style: italic; } /* Annotation */
35+code span.at { color: #7d9029; } /* Attribute */
36+code span.bn { color: #40a070; } /* BaseN */
37+code span.bu { } /* BuiltIn */
38+code span.cf { color: #007020; font-weight: bold; } /* ControlFlow */
39+code span.ch { color: #4070a0; } /* Char */
40+code span.cn { color: #880000; } /* Constant */
41+code span.co { color: #60a0b0; font-style: italic; } /* Comment */
42+code span.cv { color: #60a0b0; font-weight: bold; font-style: italic; } /* CommentVar */
43+code span.do { color: #ba2121; font-style: italic; } /* Documentation */
7−.sourceCode span.dt { color: #902000; }
44+code span.dt { color: #902000; } /* DataType */
8−.sourceCode span.dv { color: #40a070; }
45+code span.dv { color: #40a070; } /* DecVal */
9−.sourceCode span.bn { color: #40a070; }
46+code span.er { color: #ff0000; font-weight: bold; } /* Error */
47+code span.ex { } /* Extension */
10−.sourceCode span.fl { color: #40a070; }
48+code span.fl { color: #40a070; } /* Float */
11−.sourceCode span.ch { color: #4070a0; }
12−.sourceCode span.st { color: #4070a0; }
13−.sourceCode span.co { color: #60a0b0; font-style: italic; }
14−.sourceCode span.ot { color: #007020; }
15−.sourceCode span.al { color: red; font-weight: bold; }
16−.sourceCode span.fu { color: #06287e; }
49+code span.fu { color: #06287e; } /* Function */
17−.sourceCode span.re { }
50+code span.im { } /* Import */
18−.sourceCode span.er { color: red; font-weight: bold; }
51+code span.in { color: #60a0b0; font-weight: bold; font-style: italic; } /* Information */
52+code span.kw { color: #007020; font-weight: bold; } /* Keyword */
53+code span.op { color: #666666; } /* Operator */
54+code span.ot { color: #007020; } /* Other */
55+code span.pp { color: #bc7a00; } /* Preprocessor */
56+code span.sc { color: #4070a0; } /* SpecialChar */
57+code span.ss { color: #bb6688; } /* SpecialString */
58+code span.st { color: #4070a0; } /* String */
59+code span.va { color: #19177c; } /* Variable */
60+code span.vs { color: #4070a0; } /* VerbatimString */
61+code span.wa { color: #60a0b0; font-weight: bold; font-style: italic; } /* Warning */
web/examples.markdown+13−5
@@ -98,8 +98,8 @@ directly with the default Hakyll site.
98 [source](https://github.com/Libbum/AxiomaticSemantics)
98 [source](https://github.com/Libbum/AxiomaticSemantics)
99- [https://tesser.org/](http://web.archive.org/web/20161022221713/https://tesser.org/),
99- [https://tesser.org/](http://web.archive.org/web/20161022221713/https://tesser.org/),
100 [source](https://github.com/fractalcat/tesser.org)
100 [source](https://github.com/fractalcat/tesser.org)
101−- <https://wei2912.github.io/>,
101+- <https://weien.io/>,
102− [source](https://github.com/wei2912/blog_src)
102+  [source](https://github.com/wei2912/blog-src)
103- <http://www.metasepi.org/>,
103- <http://www.metasepi.org/>,
104 [source](https://github.com/metasepi/metasepi-website)
104 [source](https://github.com/metasepi/metasepi-website)
105- <http://gisli.hamstur.is/>,
105- <http://gisli.hamstur.is/>,
@@ -166,16 +166,24 @@ directly with the default Hakyll site.
166 [source](https://github.com/bendo/bendo.github.io)
166 [source](https://github.com/bendo/bendo.github.io)
167- <http://adarsh.cc/>,
167- <http://adarsh.cc/>,
168 [source](https://github.com/adarshp/adarsh.cc)
168 [source](https://github.com/adarshp/adarsh.cc)
169−- <http://zer0.degree/>,
169+- <http://0a.io>,
170− [source](https://github.com/arrchhy/zer0.degree)
170+ [source](https://github.com/arrchyy/0a.io)
171- <https://nasy.moe/>,
171- <https://nasy.moe/>,
172− [source](https://github.com/nasyxx/nasyxx.github.io)
172+ [source](https://github.com/nasyxx/NasyMoe)
173- <http://uniprocess.org/>,
173- <http://uniprocess.org/>,
174 [source](https://gitlab.com/uniprocess/uniprocess.org)
174 [source](https://gitlab.com/uniprocess/uniprocess.org)
175- <https://regularflolloping.com/>,
175- <https://regularflolloping.com/>,
176 [source](https://github.com/techieAgnostic/rf)
176 [source](https://github.com/techieAgnostic/rf)
177- <http://enter-haken.github.io/>,
177- <http://enter-haken.github.io/>,
178 [source](https://github.com/enter-haken/blog)
178 [source](https://github.com/enter-haken/blog)
179+- <https://modular.blind.guru/>,
180+ [source](https://github.com/mlang/eurorack-metadata)
181+- <http://www.jonashietala.se/>,
182+ [source](https://github.com/treeman/jonashietala)
183+- <https://abhinavsarkar.net/>,
184+ [source](https://github.com/abhin4v/abhin4v.github.io)
185+- <https://dailyreadingnotes.com/>,
186+ [source](https://github.com/hackeryarn/daily-reading-notes)
179
187
180## Hakyll 3.X
188## Hakyll 3.X
181
189
web/site.hs+14−32
@@ -1,8 +1,7 @@
1--------------------------------------------------------------------------------
1--------------------------------------------------------------------------------
2{-# LANGUAGE OverloadedStrings #-}
2{-# LANGUAGE OverloadedStrings #-}
3import Control.Arrow (second)
3import Control.Arrow (second)
4−import Control.Monad (forM_)
4+import Control.Monad (filterM, forM_)
5−import Data.Char (isDigit)
6import Data.List (isPrefixOf, sortBy)
5import Data.List (isPrefixOf, sortBy)
7import Data.Monoid ((<>))
6import Data.Monoid ((<>))
8import Data.Ord (comparing)
7import Data.Ord (comparing)
@@ -57,17 +56,11 @@ main = hakyllWith config $ do
57 create ["tutorials.html"] $ do
56 create ["tutorials.html"] $ do
58 route idRoute
57 route idRoute
59 compile $ do
58 compile $ do
60− tuts <-
59+ ctx <- tutorialsCtx <$>
61 sortBy (comparing itemIdentifier) <$> loadAll "tutorials/*"
60 sortBy (comparing itemIdentifier) <$> loadAll "tutorials/*"
62−
63− let tutorialsCtx =
64− constField "title" "Tutorials" `mappend`
65− listField "tutorials" tutorialCtx (return tuts) `mappend`
66− defaultContext
67−
68 makeItem ""
61 makeItem ""
69− >>= loadAndApplyTemplate "templates/tutorials.html" tutorialsCtx
62+ >>= loadAndApplyTemplate "templates/tutorials.html" ctx
70− >>= loadAndApplyTemplate "templates/default.html" tutorialsCtx
63+ >>= loadAndApplyTemplate "templates/default.html" ctx
71 >>= relativizeUrls
64 >>= relativizeUrls
72
65
73 -- Templates
66 -- Templates
@@ -108,28 +101,17 @@ hackage url
108 reverse $ splitPath url
101 reverse $ splitPath url
109
102
110
103
111−--------------------------------------------------------------------------------
112−data TutorialType = SeriesTutorial | ArticlesTutorial | ExternalTutorial
113− deriving (Eq)
114−
115−
116--------------------------------------------------------------------------------
104--------------------------------------------------------------------------------
117-- | Partition tutorials into tutorial series, other articles, external articles
105-- | Partition tutorials into tutorial series, other articles, external articles
118−tutorialCtx :: Context String
106+tutorialsCtx :: [Item String] -> Context String
119−tutorialCtx =
107+tutorialsCtx tuts =
120− field "isSeries" (isTutorialType SeriesTutorial) <>
108+ constField "title" "Tutorials" <>
121− field "isArticle" (isTutorialType ArticlesTutorial) <>
109+ listField "main" defaultContext (ofType "main") <>
122− field "isExternal" (isTutorialType ExternalTutorial) <>
110+ listField "articles" defaultContext (ofType "article") <>
111+ listField "externals" defaultContext (ofType "external") <>
112+ listField "robertwpearce" defaultContext (ofType "robertwpearce") <>
123 defaultContext
113 defaultContext
124 where
114 where
125− getTutorialType item = do
115+ ofType ty = filterM (\item -> do
126− mbExternal <- getMetadataField (itemIdentifier item) "external"
116+ mbType <- getMetadataField (itemIdentifier item) "type"
127− return $ case mbExternal of
117+ return $ Just ty == mbType) tuts
128− Just _ -> ExternalTutorial
129− _ -> case splitPath (toFilePath $ itemIdentifier item) of
130− [_, (x : _)] -> if isDigit x then SeriesTutorial else ArticlesTutorial
131− _ -> ArticlesTutorial
132−
133− isTutorialType ty0 item = do
134− ty1 <- getTutorialType item
135− if ty0 == ty1 then return "yes" else fail "no"
web/templates/tutorials.html+24−18
@@ -1,23 +1,19 @@
1<h1>Tutorials about Hakyll</h1>
1<h1>Tutorials about Hakyll</h1>
2<h2 id="series">Tutorial series</h2>
2<h2 id="series">Tutorial series</h2>
3<ul>
3<ul>
4− $for(tutorials)$
4+ $for(main)$
5− $if(isSeries)$
6− <li>
5+ <li>
7− <a href="$url$">$title$</a>
6+ <a href="$url$">$title$</a>
8− </li>
7+ </li>
9− $endif$
10 $endfor$
8 $endfor$
11</ul>
9</ul>
12<h2 id="articles">Other articles</h2>
10<h2 id="articles">Other articles</h2>
13<p>In no particular order:</p>
11<p>In no particular order:</p>
14<ul>
12<ul>
15− $for(tutorials)$
13+ $for(articles)$
16− $if(isArticle)$
17− <li>
14+ <li>
18− <a href="$url$">$title$</a> by <em>$author$</em>
15+ <a href="$url$">$title$</a> by <em>$author$</em>
19− </li>
16+ </li>
20− $endif$
21 $endfor$
17 $endfor$
22</ul>
18</ul>
23<h2 id="external-articles">External articles</h2>
19<h2 id="external-articles">External articles</h2>
@@ -28,12 +24,22 @@
28</p>
24</p>
29<p>In no particular order:</p>
25<p>In no particular order:</p>
30<ul>
26<ul>
31− $for(tutorials)$
27+ $for(externals)$
32− $if(isExternal)$
33− <li>
28+ <li>
34− <a href="$url$">$title$</a> by <em>$author$</em>
29+ <a href="$url$">$title$</a> by <em>$author$</em>
35− </li>
30+ </li>
36− $endif$
31+ $endfor$
32+</ul>
33+<h2 id="robertwpearce">Robert Pearce's series</h2>
34+<p>
35+ <a href="https://robertwpearce.com/">Robert Pearce</a> has been writing an
36+ excellent tutorial-style series on Hakyll as well. You can find those here:
37+</p>
38+<ul>
39+ $for(robertwpearce)$
40+ <li>
41+ <a href="$url$">$title$</a>
42+ </li>
37 $endfor$
43 $endfor$
38</ul>
44</ul>
39<p>
45<p>
web/tutorials/01-installation.markdown+3−1
@@ -1,6 +1,7 @@
1---
1---
2title: Installation
2title: Installation
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: main
4---
5---
5
6
6Installation
7Installation
@@ -18,6 +19,7 @@ Linux distro packages:
18
19
19- [Debian unstable](http://packages.debian.org/source/sid/haskell-hakyll)
20- [Debian unstable](http://packages.debian.org/source/sid/haskell-hakyll)
20- [Fedora](https://apps.fedoraproject.org/packages/ghc-hakyll)
21- [Fedora](https://apps.fedoraproject.org/packages/ghc-hakyll)
22+- [Nix](https://nixos.org/nixos/packages.html#hakyll): `$ nix-env -i hakyll`
21
23
22Building the example site
24Building the example site
23-------------------------
25-------------------------
@@ -26,7 +28,7 @@ Apart from the main Hakyll library, the cabal package also provides you with an
26executable `hakyll-init` to create an example site. This is an easy way to get
28executable `hakyll-init` to create an example site. This is an easy way to get
27started:
29started:
28
30
29− $ hakyll-init my-site
31+ $ stack exec hakyll-init my-site
30
32
31This creates a folder `my-site` in the current directory, with some example
33This creates a folder `my-site` in the current directory, with some example
32content and a generic configuration.
34content and a generic configuration.
web/tutorials/02-basics.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: The basics
2title: The basics
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: main
4---
5---
5
6
6Building and cleaning
7Building and cleaning
web/tutorials/03-rules-routes-compilers.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: Rules, routes and compilers
2title: Rules, routes and compilers
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: main
4---
5---
5
6
6Basic rules
7Basic rules
web/tutorials/04-compilers.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: 'More on compilers: load, and templates'
2title: 'More on compilers: load, and templates'
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: main
4---
5---
5
6
6Loading items
7Loading items
web/tutorials/05-snapshots-feeds.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: Snapshots, and how to produce an RSS/Atom feed
2title: Snapshots, and how to produce an RSS/Atom feed
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: main
4---
5---
5
6
6Basic feed configuration
7Basic feed configuration
web/tutorials/06-versions.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: Producing multiple versions of a single file
2title: Producing multiple versions of a single file
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: main
4---
5---
5
6
6Basics
7Basics
web/tutorials/a-guide-to-the-hakyll-module-zoo.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: A Guide to the Hakyll Module Zoo
2title: A Guide to the Hakyll Module Zoo
3author: Brent Yorgey
3author: Brent Yorgey
4+type: article
4---
5---
5
6
6The hakyll package [contains a bewildering array](/reference/) of modules, and
7The hakyll package [contains a bewildering array](/reference/) of modules, and
web/tutorials/external-add-tags-to-your-hakyll-blog.md+1−1
@@ -2,5 +2,5 @@
2title: Add tags to your Hakyll blog
2title: Add tags to your Hakyll blog
3author: Javran Cheng
3author: Javran Cheng
4url: https://javran.github.io/posts/2014-03-01-add-tags-to-your-hakyll-blog.html
4url: https://javran.github.io/posts/2014-03-01-add-tags-to-your-hakyll-blog.html
5−external: true
5+type: external
6---
6---
web/tutorials/external-blogger-port.md+1−1
@@ -2,5 +2,5 @@
2title: Port an existing Blogger blog to Hakyll
2title: Port an existing Blogger blog to Hakyll
3author: Daniel Campoverde
3author: Daniel Campoverde
4url: 'http://www.sillybytes.net/posts/from_blogger_to_hakyll.html'
4url: 'http://www.sillybytes.net/posts/from_blogger_to_hakyll.html'
5−external: true
5+type: external
6---
6---
web/tutorials/external-cassius-template.md+1−1
@@ -2,5 +2,5 @@
2title: Using Cassius templating with Hakyll
2title: Using Cassius templating with Hakyll
3author: Daniel Campoverde
3author: Daniel Campoverde
4url: 'http://www.sillybytes.net/posts/using_hakyll_with_cassius.html'
4url: 'http://www.sillybytes.net/posts/using_hakyll_with_cassius.html'
5−external: true
5+type: external
6---
6---
web/tutorials/external-clean-urls-with-hakyll.md+1−1
@@ -2,5 +2,5 @@
2title: Clean URLs with Hakyll
2title: Clean URLs with Hakyll
3author: Rohan Jain
3author: Rohan Jain
4url: http://www.rohanjain.in/hakyll-clean-urls/
4url: http://www.rohanjain.in/hakyll-clean-urls/
5−external: true
5+type: external
6---
6---
web/tutorials/external-deploying-with-widely.md+1−1
@@ -2,5 +2,5 @@
2title: Deploying with Widely
2title: Deploying with Widely
3author: Kyle Marek-Spartz
3author: Kyle Marek-Spartz
4url: http://kyle.marek-spartz.org/posts/2013-12-09-widely-and-hakyll.html
4url: http://kyle.marek-spartz.org/posts/2013-12-09-widely-and-hakyll.html
5−external: true
5+type: external
6---
6---
web/tutorials/external-external-code-inclusion.mdnew file+6
@@ -0,0 +1,6 @@
1+---
2+title: 'External code inclusion with Hakyll'
3+author: 'Tobias Pleyer'
4+url: 'http://blog.tpleyer.de/posts/2019-04-21-external-code-inclusion-with-hakyll.html'
5+type: external
6+---
web/tutorials/external-functionfield.md+1−1
@@ -2,5 +2,5 @@
2title: Hakyll's functionField
2title: Hakyll's functionField
3author: Beerend Lauwers
3author: Beerend Lauwers
4url: http://beerendlauwers.be/posts/2015-09-21-hakylls-functionfield.html
4url: http://beerendlauwers.be/posts/2015-09-21-hakylls-functionfield.html
5−external: true
5+type: external
6---
6---
web/tutorials/external-github-pages-circle-ci.md+1−1
@@ -2,5 +2,5 @@
2title: Create a GitHub page with Hakyll and CircleCI
2title: Create a GitHub page with Hakyll and CircleCI
3author: Juan Pedro Villa
3author: Juan Pedro Villa
4url: https://www.stackbuilders.com/news/dr-hakyll-create-a-github-page-with-hakyll-and-circleci
4url: https://www.stackbuilders.com/news/dr-hakyll-create-a-github-page-with-hakyll-and-circleci
5−external: true
5+type: external
6---
6---
web/tutorials/external-inlining-and-compressing-css.md+1−1
@@ -2,5 +2,5 @@
2title: Inlining and compressing CSS
2title: Inlining and compressing CSS
3author: Kyle Marek-Spartz
3author: Kyle Marek-Spartz
4url: http://kyle.marek-spartz.org/posts/2014-12-09-hakyll-css-template-compiler.html
4url: http://kyle.marek-spartz.org/posts/2014-12-09-hakyll-css-template-compiler.html
5−external: true
5+type: external
6---
6---
web/tutorials/faq.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: FAQ
2title: FAQ
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: article
4---
5---
5
6
6## "hGetContents: invalid argument" or "commitBuffer: invalid argument"
7## "hGetContents: invalid argument" or "commitBuffer: invalid argument"
web/tutorials/github-pages-tutorial.md+1
@@ -1,6 +1,7 @@
1---
1---
2title: Using Hakyll with GitHub Pages
2title: Using Hakyll with GitHub Pages
3author: Erik Stevenson
3author: Erik Stevenson
4+type: article
4---
5---
5
6
6## Introduction
7## Introduction
web/tutorials/hakyll-3-to-hakyll4-migration-guide.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: Hakyll 3 to Hakyll 4 migration guide
2title: Hakyll 3 to Hakyll 4 migration guide
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: article
4---
5---
5
6
6Introduction
7Introduction
web/tutorials/robertwpearce-01-setup-and-initial-customization.htmlnew file+5
@@ -0,0 +1,5 @@
1+---
2+title: 'Pt. 1: Setup & Initial Customization'
3+url: 'https://robertwpearce.com/hakyll-pt-1-setup-and-initial-customization.html'
4+type: robertwpearce
5+---
web/tutorials/robertwpearce-02-generating-a-sitemap-xml-file.htmlnew file+5
@@ -0,0 +1,5 @@
1+---
2+title: 'Pt. 2: Generating a Sitemap XML File'
3+url: 'https://robertwpearce.com/hakyll-pt-2-generating-a-sitemap-xml-file.html'
4+type: robertwpearce
5+---
web/tutorials/robertwpearce-03-generating-rss-and-atom-xml-feeds.htmlnew file+5
@@ -0,0 +1,5 @@
1+---
2+title: 'Pt. 3: Generating RSS and Atom XML Feeds'
3+url: 'https://robertwpearce.com/hakyll-pt-3-generating-rss-and-atom-xml-feeds.html'
4+type: robertwpearce
5+---
web/tutorials/robertwpearce-04-copying-static-files-for-your-build.htmlnew file+5
@@ -0,0 +1,5 @@
1+---
2+title: 'Pt. 4: Copying Static Files For Your Build'
3+url: 'https://robertwpearce.com/hakyll-pt-4-copying-static-files-for-your-build.html'
4+type: robertwpearce
5+---
web/tutorials/robertwpearce-05-generating-custom-post-filenames-from-a-title-slug.htmlnew file+5
@@ -0,0 +1,5 @@
1+---
2+title: 'Pt. 5 – Generating Custom Post Filenames From a Title Slug'
3+url: 'https://robertwpearce.com/hakyll-pt-5-generating-custom-post-filenames-from-a-title-slug.html'
4+type: robertwpearce
5+---
web/tutorials/using-clay-with-hakyll.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: Using Clay with Hakyll
2title: Using Clay with Hakyll
3author: Jasper Van der Jeugt
3author: Jasper Van der Jeugt
4+type: article
4---
5---
5
6
6[Clay](http://sebastiaanvisser.github.com/clay/) is a nice CSS preprocesser
7[Clay](http://sebastiaanvisser.github.com/clay/) is a nice CSS preprocesser
web/tutorials/using-teasers-in-hakyll.markdown+1
@@ -1,6 +1,7 @@
1---
1---
2title: Using teasers in Hakyll
2title: Using teasers in Hakyll
3author: Ivan Veselov
3author: Ivan Veselov
4+type: article
4---
5---
5
6
6## Introduction
7## Introduction