on master help sign in
Improve error messages
commit036c583ea243869f05a5a311c90b94943a2b635c authorJasper Van der Jeugt <m@jaspervdj.be> dateFri 30 Aug 2019, 11:46 (7 years ago) parent779fa66
18 files changed+527−231
hakyll.cabal+4−3 lib/Hakyll/Core/Compiler.hs+35−2 lib/Hakyll/Core/Compiler/Internal.hs+126−41 lib/Hakyll/Core/Compiler/Require.hs+2 lib/Hakyll/Core/Logger.hs+9−3 lib/Hakyll/Core/Provider/Metadata.hs+3−2 lib/Hakyll/Core/Runtime.hs+3−3 lib/Hakyll/Core/Store.hs+20−20 lib/Hakyll/Web/Feed.hs+24−31 lib/Hakyll/Web/Template.hs+24−2 lib/Hakyll/Web/Template/Context.hs+101−31 lib/Hakyll/Web/Template/Internal.hs+101−43 lib/Hakyll/Web/Template/Internal/Element.hs+7−14 tests/Hakyll/Core/UnixFilter/Tests.hs+6−16 tests/Hakyll/Web/Template/Context/Tests.hs+2−2 tests/Hakyll/Web/Template/Tests.hs+48−16 tests/TestSuite/Util.hs+11−2 tests/data/embed.html+1
hakyll.cabal+4−3
@@ -172,10 +172,12 @@ Library
172172 data-default >= 0.4 && < 0.8,
173173 deepseq >= 1.3 && < 1.5,
174174 directory >= 1.0 && < 1.4,
175+ file-embed >= 0.0.10.1 && < 0.0.12,
175176 filepath >= 1.0 && < 1.5,
176177 lrucache >= 1.1.1 && < 1.3,
177178 mtl >= 1 && < 2.3,
178179 network-uri >= 2.6 && < 2.7,
180+ optparse-applicative >= 0.12 && < 0.15,
179181 parsec >= 3.0 && < 3.2,
180182 process >= 1.6 && < 1.7,
181183 random >= 1.0 && < 1.2,
@@ -183,14 +185,13 @@ Library
183185 resourcet >= 1.1 && < 1.3,
184186 scientific >= 0.3.4 && < 0.4,
185187 tagsoup >= 0.13.1 && < 0.15,
188+ template-haskell >= 2.14 && < 2.15,
186189 text >= 0.11 && < 1.3,
187190 time >= 1.8 && < 1.10,
188191 time-locale-compat >= 0.1 && < 0.2,
189192 unordered-containers >= 0.2 && < 0.3,
190193 vector >= 0.11 && < 0.13,
191− yaml >= 0.8.11 && < 0.12,
192− optparse-applicative >= 0.12 && < 0.15,
193− file-embed >= 0.0.10.1 && < 0.0.12
194+ yaml >= 0.8.11 && < 0.12
194195
195196 If flag(previewServer)
196197 Build-depends:
lib/Hakyll/Core/Compiler.hs+35−2
@@ -24,13 +24,16 @@ module Hakyll.Core.Compiler
2424 , cached
2525 , unsafeCompiler
2626 , debugCompiler
27+ , noResult
28+ , withErrorMessage
2729 ) where
2830
2931
3032--------------------------------------------------------------------------------
31−import Control.Monad (when, unless)
33+import Control.Monad (unless, when, (>=>))
3234import Data.Binary (Binary)
3335import Data.ByteString.Lazy (ByteString)
36+import qualified Data.List.NonEmpty as NonEmpty
3437import Data.Typeable (Typeable)
3538import System.Environment (getProgName)
3639import System.FilePath (takeExtension)
@@ -62,6 +65,7 @@ getUnderlyingExtension = takeExtension . toFilePath <$> getUnderlying
6265
6366
6467--------------------------------------------------------------------------------
68+-- | Create an item from the underlying identifier and a given value.
6569makeItem :: a -> Compiler (Item a)
6670makeItem x = do
6771 identifier <- getUnderlying
@@ -141,6 +145,10 @@ saveSnapshot snapshot item = do
141145
142146
143147--------------------------------------------------------------------------------
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.
144152cached :: (Binary a, Typeable a)
145153 => String
146154 -> Compiler a
@@ -177,12 +185,37 @@ cached name compiler = do
177185
178186
179187--------------------------------------------------------------------------------
188+-- | Run an IO computation without dependencies in a Compiler
180189unsafeCompiler :: IO a -> Compiler a
181190unsafeCompiler = compilerUnsafeIO
182191
183192
184193--------------------------------------------------------------------------------
185−-- | Compiler for debugging purposes
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+--------------------------------------------------------------------------------
217+-- | Compiler for debugging purposes.
218+-- Passes a message to the debug logger that is printed in verbose mode.
186219debugCompiler :: String -> Compiler ()
187220debugCompiler msg = do
188221 logger <- compilerLogger <$> compilerAsk
lib/Hakyll/Core/Compiler/Internal.hs+126−41
@@ -1,6 +1,7 @@
11--------------------------------------------------------------------------------
22-- | Internally used compiler module
33{-# LANGUAGE CPP #-}
4+{-# LANGUAGE DeriveFunctor #-}
45{-# LANGUAGE FlexibleInstances #-}
56{-# LANGUAGE GADTs #-}
67{-# LANGUAGE GeneralizedNewtypeDeriving #-}
@@ -10,19 +11,26 @@ module Hakyll.Core.Compiler.Internal
1011 Snapshot
1112 , CompilerRead (..)
1213 , CompilerWrite (..)
14+ , CompilerErrors (..)
1315 , CompilerResult (..)
1416 , Compiler (..)
1517 , runCompiler
1618
1719 -- * Core operations
20+ , compilerResult
1821 , compilerTell
1922 , compilerAsk
23+ , compilerUnsafeIO
24+
25+ -- * Error operations
2026 , compilerThrow
27+ , compilerNoResult
2128 , compilerCatch
22− , compilerResult
23− , compilerUnsafeIO
29+ , compilerTry
30+ , compilerErrorMessages
2431
2532 -- * Utilities
33+ , compilerDebugEntries
2634 , compilerTellDependencies
2735 , compilerTellCacheHits
2836 ) where
@@ -32,7 +40,9 @@ module Hakyll.Core.Compiler.Internal
3240import Control.Applicative (Alternative (..))
3341import Control.Exception (SomeException, handle)
3442import 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
3646#if MIN_VERSION_base(4,9,0)
3747import Data.Semigroup (Semigroup (..))
3848#endif
@@ -45,7 +55,6 @@ import Hakyll.Core.Configuration
4555import Hakyll.Core.Dependencies
4656import Hakyll.Core.Identifier
4757import Hakyll.Core.Identifier.Pattern
48−import Hakyll.Core.Logger (Logger)
4958import qualified Hakyll.Core.Logger as Logger
5059import Hakyll.Core.Metadata
5160import Hakyll.Core.Provider
@@ -75,7 +84,7 @@ data CompilerRead = CompilerRead
7584 , -- | Compiler store
7685 compilerStore :: Store
7786 , -- | Logger
78− compilerLogger :: Logger
87+ compilerLogger :: Logger.Logger
7988 }
8089
8190
@@ -104,11 +113,29 @@ instance Monoid CompilerWrite where
104113
105114
106115--------------------------------------------------------------------------------
107−data CompilerResult a where
108− CompilerDone :: a -> CompilerWrite -> CompilerResult a
109− CompilerSnapshot :: Snapshot -> Compiler a -> CompilerResult a
110− CompilerError :: [String] -> CompilerResult a
111− CompilerRequire :: (Identifier, Snapshot) -> Compiler a -> CompilerResult a
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
135+ = CompilerDone a CompilerWrite
136+ | CompilerSnapshot Snapshot (Compiler a)
137+ | CompilerRequire (Identifier, Snapshot) (Compiler a)
138+ | CompilerError (CompilerErrors String)
112139
113140
114141--------------------------------------------------------------------------------
@@ -126,14 +153,14 @@ instance Functor Compiler where
126153 return $ case res of
127154 CompilerDone x w -> CompilerDone (f x) w
128155 CompilerSnapshot s c' -> CompilerSnapshot s (fmap f c')
129− CompilerError e -> CompilerError e
130156 CompilerRequire i c' -> CompilerRequire i (fmap f c')
157+ CompilerError e -> CompilerError e
131158 {-# INLINE fmap #-}
132159
133160
134161--------------------------------------------------------------------------------
135162instance Monad Compiler where
136− return x = Compiler $ \_ -> return $ CompilerDone x mempty
163+ return x = compilerResult $ CompilerDone x mempty
137164 {-# INLINE return #-}
138165
139166 Compiler c >>= f = Compiler $ \r -> do
@@ -146,14 +173,14 @@ instance Monad Compiler where
146173 CompilerSnapshot s c' -> CompilerSnapshot s $ do
147174 compilerTell w -- Save dependencies!
148175 c'
149− CompilerError e -> CompilerError e
150176 CompilerRequire i c' -> CompilerRequire i $ do
151177 compilerTell w -- Save dependencies!
152178 c'
179+ CompilerError e -> CompilerError e
153180
154181 CompilerSnapshot s c' -> return $ CompilerSnapshot s (c' >>= f)
155− CompilerError e -> return $ CompilerError e
156182 CompilerRequire i c' -> return $ CompilerRequire i (c' >>= f)
183+ CompilerError e -> return $ CompilerError e
157184 {-# INLINE (>>=) #-}
158185
159186 fail = compilerThrow . return
@@ -170,87 +197,145 @@ instance Applicative Compiler where
170197
171198
172199--------------------------------------------------------------------------------
200+-- | Access provided metadata from anywhere
173201instance MonadMetadata Compiler where
174202 getMetadata = compilerGetMetadata
175203 getMatches = compilerGetMatches
176204
177205
178206--------------------------------------------------------------------------------
207+-- | Compilation may fail with multiple error messages.
208+-- 'catchError' handles errors from 'throwError', 'fail' and 'Hakyll.Core.Compiler.noResult'
179209instance MonadError [String] Compiler where
180− throwError = compilerThrow
181− catchError = compilerCatch
210+ throwError = compilerThrow
211+ catchError c = compilerCatch c . (. compilerErrorMessages)
182212
183213
184214--------------------------------------------------------------------------------
215+-- | Like 'unCompiler' but treating IO exceptions as 'CompilerError's
185216runCompiler :: Compiler a -> CompilerRead -> IO (CompilerResult a)
186217runCompiler compiler read' = handle handler $ unCompiler compiler read'
187218 where
188219 handler :: SomeException -> IO (CompilerResult a)
189− handler e = return $ CompilerError [show e]
220+ handler e = return $ CompilerError $ CompilationFailure $ show e :| []
190221
191222
192223--------------------------------------------------------------------------------
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.
193227instance Alternative Compiler where
194− empty = compilerThrow []
195− x <|> y = compilerCatch x $ \es -> do
196− logger <- compilerLogger <$> compilerAsk
197− forM_ es $ \e -> compilerUnsafeIO $ Logger.debug logger $
198− "Hakyll.Core.Compiler.Internal: Alternative failed: " ++ e
199− y
228+ empty = compilerNoResult []
229+ x <|> y = x `compilerCatch` (\rx -> y `compilerCatch` (\ry ->
230+ case (rx, ry) of
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
240+ debug = compilerDebugEntries "Hakyll.Core.Compiler.Internal: Alternative fail suppressed"
200241 {-# INLINE (<|>) #-}
201242
202243
203244--------------------------------------------------------------------------------
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
204253compilerAsk :: Compiler CompilerRead
205254compilerAsk = Compiler $ \r -> return $ CompilerDone r mempty
206255{-# INLINE compilerAsk #-}
207256
208257
209258--------------------------------------------------------------------------------
259+-- | Put a 'CompilerWrite'
210260compilerTell :: CompilerWrite -> Compiler ()
211−compilerTell deps = Compiler $ \_ -> return $ CompilerDone () deps
261+compilerTell = compilerResult . CompilerDone ()
212262{-# INLINE compilerTell #-}
213263
214264
215265--------------------------------------------------------------------------------
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.
216279compilerThrow :: [String] -> Compiler a
217−compilerThrow es = Compiler $ \_ -> return $ CompilerError es
218−{-# INLINE compilerThrow #-}
280+compilerThrow = compilerResult . CompilerError .
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
219287
220288
221289--------------------------------------------------------------------------------
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
223310compilerCatch (Compiler x) f = Compiler $ \r -> do
224311 res <- x r
225312 case res of
226313 CompilerDone res' w -> return (CompilerDone res' w)
227314 CompilerSnapshot s c -> return (CompilerSnapshot s (compilerCatch c f))
228− CompilerError e -> unCompiler (f e) r
229315 CompilerRequire i c -> return (CompilerRequire i (compilerCatch c f))
316+ CompilerError e -> unCompiler (f e) r
230317{-# INLINE compilerCatch #-}
231318
232319
233320--------------------------------------------------------------------------------
234−-- | Put the result back in a compiler
235−compilerResult :: CompilerResult a -> Compiler a
236−compilerResult x = Compiler $ \_ -> return x
237−{-# INLINE compilerResult #-}
238−
321+compilerDebugLog :: [String] -> Compiler ()
322+compilerDebugLog ms = do
323+ logger <- compilerLogger <$> compilerAsk
324+ compilerUnsafeIO $ forM_ ms $ Logger.debug logger
239325
240326--------------------------------------------------------------------------------
241−compilerUnsafeIO :: IO a -> Compiler a
242−compilerUnsafeIO io = Compiler $ \_ -> do
243− x <- io
244− return $ CompilerDone x mempty
245−{-# INLINE compilerUnsafeIO #-}
327+-- | Pass a list of messages with a heading to the debug logger
328+compilerDebugEntries :: String -> [String] -> Compiler ()
329+compilerDebugEntries msg = compilerDebugLog . (msg:) . map indent
330+ where
331+ indent = unlines . map (" "++) . lines
246332
247333
248334--------------------------------------------------------------------------------
249335compilerTellDependencies :: [Dependency] -> Compiler ()
250336compilerTellDependencies ds = do
251− logger <- compilerLogger <$> compilerAsk
252− forM_ ds $ \d -> compilerUnsafeIO $ Logger.debug logger $
253− "Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d
337+ compilerDebugLog $ map (\d ->
338+ "Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d) ds
254339 compilerTell mempty {compilerDependencies = ds}
255340{-# INLINE compilerTellDependencies #-}
256341
lib/Hakyll/Core/Compiler/Require.hs+2
@@ -91,6 +91,7 @@ loadBody id' = loadSnapshotBody id' final
9191
9292
9393--------------------------------------------------------------------------------
94+-- | A shortcut for only requiring the body for a specific snapshot of an item
9495loadSnapshotBody :: (Binary a, Typeable a)
9596 => Identifier -> Snapshot -> Compiler a
9697loadSnapshotBody id' snapshot = fmap itemBody $ loadSnapshot id' snapshot
@@ -103,6 +104,7 @@ loadAll pattern = loadAllSnapshots pattern final
103104
104105
105106--------------------------------------------------------------------------------
107+-- | Load a specific snapshot for each of dynamic list of items
106108loadAllSnapshots :: (Binary a, Typeable a)
107109 => Pattern -> Snapshot -> Compiler [Item a]
108110loadAllSnapshots pattern snapshot = do
lib/Hakyll/Core/Logger.hs+9−3
@@ -18,6 +18,7 @@ import Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)
1818import Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)
1919import Control.Monad (forever)
2020import Control.Monad.Trans (MonadIO, liftIO)
21+import Data.List (intercalate)
2122import Prelude hiding (error)
2223
2324
@@ -79,7 +80,7 @@ string l v m
7980
8081--------------------------------------------------------------------------------
8182error :: MonadIO m => Logger -> String -> m ()
82−error l m = string l Error $ " [ERROR] " ++ m
83+error l m = string l Error $ " [ERROR] " ++ indent m
8384
8485
8586--------------------------------------------------------------------------------
@@ -89,9 +90,14 @@ header l = string l Message
8990
9091--------------------------------------------------------------------------------
9192message :: MonadIO m => Logger -> String -> m ()
92−message l m = string l Message $ " " ++ m
93+message l m = string l Message $ " " ++ indent m
9394
9495
9596--------------------------------------------------------------------------------
9697debug :: 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
2727import Hakyll.Core.Metadata
2828import Hakyll.Core.Provider.Internal
2929import System.IO as IO
30+import System.IO.Error (modifyIOError, ioeSetLocation)
3031
3132
3233--------------------------------------------------------------------------------
@@ -51,7 +52,7 @@ loadMetadata p identifier = do
5152--------------------------------------------------------------------------------
5253loadMetadataHeader :: FilePath -> IO (Metadata, String)
5354loadMetadataHeader fp = do
54− fileContent <- readFile fp
55+ fileContent <- modifyIOError (`ioeSetLocation` "loadMetadataHeader") $ readFile fp
5556 case parsePage fileContent of
5657 Right x -> return x
5758 Left err -> throwIO $ MetadataException fp err
@@ -60,7 +61,7 @@ loadMetadataHeader fp = do
6061--------------------------------------------------------------------------------
6162loadMetadataFile :: FilePath -> IO Metadata
6263loadMetadataFile fp = do
63− fileContent <- B.readFile fp
64+ fileContent <- modifyIOError (`ioeSetLocation` "loadMetadataFile") $ B.readFile fp
6465 let errOrMeta = Yaml.decodeEither' fileContent
6566 either (fail . show) return errOrMeta
6667
lib/Hakyll/Core/Runtime.hs+3−3
@@ -199,9 +199,9 @@ chase trail id'
199199 result <- liftIO $ runCompiler compiler read'
200200 case result of
201201 -- Rethrow error
202− CompilerError [] -> throwError
203− "Compiler failed but no info given, try running with -v?"
204− CompilerError es -> throwError $ intercalate "; " es
202+ CompilerError e -> throwError $ case compilerErrorMessages e of
203+ [] -> "Compiler failed but no info given, try running with -v?"
204+ es -> intercalate "; " es
205205
206206 -- Signal that a snapshot was saved ->
207207 CompilerSnapshot snapshot c -> do
lib/Hakyll/Core/Store.hs+20−20
@@ -16,7 +16,6 @@ module Hakyll.Core.Store
1616
1717
1818--------------------------------------------------------------------------------
19−import Control.Exception (IOException, handle)
2019import qualified Crypto.Hash.MD5 as MD5
2120import Data.Binary (Binary, decode, encodeFile)
2221import qualified Data.ByteString as B
@@ -32,6 +31,8 @@ import System.Directory (createDirectoryIfMissing)
3231import System.Directory (doesFileExist, removeFile)
3332import System.FilePath ((</>))
3433import System.IO (IOMode (..), hClose, openFile)
34+import System.IO.Error (catchIOError, ioeSetFileName,
35+ ioeSetLocation, modifyIOError)
3536
3637
3738--------------------------------------------------------------------------------
@@ -84,6 +85,14 @@ new inMemory directory = do
8485 where
8586 csize = Just 500
8687
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)
8796
8897--------------------------------------------------------------------------------
8998-- | Auxiliary: add an item to the in-memory cache
@@ -124,17 +133,16 @@ cacheDelete (Store _ (Just lru)) key = do
124133--------------------------------------------------------------------------------
125134-- | Store an item
126135set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()
127−set store identifier value = do
128− encodeFile (storeDirectory store </> key) value
136+set store identifier value = withStore store "set" (\key path -> do
137+ encodeFile path value
129138 cacheInsert store key value
130− where
131− key = hash identifier
139+ ) identifier
132140
133141
134142--------------------------------------------------------------------------------
135143-- | Load an item
136144get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)
137−get store identifier = do
145+get store = withStore store "get" $ \key path -> do
138146 -- First check the in-memory map
139147 ref <- cacheLookup store key
140148 case ref of
@@ -146,17 +154,14 @@ get store identifier = do
146154 then return NotFound
147155 -- Found in the filesystem
148156 else do
149− v <- decodeClose
157+ v <- decodeClose path
150158 cacheInsert store key v
151159 return $ Found v
152160 -- Found in the in-memory map (or wrong type), just return
153161 s -> return s
154162 where
155− key = hash identifier
156− path = storeDirectory store </> key
157−
158163 -- 'decodeFile' from Data.Binary which closes the file ASAP
159− decodeClose = do
164+ decodeClose path = do
160165 h <- openFile path ReadMode
161166 lbs <- BL.hGetContents h
162167 BL.length lbs `seq` hClose h
@@ -166,28 +171,23 @@ get store identifier = do
166171--------------------------------------------------------------------------------
167172-- | Strict function
168173isMember :: Store -> [String] -> IO Bool
169−isMember store identifier = do
174+isMember store = withStore store "isMember" $ \key path -> do
170175 inCache <- cacheIsMember store key
171176 if inCache then return True else doesFileExist path
172− where
173− key = hash identifier
174− path = storeDirectory store </> key
175177
176178
177179--------------------------------------------------------------------------------
178180-- | Delete an item
179181delete :: Store -> [String] -> IO ()
180−delete store identifier = do
182+delete store = withStore store "delete" $ \key path -> do
181183 cacheDelete store key
182− deleteFile $ storeDirectory store </> key
183− where
184− key = hash identifier
184+ deleteFile path
185185
186186
187187--------------------------------------------------------------------------------
188188-- | Delete a file unless it doesn't exist...
189189deleteFile :: FilePath -> IO ()
190−deleteFile = handle (\(_ :: IOException) -> return ()) . removeFile
190+deleteFile = (`catchIOError` \_ -> return ()) . removeFile
191191
192192
193193--------------------------------------------------------------------------------
lib/Hakyll/Web/Feed.hs+24−31
@@ -1,4 +1,5 @@
1−{-# LANGUAGE TemplateHaskell #-}
1+{-# LANGUAGE OverloadedStrings #-}
2+{-# LANGUAGE TemplateHaskell #-}
23
34--------------------------------------------------------------------------------
45-- | A Module that allows easy rendering of RSS feeds.
@@ -37,27 +38,26 @@ import Hakyll.Web.Template.List
3738
3839
3940--------------------------------------------------------------------------------
40−import Data.FileEmbed (makeRelativeToProject, embedFile)
41−import qualified Data.Text as T
42−import qualified Data.Text.Encoding as T
41+import Data.FileEmbed (makeRelativeToProject)
4342
4443
4544--------------------------------------------------------------------------------
46−rssTemplate :: String
47−rssTemplate = T.unpack $
48− T.decodeUtf8 $(makeRelativeToProject "data/templates/rss.xml" >>= embedFile)
45+rssTemplate :: Template
46+rssTemplate =
47+ $(makeRelativeToProject "data/templates/rss.xml" >>= embedTemplate)
4948
50−rssItemTemplate :: String
51−rssItemTemplate = T.unpack $
52− T.decodeUtf8 $(makeRelativeToProject "data/templates/rss-item.xml" >>= embedFile)
49+rssItemTemplate :: Template
50+rssItemTemplate =
51+ $(makeRelativeToProject "data/templates/rss-item.xml" >>= embedTemplate)
5352
54−atomTemplate :: String
55−atomTemplate = T.unpack $
56− T.decodeUtf8 $(makeRelativeToProject "data/templates/atom.xml" >>= embedFile)
53+atomTemplate :: Template
54+atomTemplate =
55+ $(makeRelativeToProject "data/templates/atom.xml" >>= embedTemplate)
56+
57+atomItemTemplate :: Template
58+atomItemTemplate =
59+ $(makeRelativeToProject "data/templates/atom-item.xml" >>= embedTemplate)
5760
58−atomItemTemplate :: String
59−atomItemTemplate = T.unpack $
60− T.decodeUtf8 $(makeRelativeToProject "data/templates/atom-item.xml" >>= embedFile)
6161
6262--------------------------------------------------------------------------------
6363-- | This is a data structure to keep the configuration of a feed.
@@ -77,16 +77,13 @@ data FeedConfiguration = FeedConfiguration
7777
7878--------------------------------------------------------------------------------
7979-- | Abstract function to render any feed.
80−renderFeed :: String -- ^ Default feed template
81− -> String -- ^ Default item template
80+renderFeed :: Template -- ^ Default feed template
81+ -> Template -- ^ Default item template
8282 -> FeedConfiguration -- ^ Feed configuration
8383 -> Context String -- ^ Context for the items
8484 -> [Item String] -- ^ Input items
8585 -> Compiler (Item String) -- ^ Resulting item
86−renderFeed defFeed defItem config itemContext items = do
87− feedTpl <- readTemplateFile defFeed
88− itemTpl <- readTemplateFile defItem
89−
86+renderFeed feedTpl itemTpl config itemContext items = do
9087 protectedItems <- mapM (applyFilter protectCDATA) items
9188 body <- makeItem =<< applyTemplateList itemTpl itemContext' protectedItems
9289 applyTemplate feedTpl feedContext body
@@ -120,18 +117,14 @@ renderFeed defFeed defItem config itemContext items = do
120117 updatedField = field "updated" $ \_ -> case items of
121118 [] -> return "Unknown"
122119 (x : _) -> unContext itemContext' "updated" [] x >>= \cf -> case cf of
123− ListField _ _ -> fail "Hakyll.Web.Feed.renderFeed: Internal error"
124120 StringField s -> return s
125−
126− readTemplateFile :: String -> Compiler Template
127− readTemplateFile value = pure $ template $ readTemplateElems value
128−
121+ _ -> fail "Hakyll.Web.Feed.renderFeed: Internal error"
129122
130123--------------------------------------------------------------------------------
131124-- | Render an RSS feed using given templates with a number of items.
132125renderRssWithTemplates ::
133− String -- ^ Feed template
134− -> String -- ^ Item template
126+ Template -- ^ Feed template
127+ -> Template -- ^ Item template
135128 -> FeedConfiguration -- ^ Feed configuration
136129 -> Context String -- ^ Item context
137130 -> [Item String] -- ^ Feed items
@@ -144,8 +137,8 @@ renderRssWithTemplates feedTemplate itemTemplate config context = renderFeed
144137--------------------------------------------------------------------------------
145138-- | Render an Atom feed using given templates with a number of items.
146139renderAtomWithTemplates ::
147− String -- ^ Feed template
148− -> String -- ^ Item template
140+ Template -- ^ Feed template
141+ -> Template -- ^ Item template
149142 -> FeedConfiguration -- ^ Feed configuration
150143 -> Context String -- ^ Item context
151144 -> [Item String] -- ^ Feed items
lib/Hakyll/Web/Template.hs+24−2
@@ -138,19 +138,41 @@
138138-- > 3...2...1
139139-- > </p>
140140--
141+{-# LANGUAGE TemplateHaskell #-}
141142module Hakyll.Web.Template
142143 ( Template
143− , template
144− , readTemplateElems
145144 , templateBodyCompiler
146145 , templateCompiler
147146 , applyTemplate
148147 , loadAndApplyTemplate
149148 , applyAsTemplate
150149 , readTemplate
150+ , compileTemplateItem
151151 , unsafeReadTemplateFile
152+ , embedTemplate
152153 ) where
153154
154155
155156--------------------------------------------------------------------------------
156157import 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+101−31
@@ -1,3 +1,23 @@
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+
121--------------------------------------------------------------------------------
222{-# LANGUAGE CPP #-}
323{-# LANGUAGE ExistentialQuantification #-}
@@ -50,13 +70,16 @@ import Hakyll.Core.Metadata
5070import Hakyll.Core.Provider
5171import Hakyll.Core.Util.String (needlePrefix, splitAll)
5272import Hakyll.Web.Html
53−import System.FilePath (splitDirectories, takeBaseName, dropExtension)
73+import Prelude hiding (id)
74+import System.FilePath (dropExtension, splitDirectories,
75+ takeBaseName)
5476
5577
5678--------------------------------------------------------------------------------
5779-- | Mostly for internal usage
5880data ContextField
59− = StringField String
81+ = EmptyField
82+ | StringField String
6083 | forall a. ListField (Context a) [Item a]
6184
6285
@@ -81,6 +104,8 @@ newtype Context a = Context
81104
82105
83106--------------------------------------------------------------------------------
107+-- | Tries to find a key in the left context,
108+-- or when that fails in the right context.
84109#if MIN_VERSION_base(4,9,0)
85110instance Semigroup (Context a) where
86111 (<>) (Context f) (Context g) = Context $ \k a i -> f k a i <|> g k a i
@@ -97,64 +122,101 @@ instance Monoid (Context a) where
97122
98123--------------------------------------------------------------------------------
99124field' :: String -> (Item a -> Compiler ContextField) -> Context a
100−field' key value = Context $ \k _ i -> if k == key then value i else empty
125+field' key value = Context $ \k _ i ->
126+ if k == key
127+ then value i
128+ else noResult $ "Tried field " ++ key
101129
102130
103131--------------------------------------------------------------------------------
104−-- | Constructs a new field in the 'Context.'
132+-- | Constructs a new field for a 'Context'.
133+-- If the key matches, the compiler is run and its result is substituted in the
134+-- template.
135+--
136+-- If the compiler fails, the field will be considered non-existent
137+-- in an @$if()$@ macro or ultimately break the template application
138+-- (unless the key is found in another context when using '<>').
139+-- Use 'empty' or 'noResult' for intentional failures of fields used in
140+-- @$if()$@, to distinguish them from exceptions thrown with 'fail'.
105141field
106142 :: String -- ^ Key
107143 -> (Item a -> Compiler String) -- ^ Function that constructs a value based
108− -- on the item
144+ -- on the item (e.g. accessing metadata)
109145 -> Context a
110146field key value = field' key (fmap StringField . value)
111147
112148
113149--------------------------------------------------------------------------------
114150-- | Creates a 'field' to use with the @$if()$@ template macro.
151+-- Attempting to substitute the field into the template will cause an error.
115152boolField
116153 :: String
117154 -> (Item a -> Bool)
118155 -> Context a
119−boolField name f = field name (\i -> if f i
120− then pure (error $ unwords ["no string value for bool field:",name])
121− else empty)
156+boolField name f = field' name (\i -> if f i
157+ then return EmptyField
158+ else noResult $ "Field " ++ name ++ " is false")
122159
123160
124161--------------------------------------------------------------------------------
125−-- | Creates a 'field' that does not depend on the 'Item'
126−constField :: String -> String -> Context a
162+-- | Creates a 'field' that does not depend on the 'Item' but always yields
163+-- the same string
164+constField :: String -- ^ Key
165+ -> String -- ^ Value
166+ -> Context a
127167constField key = field key . const . return
128168
129169
130170--------------------------------------------------------------------------------
171+-- | Creates a list field to be consumed by a @$for(…)$@ expression.
172+-- The compiler returns multiple items which are rendered in the loop body
173+-- with the supplied context.
131174listField :: String -> Context a -> Compiler [Item a] -> Context b
132175listField key c xs = listFieldWith key c (const xs)
133176
134177
135178--------------------------------------------------------------------------------
179+-- | Creates a list field like 'listField', but supplies the current page
180+-- to the compiler.
136181listFieldWith
137182 :: String -> Context a -> (Item b -> Compiler [Item a]) -> Context b
138183listFieldWith key c f = field' key $ fmap (ListField c) . f
139184
140185
141186--------------------------------------------------------------------------------
142−functionField :: String -> ([String] -> Item a -> Compiler String) -> Context a
187+-- | Creates a variadic function field.
188+--
189+-- The function will be called with the dynamically evaluated string arguments
190+-- from the template as well as the page that is currently rendered.
191+functionField :: String -- ^ Key
192+ -> ([String] -> Item a -> Compiler String) -- ^ Function
193+ -> Context a
143194functionField name value = Context $ \k args i ->
144195 if k == name
145196 then StringField <$> value args i
146− else empty
197+ else noResult $ "Tried function field " ++ name
147198
148199
149200--------------------------------------------------------------------------------
201+-- | Transform the respective string results of all fields in a context.
202+-- For example,
203+--
204+-- > mapContext (++"c") (constField "x" "a" <> constField "y" "b")
205+--
206+-- is equivalent to
207+--
208+-- > constField "x" "ac" <> constField "y" "bc"
209+--
150210mapContext :: (String -> String) -> Context a -> Context a
151211mapContext f (Context c) = Context $ \k a i -> do
152212 fld <- c k a i
153213 case fld of
214+ EmptyField -> wrongType "boolField"
154215 StringField str -> return $ StringField (f str)
155− ListField _ _ -> fail $
156− "Hakyll.Web.Template.Context.mapContext: " ++
157− "can't map over a ListField!"
216+ _ -> wrongType "ListField"
217+ where
218+ wrongType typ = fail $ "Hakyll.Web.Template.Context.mapContext: " ++
219+ "can't map over a " ++ typ ++ "!"
158220
159221--------------------------------------------------------------------------------
160222-- | A context that allows snippet inclusion. In processed file, use as:
@@ -163,15 +225,15 @@ mapContext f (Context c) = Context $ \k a i -> do
163225-- > $snippet("path/to/snippet/")$
164226-- > ...
165227--
166−-- The contents of the included file will not be interpolated.
228+-- The contents of the included file will not be interpolated like @partial@
229+-- does it.
167230--
168231snippetField :: Context String
169232snippetField = functionField "snippet" f
170233 where
171234 f [contentsPath] _ = loadBody (fromFilePath contentsPath)
172− f _ i = error $
173− "Too many arguments to function 'snippet()' in item " ++
174− show (itemIdentifier i)
235+ f [] _ = fail "No argument to function 'snippet()'"
236+ f _ _ = fail "Too many arguments to function 'snippet()'"
175237
176238--------------------------------------------------------------------------------
177239-- | A context that contains (in that order)
@@ -191,8 +253,7 @@ defaultContext =
191253 metadataField `mappend`
192254 urlField "url" `mappend`
193255 pathField "path" `mappend`
194− titleField "title" `mappend`
195− missingField
256+ titleField "title"
196257
197258
198259--------------------------------------------------------------------------------
@@ -210,15 +271,20 @@ bodyField key = field key $ return . itemBody
210271-- | Map any field to its metadata value, if present
211272metadataField :: Context a
212273metadataField = Context $ \k _ i -> do
213− value <- getMetadataField (itemIdentifier i) k
214− maybe empty (return . StringField) value
274+ let id = itemIdentifier i
275+ empty' = noResult $ "No '" ++ k ++ "' field in metadata " ++
276+ "of item " ++ show id
277+ value <- getMetadataField id k
278+ maybe empty' (return . StringField) value
215279
216280
217281--------------------------------------------------------------------------------
218282-- | Absolute url to the resulting item
219283urlField :: String -> Context a
220−urlField key = field key $
221− fmap (maybe empty toUrl) . getRoute . itemIdentifier
284+urlField key = field key $ \i -> do
285+ let id = itemIdentifier i
286+ empty' = fail $ "No route url found for item " ++ show id
287+ fmap (maybe empty' toUrl) $ getRoute id
222288
223289
224290--------------------------------------------------------------------------------
@@ -272,8 +338,8 @@ titleField = mapContext takeBaseName . pathField
272338--
273339-- As another alternative, if none of the above matches, and the file has a
274340-- path which contains nested directories specifying a date, then that date
275−-- will be used. In other words, if the path is of the form
276−-- @**//yyyy//mm//dd//**//main.extension@ .
341+-- will be used. In other words, if the path is of the form
342+-- @**//yyyy//mm//dd//**//main.extension@ .
277343-- As above, in case of multiple matches, the rightmost one is used.
278344
279345dateField :: String -- ^ Key in which the rendered date should be placed
@@ -285,7 +351,7 @@ dateField = dateFieldWith defaultTimeLocale
285351--------------------------------------------------------------------------------
286352-- | This is an extended version of 'dateField' that allows you to
287353-- specify a time locale that is used for outputting the date. For more
288−-- details, see 'dateField'.
354+-- details, see 'dateField' and 'formatTime'.
289355dateFieldWith :: TimeLocale -- ^ Output time locale
290356 -> String -- ^ Destination key
291357 -> String -- ^ Format to use on the date
@@ -340,6 +406,7 @@ getItemModificationTime identifier = do
340406
341407
342408--------------------------------------------------------------------------------
409+-- | Creates a field with the last modification date of the underlying item.
343410modificationTimeField :: String -- ^ Key
344411 -> String -- ^ Format
345412 -> Context a -- ^ Resulting context
@@ -347,6 +414,8 @@ modificationTimeField = modificationTimeFieldWith defaultTimeLocale
347414
348415
349416--------------------------------------------------------------------------------
417+-- | Creates a field with the last modification date of the underlying item
418+-- in a custom localisation format (see 'formatTime').
350419modificationTimeFieldWith :: TimeLocale -- ^ Time output locale
351420 -> String -- ^ Key
352421 -> String -- ^ Format
@@ -385,10 +454,11 @@ teaserFieldWithSeparator separator key snapshot = field key $ \item -> do
385454
386455
387456--------------------------------------------------------------------------------
457+-- | Constantly reports any field as missing. Mostly for internal usage,
458+-- it is the last choice in every context used in a template application.
388459missingField :: Context a
389−missingField = Context $ \k _ i -> fail $
390− "Missing field $" ++ k ++ "$ in context for item " ++
391− show (itemIdentifier i)
460+missingField = Context $ \k _ _ -> noResult $
461+ "Missing field '" ++ k ++ "' in context"
392462
393463parseTimeM :: Bool -> TimeLocale -> String -> String -> Maybe UTCTime
394464#if MIN_VERSION_time(1,5,0)
lib/Hakyll/Web/Template/Internal.hs+101−43
@@ -1,15 +1,18 @@
1+{-# LANGUAGE DeriveAnyClass #-}
2+{-# LANGUAGE DeriveGeneric #-}
13{-# LANGUAGE GeneralizedNewtypeDeriving #-}
24{-# LANGUAGE ScopedTypeVariables #-}
5+
36module Hakyll.Web.Template.Internal
47 ( Template (..)
58 , template
69 , templateBodyCompiler
710 , templateCompiler
811 , applyTemplate
9− , applyTemplate'
1012 , loadAndApplyTemplate
1113 , applyAsTemplate
1214 , readTemplate
15+ , compileTemplateItem
1316 , unsafeReadTemplateFile
1417
1518 , module Hakyll.Web.Template.Internal.Element
@@ -18,16 +21,18 @@ module Hakyll.Web.Template.Internal
1821
1922
2023--------------------------------------------------------------------------------
21−import Control.Monad.Except (MonadError (..))
24+import Control.Monad.Except (catchError)
2225import Data.Binary (Binary)
2326import Data.List (intercalate)
27+import qualified Data.List.NonEmpty as NonEmpty
2428import Data.Typeable (Typeable)
2529import GHC.Exts (IsString (..))
26−import Prelude hiding (id)
30+import GHC.Generics (Generic)
2731
2832
2933--------------------------------------------------------------------------------
3034import Hakyll.Core.Compiler
35+import Hakyll.Core.Compiler.Internal
3136import Hakyll.Core.Identifier
3237import Hakyll.Core.Item
3338import Hakyll.Core.Writable
@@ -38,9 +43,10 @@ import Hakyll.Web.Template.Internal.Trim
3843
3944--------------------------------------------------------------------------------
4045-- | Datatype used for template substitutions.
41−newtype Template = Template
42− { unTemplate :: [TemplateElement]
43− } deriving (Show, Eq, Binary, Typeable)
46+data Template = Template
47+ { tplElements :: [TemplateElement]
48+ , tplOrigin :: FilePath -- Only for error messages.
49+ } deriving (Show, Eq, Generic, Binary, Typeable)
4450
4551
4652--------------------------------------------------------------------------------
@@ -56,39 +62,68 @@ instance IsString Template where
5662
5763--------------------------------------------------------------------------------
5864-- | Wrap the constructor to ensure trim is called.
59−template :: [TemplateElement] -> Template
60−template = Template . trim
65+template :: FilePath -> [TemplateElement] -> Template
66+template p = flip Template p . trim
6167
6268
6369--------------------------------------------------------------------------------
70+-- | Parse a string into a template.
71+-- You should prefer 'compileTemplateItem' over this.
6472readTemplate :: String -> Template
65−readTemplate = Template . trim . readTemplateElems
73+readTemplate = either error (template origin) . parseTemplateElemsFile origin
74+ where
75+ origin = "{literal}"
76+{-# DEPRECATED readTemplate "Use templateCompiler instead" #-}
77+
78+--------------------------------------------------------------------------------
79+-- | Parse an item body into a template.
80+-- Provides useful error messages in the 'Compiler' monad.
81+compileTemplateItem :: Item String -> Compiler Template
82+compileTemplateItem item = let file = itemIdentifier item
83+ in compileTemplateFile file (itemBody item)
84+
85+--------------------------------------------------------------------------------
86+compileTemplateFile :: Identifier -> String -> Compiler Template
87+compileTemplateFile file = either fail (return . template origin)
88+ . parseTemplateElemsFile origin
89+ where
90+ origin = show file
6691
6792--------------------------------------------------------------------------------
6893-- | Read a template, without metadata header
6994templateBodyCompiler :: Compiler (Item Template)
7095templateBodyCompiler = cached "Hakyll.Web.Template.templateBodyCompiler" $ do
7196 item <- getResourceBody
72− file <- getResourceFilePath
73− return $ fmap (template . readTemplateElemsFile file) item
97+ file <- getUnderlying
98+ withItemBody (compileTemplateFile file) item
7499
75100--------------------------------------------------------------------------------
76101-- | Read complete file contents as a template
77102templateCompiler :: Compiler (Item Template)
78103templateCompiler = cached "Hakyll.Web.Template.templateCompiler" $ do
79104 item <- getResourceString
80− file <- getResourceFilePath
81− return $ fmap (template . readTemplateElemsFile file) item
105+ file <- getUnderlying
106+ withItemBody (compileTemplateFile file) item
82107
83108
84109--------------------------------------------------------------------------------
110+-- | Interpolate template expressions from context values in a page
85111applyTemplate :: Template -- ^ Template
86112 -> Context a -- ^ Context
87113 -> Item a -- ^ Page
88114 -> Compiler (Item String) -- ^ Resulting item
89115applyTemplate tpl context item = do
90− body <- applyTemplate' (unTemplate tpl) context item
116+ body <- applyTemplate' (tplElements tpl) context item `catchError` handler
91117 return $ itemSetBody body item
118+ where
119+ tplName = tplOrigin tpl
120+ itemName = show $ itemIdentifier item
121+ handler es = fail $ "Hakyll.Web.Template.applyTemplate: Failed to " ++
122+ (if tplName == itemName
123+ then "interpolate template in item " ++ itemName
124+ else "apply template " ++ tplName ++ " to item " ++ itemName) ++
125+ ":\n" ++ intercalate ",\n" es
126+
92127
93128
94129--------------------------------------------------------------------------------
@@ -105,9 +140,6 @@ applyTemplate' tes context x = go tes
105140
106141 go = fmap concat . mapM applyElem
107142
108− trimError = error $ "Hakyll.Web.Template.applyTemplate: template not " ++
109− "fully trimmed."
110−
111143 ---------------------------------------------------------------------------
112144
113145 applyElem :: TemplateElement -> Compiler String
@@ -118,29 +150,43 @@ applyTemplate' tes context x = go tes
118150
119151 applyElem (Chunk c) = return c
120152
121− applyElem (Expr e) = applyExpr e >>= getString e
153+ applyElem (Expr e) = withErrorMessage evalMsg (applyStringExpr typeMsg e)
154+ where
155+ evalMsg = "In expr '$" ++ show e ++ "$'"
156+ typeMsg = "expr '$" ++ show e ++ "$'"
122157
123158 applyElem Escaped = return "$"
124159
125− applyElem (If e t mf) = (applyExpr e >> go t) `catchError` handler
160+ applyElem (If e t mf) = compilerTry (applyExpr e) >>= handle
126161 where
127− handler _ = case mf of
128− Nothing -> return ""
129− Just f -> go f
130−
131− applyElem (For e b s) = applyExpr e >>= \cf -> case cf of
132− StringField _ -> fail $
133− "Hakyll.Web.Template.applyTemplateWith: expected ListField but " ++
134− "got StringField for expr " ++ show e
135− ListField c xs -> do
162+ f = maybe (return "") go mf
163+ handle (Right _) = go t
164+ handle (Left (CompilationNoResult _)) = f
165+ handle (Left (CompilationFailure es)) = debug (NonEmpty.toList es) >> f
166+ debug = compilerDebugEntries ("Hakyll.Web.Template.applyTemplate: " ++
167+ "[ERROR] in 'if' condition on expr '" ++ show e ++ "':")
168+
169+ applyElem (For e b s) = withErrorMessage headMsg (applyExpr e) >>= \cf -> case cf of
170+ EmptyField -> expected "list" "boolean" typeMsg
171+ StringField _ -> expected "list" "string" typeMsg
172+ ListField c xs -> withErrorMessage bodyMsg $ do
136173 sep <- maybe (return "") go s
137174 bs <- mapM (applyTemplate' b c) xs
138175 return $ intercalate sep bs
139−
140− applyElem (Partial e) = do
141− p <- applyExpr e >>= getString e
142− Template tpl' <- loadBody (fromFilePath p)
143− applyTemplate' tpl' context x
176+ where
177+ headMsg = "In expr '$for(" ++ show e ++ ")$'"
178+ typeMsg = "loop expr '" ++ show e ++ "'"
179+ bodyMsg = "In loop context of '$for(" ++ show e ++ ")$'"
180+
181+ applyElem (Partial e) = withErrorMessage headMsg $
182+ applyStringExpr typeMsg e >>= \p ->
183+ withErrorMessage inclMsg $ do
184+ tpl' <- loadBody (fromFilePath p)
185+ itemBody <$> applyTemplate tpl' context x
186+ where
187+ headMsg = "In expr '$partial(" ++ show e ++ ")$'"
188+ typeMsg = "partial expr '" ++ show e ++ "'"
189+ inclMsg = "In inclusion of '$partial(" ++ show e ++ ")$'"
144190
145191 ---------------------------------------------------------------------------
146192
@@ -149,17 +195,29 @@ applyTemplate' tes context x = go tes
149195 applyExpr (Ident (TemplateKey k)) = context' k [] x
150196
151197 applyExpr (Call (TemplateKey k) args) = do
152− args' <- mapM (\e -> applyExpr e >>= getString e) args
198+ args' <- mapM (\e -> applyStringExpr (typeMsg e) e) args
153199 context' k args' x
200+ where
201+ typeMsg e = "argument '" ++ show e ++ "'"
154202
155203 applyExpr (StringLiteral s) = return (StringField s)
156204
157205 ----------------------------------------------------------------------------
158206
159− getString _ (StringField s) = return s
160− getString e (ListField _ _) = fail $
161− "Hakyll.Web.Template.applyTemplateWith: expected StringField but " ++
162− "got ListField for expr " ++ show e
207+ applyStringExpr :: String -> TemplateExpr -> Compiler String
208+ applyStringExpr msg expr =
209+ applyExpr expr >>= getString
210+ where
211+ getString EmptyField = expected "string" "boolean" msg
212+ getString (StringField s) = return s
213+ getString (ListField _ _) = expected "string" "list" msg
214+
215+ expected typ act expr = fail $ unwords ["Hakyll.Web.Template.applyTemplate:",
216+ "expected", typ, "but got", act, "for", expr]
217+
218+ -- expected to never happen with all templates constructed by 'template'
219+ trimError = fail $
220+ "Hakyll.Web.Template.applyTemplate: template not fully trimmed."
163221
164222
165223--------------------------------------------------------------------------------
@@ -189,14 +247,14 @@ loadAndApplyTemplate identifier context item = do
189247applyAsTemplate :: Context String -- ^ Context
190248 -> Item String -- ^ Item and template
191249 -> Compiler (Item String) -- ^ Resulting item
192−applyAsTemplate context item =
193− let tpl = template $ readTemplateElemsFile file (itemBody item)
194− file = toFilePath $ itemIdentifier item
195− in applyTemplate tpl context item
250+applyAsTemplate context item = do
251+ tpl <- compileTemplateItem item
252+ applyTemplate tpl context item
196253
197254
198255--------------------------------------------------------------------------------
199256unsafeReadTemplateFile :: FilePath -> Compiler Template
200257unsafeReadTemplateFile file = do
201258 tpl <- unsafeCompiler $ readFile file
202− pure $ template $ readTemplateElemsFile file tpl
259+ compileTemplateFile (fromFilePath file) tpl
260+{-# DEPRECATED unsafeReadTemplateFile "Use templateCompiler" #-}
lib/Hakyll/Web/Template/Internal/Element.hs+7−14
@@ -7,14 +7,14 @@ module Hakyll.Web.Template.Internal.Element
77 , TemplateExpr (..)
88 , TemplateElement (..)
99 , templateElems
10− , readTemplateElems
11− , readTemplateElemsFile
10+ , parseTemplateElemsFile
1211 ) where
1312
1413
1514--------------------------------------------------------------------------------
16−import Control.Applicative ((<|>))
15+import Control.Applicative ((<|>), (<*))
1716import Control.Monad (void)
17+import Control.Arrow (left)
1818import Data.Binary (Binary, get, getWord8, put, putWord8)
1919import Data.List (intercalate)
2020import Data.Maybe (isJust)
@@ -107,17 +107,10 @@ instance Binary TemplateExpr where
107107 2 -> StringLiteral <$> get
108108 _ -> error "Hakyll.Web.Template.Internal: Error reading cached template"
109109
110−
111−--------------------------------------------------------------------------------
112−readTemplateElems :: String -> [TemplateElement]
113−readTemplateElems = readTemplateElemsFile "{literal}"
114−
115−
116110--------------------------------------------------------------------------------
117−readTemplateElemsFile :: FilePath -> String -> [TemplateElement]
118−readTemplateElemsFile file input = case P.parse templateElems file input of
119− Left err -> error $ "Cannot parse template: " ++ show err
120− Right t -> t
111+parseTemplateElemsFile :: FilePath -> String -> Either String [TemplateElement]
112+parseTemplateElemsFile file = left (\e -> "Cannot parse template " ++ show e)
113+ . P.parse (templateElems <* P.eof) file
121114
122115
123116--------------------------------------------------------------------------------
@@ -167,7 +160,7 @@ trimOpen = do
167160--------------------------------------------------------------------------------
168161trimClose :: P.Parser Bool
169162trimClose = do
170− trimIfR <- P.optionMaybe $ P.try (P.char '-')
163+ trimIfR <- P.optionMaybe $ (P.char '-')
171164 void $ P.char '$'
172165 pure $ isJust trimIfR
173166
tests/Hakyll/Core/UnixFilter/Tests.hs+6−16
@@ -6,18 +6,16 @@ module Hakyll.Core.UnixFilter.Tests
66
77
88--------------------------------------------------------------------------------
9−import Data.List (isInfixOf)
10−import Test.Tasty (TestTree, testGroup)
11−import Test.Tasty.HUnit (testCase)
12−import qualified Test.Tasty.HUnit as H
9+import Test.Tasty (TestTree, testGroup)
10+import Test.Tasty.HUnit (testCase)
11+import qualified Test.Tasty.HUnit as H
1312
1413
1514--------------------------------------------------------------------------------
1615import Hakyll.Core.Compiler
17−import Hakyll.Core.Compiler.Internal
16+import Hakyll.Core.Identifier
1817import Hakyll.Core.Item
1918import Hakyll.Core.UnixFilter
20−import Hakyll.Core.Identifier
2119import TestSuite.Util
2220
2321
@@ -51,10 +49,7 @@ unixFilterFalse :: H.Assertion
5149unixFilterFalse = do
5250 store <- newTestStore
5351 provider <- newTestProvider store
54− result <- testCompiler store provider testMarkdown compiler
55− case result of
56− CompilerError es -> True H.@=? any ("exit code" `isInfixOf`) es
57− _ -> H.assertFailure "Expecting CompilerError"
52+ testCompilerError store provider testMarkdown compiler "exit code"
5853 cleanTestEnv
5954 where
6055 compiler = getResourceString >>= withItemBody (unixFilter "false" [])
@@ -65,12 +60,7 @@ unixFilterError :: H.Assertion
6560unixFilterError = do
6661 store <- newTestStore
6762 provider <- newTestProvider store
68− result <- testCompiler store provider testMarkdown compiler
69− case result of
70− CompilerError es -> True H.@=? any containsIncorrectOptionMessage es
71− _ -> H.assertFailure "Expecting CompilerError"
63+ testCompilerError store provider testMarkdown compiler "option"
7264 cleanTestEnv
7365 where
7466 compiler = getResourceString >>= withItemBody (unixFilter "head" ["-#"])
75− incorrectOptionMessages = ["invalid option", "illegal option"]
76− containsIncorrectOptionMessage output = any (`isInfixOf` output) incorrectOptionMessages
tests/Hakyll/Web/Template/Context/Tests.hs+2−2
@@ -62,6 +62,6 @@ testContextDone store provider identifier key context =
6262 cf <- unContext context key [] item
6363 case cf of
6464 StringField str -> return str
65− ListField _ _ -> error $
65+ _ -> error $
6666 "Hakyll.Web.Template.Context.Tests.testContextDone: " ++
67− "Didn't expect ListField"
67+ "expected StringField"
tests/Hakyll/Web/Template/Tests.hs+48−16
@@ -1,5 +1,6 @@
11--------------------------------------------------------------------------------
22{-# LANGUAGE OverloadedStrings #-}
3+{-# LANGUAGE TemplateHaskell #-}
34module Hakyll.Web.Template.Tests
45 ( tests
56 ) where
@@ -7,9 +8,10 @@ module Hakyll.Web.Template.Tests
78
89--------------------------------------------------------------------------------
910import Test.Tasty (TestTree, testGroup)
10−import Test.Tasty.HUnit (Assertion, testCase, (@=?),
11− (@?=))
11+import Test.Tasty.HUnit (Assertion, assertBool, testCase,
12+ (@=?), (@?=))
1213
14+import Data.Either (isLeft)
1315
1416--------------------------------------------------------------------------------
1517import Hakyll.Core.Compiler
@@ -32,13 +34,13 @@ tests = testGroup "Hakyll.Web.Template.Tests" $ concat
3234 , testCase "applyJoinTemplateList" testApplyJoinTemplateList
3335 ]
3436
35− , fromAssertions "readTemplate"
36− [ [Chunk "Hello ", Expr (Call "guest" [])]
37− @=? readTemplateElems "Hello $guest()$"
38− , [If (Call "a" [StringLiteral "bar"]) [Chunk "foo"] Nothing]
39− @=? readTemplateElems "$if(a(\"bar\"))$foo$endif$"
37+ , fromAssertions "parseTemplate"
38+ [ Right [Chunk "Hello ", Expr (Call "guest" [])]
39+ @=? parse "Hello $guest()$"
40+ , Right [If (Call "a" [StringLiteral "bar"]) [Chunk "foo"] Nothing]
41+ @=? parse "$if(a(\"bar\"))$foo$endif$"
4042 -- 'If' trim check.
41− , [ TrimL
43+ , Right [ TrimL
4244 , If (Ident (TemplateKey "body"))
4345 [ TrimR
4446 , Chunk "\n"
@@ -54,29 +56,39 @@ tests = testGroup "Hakyll.Web.Template.Tests" $ concat
5456 ])
5557 , TrimR
5658 ]
57− @=? readTemplateElems "$-if(body)-$\n$body$\n$-else-$\n$body$\n$-endif-$"
59+ @=? parse "$-if(body)-$\n$body$\n$-else-$\n$body$\n$-endif-$"
5860 -- 'For' trim check.
59− , [ TrimL
61+ , Right [ TrimL
6062 , For (Ident (TemplateKey "authors"))
6163 [TrimR, Chunk "\n body \n", TrimL]
6264 Nothing
6365 , TrimR
6466 ]
65− @=? readTemplateElems "$-for(authors)-$\n body \n$-endfor-$"
67+ @=? parse "$-for(authors)-$\n body \n$-endfor-$"
6668 -- 'Partial' trim check.
67− , [ TrimL
69+ , Right [ TrimL
6870 , Partial (StringLiteral "path")
6971 , TrimR
7072 ]
71− @=? readTemplateElems "$-partial(\"path\")-$"
73+ @=? parse "$-partial(\"path\")-$"
7274 -- 'Expr' trim check.
73− , [ TrimL
75+ , Right [ TrimL
7476 , Expr (Ident (TemplateKey "foo"))
7577 , TrimR
7678 ]
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$"
7886 ]
87+
88+ , [testCase "embeddedTemplate" testEmbeddedTemplate]
7989 ]
90+ where
91+ parse = parseTemplateElemsFile ""
8092
8193
8294--------------------------------------------------------------------------------
@@ -113,6 +125,8 @@ testApplyJoinTemplateList :: Assertion
113125testApplyJoinTemplateList = do
114126 store <- newTestStore
115127 provider <- newTestProvider store
128+ tpl <- testCompilerDone store provider "tpl" $
129+ compileTemplateItem (Item "tpl" "<b>$body$</b>")
116130 str <- testCompilerDone store provider "item3" $
117131 applyJoinTemplateList ", " tpl defaultContext [i1, i2]
118132
@@ -121,4 +135,22 @@ testApplyJoinTemplateList = do
121135 where
122136 i1 = Item "item1" "Hello"
123137 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/Util.hs+11−2
@@ -6,6 +6,7 @@ module TestSuite.Util
66 , newTestProvider
77 , testCompiler
88 , testCompilerDone
9+ , testCompilerError
910 , testConfiguration
1011 , cleanTestEnv
1112 , renderParagraphs
@@ -13,7 +14,7 @@ module TestSuite.Util
1314
1415
1516--------------------------------------------------------------------------------
16−import Data.List (intercalate)
17+import Data.List (intercalate, isInfixOf)
1718import Data.Monoid (mempty)
1819import qualified Data.Set as S
1920import Test.Tasty
@@ -80,13 +81,21 @@ testCompilerDone store provider underlying compiler = do
8081 CompilerDone x _ -> return x
8182 CompilerError e -> fail $
8283 "TestSuite.Util.testCompilerDone: compiler " ++ show underlying ++
83− " threw: " ++ intercalate "; " e
84+ " threw: " ++ intercalate "; " (compilerErrorMessages e)
8485 CompilerRequire i _ -> fail $
8586 "TestSuite.Util.testCompilerDone: compiler " ++ show underlying ++
8687 " requires: " ++ show i
8788 CompilerSnapshot _ _ -> fail
8889 "TestSuite.Util.testCompilerDone: unexpected CompilerSnapshot"
8990
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"
9099
91100--------------------------------------------------------------------------------
92101testConfiguration :: Configuration
tests/data/embed.htmlnew file+1
@@ -0,0 +1 @@
1+<p>$body$</p>