on master help sign in
Merge branch 'master' into dev-metadata-route
61dcb5fJasper Van der Jeugt
hakyll.cabal+5−4
@@ -1,5 +1,5 @@
1Name: hakyll
1Name: hakyll
2−Version: 4.1.2.0
2+Version: 4.1.4.0
3
3
4Synopsis: A static website compiler library
4Synopsis: A static website compiler library
5Description:
5Description:
@@ -64,8 +64,10 @@ Data-files:
64Extra-source-files:
64Extra-source-files:
65 tests/data/example.md
65 tests/data/example.md
66 tests/data/example.md.metadata
66 tests/data/example.md.metadata
67+ tests/data/russian.md
67 tests/data/template.html
68 tests/data/template.html
68 tests/data/template.html.out
69 tests/data/template.html.out
70+ tests/data/posts/2010-08-26-birthday.md
69
71
70Source-Repository head
72Source-Repository head
71 Type: git
73 Type: git
@@ -93,7 +95,7 @@ Library
93 directory >= 1.0 && < 1.3,
95 directory >= 1.0 && < 1.3,
94 filepath >= 1.0 && < 1.4,
96 filepath >= 1.0 && < 1.4,
95 http-conduit >= 1.8 && < 1.9,
97 http-conduit >= 1.8 && < 1.9,
96− http-types >= 0.7 && < 0.8,
98+ http-types >= 0.7 && < 0.9,
97 lrucache >= 1.1.1 && < 1.2,
99 lrucache >= 1.1.1 && < 1.2,
98 mtl >= 1 && < 2.2,
100 mtl >= 1 && < 2.2,
99 old-locale >= 1.0 && < 1.1,
101 old-locale >= 1.0 && < 1.1,
@@ -149,7 +151,6 @@ Library
149 Hakyll.Core.Provider.Internal
151 Hakyll.Core.Provider.Internal
150 Hakyll.Core.Provider.Metadata
152 Hakyll.Core.Provider.Metadata
151 Hakyll.Core.Provider.MetadataCache
153 Hakyll.Core.Provider.MetadataCache
152− Hakyll.Core.Provider.Modified
153 Hakyll.Core.Rules.Internal
154 Hakyll.Core.Rules.Internal
154 Hakyll.Core.Runtime
155 Hakyll.Core.Runtime
155 Hakyll.Core.Store
156 Hakyll.Core.Store
@@ -192,7 +193,7 @@ Test-suite hakyll-tests
192 directory >= 1.0 && < 1.3,
193 directory >= 1.0 && < 1.3,
193 filepath >= 1.0 && < 1.4,
194 filepath >= 1.0 && < 1.4,
194 http-conduit >= 1.8 && < 1.9,
195 http-conduit >= 1.8 && < 1.9,
195− http-types >= 0.7 && < 0.8,
196+ http-types >= 0.7 && < 0.9,
196 lrucache >= 1.1.1 && < 1.2,
197 lrucache >= 1.1.1 && < 1.2,
197 mtl >= 1 && < 2.2,
198 mtl >= 1 && < 2.2,
198 old-locale >= 1.0 && < 1.1,
199 old-locale >= 1.0 && < 1.1,
src/Hakyll/Check.hs+2−1
@@ -94,7 +94,8 @@ runChecker checker config verbosity check' = do
94checkDestination :: Checker ()
94checkDestination :: Checker ()
95checkDestination = do
95checkDestination = do
96 config <- checkerConfig <$> ask
96 config <- checkerConfig <$> ask
97+ files <- liftIO $
97− files <- liftIO $ getRecursiveContents (destinationDirectory config)
98+ getRecursiveContents (const False) (destinationDirectory config)
98
99
99 let htmls =
100 let htmls =
100 [ destinationDirectory config </> file
101 [ destinationDirectory config </> file
src/Hakyll/Core/Compiler.hs+1−1
@@ -136,7 +136,7 @@ cached name compiler = do
136 id' <- compilerUnderlying <$> compilerAsk
136 id' <- compilerUnderlying <$> compilerAsk
137 store <- compilerStore <$> compilerAsk
137 store <- compilerStore <$> compilerAsk
138 provider <- compilerProvider <$> compilerAsk
138 provider <- compilerProvider <$> compilerAsk
139− modified <- compilerUnsafeIO $ resourceModified provider id'
139+ let modified = resourceModified provider id'
140 if modified
140 if modified
141 then do
141 then do
142 x <- compiler
142 x <- compiler
src/Hakyll/Core/Configuration.hs+1−1
@@ -37,7 +37,7 @@ data Configuration = Configuration
37 --
37 --
38 -- Note that the files in 'destinationDirectory' and 'storeDirectory' will
38 -- Note that the files in 'destinationDirectory' and 'storeDirectory' will
39 -- also be ignored. Note that this is the configuration parameter, if you
39 -- also be ignored. Note that this is the configuration parameter, if you
40− -- want to use the test, you should use 'shouldIgnoreFile'.
40+ -- want to use the test, you should use 'shouldIgnoreFile'.
41 --
41 --
42 ignoreFile :: FilePath -> Bool
42 ignoreFile :: FilePath -> Bool
43 , -- | Here, you can plug in a system command to upload/deploy your site.
43 , -- | Here, you can plug in a system command to upload/deploy your site.
src/Hakyll/Core/Provider.hs+22−26
@@ -3,44 +3,40 @@
3-- caching.
3-- caching.
4module Hakyll.Core.Provider
4module Hakyll.Core.Provider
5 ( -- * Constructing resource providers
5 ( -- * Constructing resource providers
6− Provider
6+ Internal.Provider
7 , newProvider
7 , newProvider
8
8
9 -- * Querying resource properties
9 -- * Querying resource properties
10− , resourceList
10+ , Internal.resourceList
11− , resourceExists
11+ , Internal.resourceExists
12− , resourceModified
12+ , Internal.resourceModified
13− , resourceModificationTime
13+ , Internal.resourceModificationTime
14
14
15 -- * Access to raw resource content
15 -- * Access to raw resource content
16− , resourceString
16+ , Internal.resourceString
17− , resourceLBS
17+ , Internal.resourceLBS
18
18
19 -- * Access to metadata and body content
19 -- * Access to metadata and body content
20− , resourceMetadata
20+ , Internal.resourceMetadata
21− , resourceBody
21+ , Internal.resourceBody
22 ) where
22 ) where
23
23
24
24
25--------------------------------------------------------------------------------
25--------------------------------------------------------------------------------
26−import Hakyll.Core.Identifier
27−import Hakyll.Core.Metadata
28−import Hakyll.Core.Provider.Internal
26+import qualified Hakyll.Core.Provider.Internal as Internal
29import qualified Hakyll.Core.Provider.MetadataCache as Internal
27import qualified Hakyll.Core.Provider.MetadataCache as Internal
30−import Hakyll.Core.Provider.Modified
28+import Hakyll.Core.Store (Store)
31
29
32
30
33--------------------------------------------------------------------------------
31--------------------------------------------------------------------------------
34−-- | Wrapper to ensure metadata cache is invalidated if necessary
32+-- | Create a resource provider
35−resourceMetadata :: Provider -> Identifier -> IO Metadata
33+newProvider :: Store -- ^ Store to use
36−resourceMetadata rp r = do
34+ -> (FilePath -> Bool) -- ^ Should we ignore this file?
37− _ <- resourceModified rp r
35+ -> FilePath -- ^ Search directory
38− Internal.resourceMetadata rp r
36+ -> IO Internal.Provider -- ^ Resulting provider
39−
37+newProvider store ignore directory = do
40−
38+ -- Delete metadata cache where necessary
41−--------------------------------------------------------------------------------
39+ p <- Internal.newProvider store ignore directory
42−-- | Wrapper to ensure metadata cache is invalidated if necessary
40+ mapM_ (Internal.resourceInvalidateMetadataCache p) $
43−resourceBody :: Provider -> Identifier -> IO String
44−resourceBody rp r = do
45− _ <- resourceModified rp r
41+ filter (Internal.resourceModified p) $ Internal.resourceList p
46− Internal.resourceBody rp r
42+ return p
src/Hakyll/Core/Provider/Internal.hs+133−24
@@ -1,46 +1,103 @@
1--------------------------------------------------------------------------------
1--------------------------------------------------------------------------------
2+{-# LANGUAGE CPP #-}
3+{-# LANGUAGE DeriveDataTypeable #-}
4+{-# LANGUAGE GeneralizedNewtypeDeriving #-}
2module Hakyll.Core.Provider.Internal
5module Hakyll.Core.Provider.Internal
6+ ( ResourceInfo (..)
3− ( Provider (..)
7+ , Provider (..)
4 , newProvider
8 , newProvider
5
9
6 , resourceList
10 , resourceList
7 , resourceExists
11 , resourceExists
8− , resourceMetadataResource
9
12
10 , resourceFilePath
13 , resourceFilePath
11 , resourceString
14 , resourceString
12 , resourceLBS
15 , resourceLBS
16+
17+ , resourceModified
18+ , resourceModificationTime
13 ) where
19 ) where
14
20
15
21
16--------------------------------------------------------------------------------
22--------------------------------------------------------------------------------
17−import Control.Applicative ((<$>))
23+import Control.Applicative ((<$>), (<*>))
24+import Control.DeepSeq (NFData (..), deepseq)
25+import Control.Monad (forM)
26+import Data.Binary (Binary (..))
18import qualified Data.ByteString.Lazy as BL
27import qualified Data.ByteString.Lazy as BL
19−import Data.IORef
20import Data.Map (Map)
28import Data.Map (Map)
21import qualified Data.Map as M
29import qualified Data.Map as M
30+import Data.Maybe (fromMaybe)
31+import Data.Monoid (mempty)
22import Data.Set (Set)
32import Data.Set (Set)
23import qualified Data.Set as S
33import qualified Data.Set as S
34+import Data.Time (Day (..), UTCTime (..),
35+ secondsToDiffTime)
36+import Data.Typeable (Typeable)
37+import System.Directory (getModificationTime)
24import System.FilePath (addExtension, (</>))
38import System.FilePath (addExtension, (</>))
25
39
26
40
41+--------------------------------------------------------------------------------
42+#if !MIN_VERSION_directory(1,2,0)
43+import Data.Time (readTime)
44+import System.Locale (defaultTimeLocale)
45+import System.Time (formatCalendarTime, toCalendarTime)
46+#endif
47+
48+
27--------------------------------------------------------------------------------
49--------------------------------------------------------------------------------
28import Hakyll.Core.Identifier
50import Hakyll.Core.Identifier
29−import Hakyll.Core.Store
51+import Hakyll.Core.Store (Store)
52+import qualified Hakyll.Core.Store as Store
30import Hakyll.Core.Util.File
53import Hakyll.Core.Util.File
31
54
32
55
56+--------------------------------------------------------------------------------
57+-- | Because UTCTime doesn't have a Binary instance...
58+newtype BinaryTime = BinaryTime {unBinaryTime :: UTCTime}
59+ deriving (Eq, NFData, Ord, Show, Typeable)
60+
61+
62+--------------------------------------------------------------------------------
63+instance Binary BinaryTime where
64+ put (BinaryTime (UTCTime (ModifiedJulianDay d) dt)) =
65+ put d >> put (floor dt :: Integer)
66+
67+ get = fmap BinaryTime $ UTCTime
68+ <$> (ModifiedJulianDay <$> get)
69+ <*> (secondsToDiffTime <$> get)
70+
71+
72+--------------------------------------------------------------------------------
73+data ResourceInfo = ResourceInfo
74+ { resourceInfoModified :: BinaryTime
75+ , resourceInfoMetadata :: Maybe Identifier
76+ } deriving (Show, Typeable)
77+
78+
79+--------------------------------------------------------------------------------
80+instance Binary ResourceInfo where
81+ put (ResourceInfo mtime meta) = put mtime >> put meta
82+ get = ResourceInfo <$> get <*> get
83+
84+
85+--------------------------------------------------------------------------------
86+instance NFData ResourceInfo where
87+ rnf (ResourceInfo mtime meta) = rnf mtime `seq` rnf meta `seq` ()
88+
89+
33--------------------------------------------------------------------------------
90--------------------------------------------------------------------------------
34-- | Responsible for retrieving and listing resources
91-- | Responsible for retrieving and listing resources
35data Provider = Provider
92data Provider = Provider
36 { -- Top of the provided directory
93 { -- Top of the provided directory
37− providerDirectory :: FilePath
94+ providerDirectory :: FilePath
38 , -- | A list of all files found
95 , -- | A list of all files found
39− providerSet :: Set Identifier
96+ providerFiles :: Map Identifier ResourceInfo
40− , -- | Cache keeping track of modified files
97+ , -- | A list of the files from the previous run
41− providerModifiedCache :: IORef (Map Identifier Bool)
98+ providerOldFiles :: Map Identifier ResourceInfo
42 , -- | Underlying persistent store for caching
99 , -- | Underlying persistent store for caching
43− providerStore :: Store
100+ providerStore :: Store
44 }
101 }
45
102
46
103
@@ -51,30 +108,47 @@ newProvider :: Store -- ^ Store to use
51 -> FilePath -- ^ Search directory
108 -> FilePath -- ^ Search directory
52 -> IO Provider -- ^ Resulting provider
109 -> IO Provider -- ^ Resulting provider
53newProvider store ignore directory = do
110newProvider store ignore directory = do
54− list <- map fromFilePath . filter (not . ignore) <$>
111+ list <- map fromFilePath <$> getRecursiveContents ignore directory
55− getRecursiveContents directory
112+ let universe = S.fromList list
56− cache <- newIORef M.empty
113+ files <- fmap (maxmtime . M.fromList) $ forM list $ \identifier -> do
114+ rInfo <- getResourceInfo directory universe identifier
115+ return (identifier, rInfo)
116+
117+ -- Get the old files from the store, and then immediately replace them by
118+ -- the new files.
119+ oldFiles <- fromMaybe mempty . Store.toMaybe <$> Store.get store oldKey
120+ oldFiles `deepseq` Store.set store oldKey files
121+
57− return $ Provider directory (S.fromList list) cache store
122+ return $ Provider directory files oldFiles store
123+ where
124+ oldKey = ["Hakyll.Core.Provider.Internal.newProvider", "oldFiles"]
125+
126+ -- Update modified if metadata is modified
127+ maxmtime files = flip M.map files $ \rInfo@(ResourceInfo mtime meta) ->
128+ let metaMod = fmap resourceInfoModified $ meta >>= flip M.lookup files
129+ in rInfo {resourceInfoModified = maybe mtime (max mtime) metaMod}
130+
131+
132+--------------------------------------------------------------------------------
133+getResourceInfo :: FilePath -> Set Identifier -> Identifier -> IO ResourceInfo
134+getResourceInfo directory universe identifier = do
135+ mtime <- fileModificationTime $ directory </> toFilePath identifier
136+ return $ ResourceInfo (BinaryTime mtime) $
137+ if mdRsc `S.member` universe then Just mdRsc else Nothing
138+ where
139+ mdRsc = fromFilePath $ flip addExtension "metadata" $ toFilePath identifier
58
140
59
141
60--------------------------------------------------------------------------------
142--------------------------------------------------------------------------------
61resourceList :: Provider -> [Identifier]
143resourceList :: Provider -> [Identifier]
62−resourceList = S.toList . providerSet
144+resourceList = M.keys . providerFiles
63
145
64
146
65--------------------------------------------------------------------------------
147--------------------------------------------------------------------------------
66-- | Check if a given resource exists
148-- | Check if a given resource exists
67resourceExists :: Provider -> Identifier -> Bool
149resourceExists :: Provider -> Identifier -> Bool
68resourceExists provider =
150resourceExists provider =
69− (`S.member` providerSet provider) . setVersion Nothing
151+ (`M.member` providerFiles provider) . setVersion Nothing
70−
71−
72−--------------------------------------------------------------------------------
73−-- | Each resource may have an associated metadata resource (with a @.metadata@
74−-- filename)
75−resourceMetadataResource :: Identifier -> Identifier
76−resourceMetadataResource =
77− fromFilePath . flip addExtension "metadata" . toFilePath
78
152
79
153
80--------------------------------------------------------------------------------
154--------------------------------------------------------------------------------
@@ -92,3 +166,38 @@ resourceString p i = readFile $ resourceFilePath p i
92-- | Get the raw body of a resource of a lazy bytestring
166-- | Get the raw body of a resource of a lazy bytestring
93resourceLBS :: Provider -> Identifier -> IO BL.ByteString
167resourceLBS :: Provider -> Identifier -> IO BL.ByteString
94resourceLBS p i = BL.readFile $ resourceFilePath p i
168resourceLBS p i = BL.readFile $ resourceFilePath p i
169+
170+
171+--------------------------------------------------------------------------------
172+-- | A resource is modified if it or its metadata has changed
173+resourceModified :: Provider -> Identifier -> Bool
174+resourceModified p r = case (ri, oldRi) of
175+ (Nothing, _) -> False
176+ (Just _, Nothing) -> True
177+ (Just n, Just o) -> resourceInfoModified n > resourceInfoModified o
178+ where
179+ normal = setVersion Nothing r
180+ ri = M.lookup normal (providerFiles p)
181+ oldRi = M.lookup normal (providerOldFiles p)
182+
183+
184+--------------------------------------------------------------------------------
185+resourceModificationTime :: Provider -> Identifier -> UTCTime
186+resourceModificationTime p i =
187+ case M.lookup (setVersion Nothing i) (providerFiles p) of
188+ Just ri -> unBinaryTime $ resourceInfoModified ri
189+ Nothing -> error $
190+ "Hakyll.Core.Provider.Internal.resourceModificationTime: " ++
191+ "resource " ++ show i ++ " does not exist"
192+
193+
194+--------------------------------------------------------------------------------
195+fileModificationTime :: FilePath -> IO UTCTime
196+fileModificationTime fp = do
197+#if MIN_VERSION_directory(1,2,0)
198+ getModificationTime fp
199+#else
200+ ct <- toCalendarTime =<< getModificationTime fp
201+ let str = formatCalendarTime defaultTimeLocale "%s" ct
202+ return $ readTime defaultTimeLocale "%s" str
203+#endif
src/Hakyll/Core/Provider/Metadata.hs+4−3
@@ -31,13 +31,14 @@ loadMetadata p identifier = do
31 then second Just <$> loadMetadataHeader fp
31 then second Just <$> loadMetadataHeader fp
32 else return (M.empty, Nothing)
32 else return (M.empty, Nothing)
33
33
34− emd <- if resourceExists p mi then loadMetadataFile mfp else return M.empty
34+ emd <- case mi of
35+ Nothing -> return M.empty
36+ Just mi' -> loadMetadataFile $ resourceFilePath p mi'
35
37
36 return (M.union md emd, body)
38 return (M.union md emd, body)
37 where
39 where
38 fp = resourceFilePath p identifier
40 fp = resourceFilePath p identifier
39− mi = resourceMetadataResource identifier
41+ mi = M.lookup identifier (providerFiles p) >>= resourceInfoMetadata
40− mfp = resourceFilePath p mi
41
42
42
43
43--------------------------------------------------------------------------------
44--------------------------------------------------------------------------------
src/Hakyll/Core/Provider/MetadataCache.hs+1
@@ -23,6 +23,7 @@ resourceMetadata :: Provider -> Identifier -> IO Metadata
23resourceMetadata p r
23resourceMetadata p r
24 | not (resourceExists p r) = return M.empty
24 | not (resourceExists p r) = return M.empty
25 | otherwise = do
25 | otherwise = do
26+ -- TODO keep time in md cache
26 load p r
27 load p r
27 Store.Found md <- Store.get (providerStore p)
28 Store.Found md <- Store.get (providerStore p)
28 [name, toFilePath r, "metadata"]
29 [name, toFilePath r, "metadata"]
src/Hakyll/Core/Provider/Modified.hsdeleted−101
@@ -1,101 +0,0 @@
1−--------------------------------------------------------------------------------
2−{-# LANGUAGE CPP #-}
3−module Hakyll.Core.Provider.Modified
4− ( resourceModified
5− , resourceModificationTime
6− ) where
7−
8−
9−--------------------------------------------------------------------------------
10−import Control.Applicative ((<$>), (<*>))
11−import Control.Monad (when)
12−import qualified Crypto.Hash.MD5 as MD5
13−import qualified Data.ByteString as B
14−import qualified Data.ByteString.Lazy as BL
15−import Data.IORef
16−import qualified Data.Map as M
17−import Data.Time (UTCTime)
18−import System.Directory (getModificationTime)
19−
20−
21−--------------------------------------------------------------------------------
22−#if !MIN_VERSION_directory(1,2,0)
23−import Data.Time (readTime)
24−import System.Locale (defaultTimeLocale)
25−import System.Time (formatCalendarTime,
26− toCalendarTime)
27−#endif
28−
29−
30−--------------------------------------------------------------------------------
31−import Hakyll.Core.Identifier
32−import Hakyll.Core.Provider.Internal
33−import Hakyll.Core.Provider.MetadataCache
34−import Hakyll.Core.Store (Store)
35−import qualified Hakyll.Core.Store as Store
36−
37−
38−--------------------------------------------------------------------------------
39−-- | A resource is modified if it or its metadata has changed
40−resourceModified :: Provider -> Identifier -> IO Bool
41−resourceModified p r
42− | not exists = return False
43− | otherwise = do
44− cache <- readIORef cacheRef
45− case M.lookup normalized cache of
46− Just m -> return m
47− Nothing -> do
48− -- Check if the actual file was modified, and do a recursive
49− -- call to check if the metadata file was modified
50− m <- (||)
51− <$> fileDigestModified store filePath
52− <*> resourceModified p (resourceMetadataResource r)
53− modifyIORef cacheRef (M.insert normalized m)
54−
55− -- Important! (But ugly)
56− when m $ resourceInvalidateMetadataCache p r
57−
58− return m
59− where
60− normalized = setVersion Nothing r
61− exists = resourceExists p r
62− store = providerStore p
63− cacheRef = providerModifiedCache p
64− filePath = resourceFilePath p r
65−
66−
67−--------------------------------------------------------------------------------
68−-- | Utility: Check if a the digest of a file was modified
69−fileDigestModified :: Store -> FilePath -> IO Bool
70−fileDigestModified store fp = do
71− -- Get the latest seen digest from the store, and calculate the current
72− -- digest for the
73− lastDigest <- Store.get store key
74− newDigest <- fileDigest fp
75− if Store.Found newDigest == lastDigest
76− -- All is fine, not modified
77− then return False
78− -- Resource modified; store new digest
79− else do
80− Store.set store key newDigest
81− return True
82− where
83− key = ["Hakyll.Core.Resource.Provider.fileModified", fp]
84−
85−
86−--------------------------------------------------------------------------------
87−-- | Utility: Retrieve a digest for a given file
88−fileDigest :: FilePath -> IO B.ByteString
89−fileDigest = fmap MD5.hashlazy . BL.readFile
90−
91−
92−--------------------------------------------------------------------------------
93−resourceModificationTime :: Provider -> Identifier -> IO UTCTime
94−resourceModificationTime p i = do
95−#if MIN_VERSION_directory(1,2,0)
96− getModificationTime $ resourceFilePath p i
97−#else
98− ct <- toCalendarTime =<< getModificationTime (resourceFilePath p i)
99− let str = formatCalendarTime defaultTimeLocale "%s" ct
100− return $ readTime defaultTimeLocale "%s" str
101−#endif
src/Hakyll/Core/Rules/Internal.hs+13−7
@@ -16,9 +16,9 @@ import Control.Applicative (Applicative, (<$>))
16import Control.Monad.Reader (ask)
16import Control.Monad.Reader (ask)
17import Control.Monad.RWS (RWST, runRWST)
17import Control.Monad.RWS (RWST, runRWST)
18import Control.Monad.Trans (liftIO)
18import Control.Monad.Trans (liftIO)
19−import qualified Data.Map as M
20import Data.Monoid (Monoid, mappend, mempty)
19import Data.Monoid (Monoid, mappend, mempty)
21import Data.Set (Set)
20import Data.Set (Set)
21+import qualified Data.Set as S
22
22
23
23
24--------------------------------------------------------------------------------
24--------------------------------------------------------------------------------
@@ -92,7 +92,12 @@ instance MonadMetadata Rules where
92runRules :: Rules a -> Provider -> IO RuleSet
92runRules :: Rules a -> Provider -> IO RuleSet
93runRules rules provider = do
93runRules rules provider = do
94 (_, _, ruleSet) <- runRWST (unRules rules) env emptyRulesState
94 (_, _, ruleSet) <- runRWST (unRules rules) env emptyRulesState
95− return $ nubCompilers ruleSet
95+ case findDuplicate (map fst $ rulesCompilers ruleSet) of
96+ Nothing -> return ruleSet
97+ Just id' -> error $
98+ "Hakyll.Core.Rules.Internal: two different rules for " ++
99+ show id' ++ " exist, bailing out"
100+
96 where
101 where
97 env = RulesRead
102 env = RulesRead
98 { rulesProvider = provider
103 { rulesProvider = provider
@@ -102,9 +107,10 @@ runRules rules provider = do
102
107
103
108
104--------------------------------------------------------------------------------
109--------------------------------------------------------------------------------
105−-- | Remove duplicate compilers from the 'RuleSet'. When two compilers match an
110+findDuplicate :: Ord a => [a] -> Maybe a
106−-- item, we prefer the first one
111+findDuplicate = go S.empty
107−nubCompilers :: RuleSet -> RuleSet
108−nubCompilers set = set {rulesCompilers = nubCompilers' (rulesCompilers set)}
109 where
112 where
110− nubCompilers' = M.toList . M.fromListWith (flip const)
113+ go _ [] = Nothing
114+ go s (x : xs)
115+ | x `S.member` s = Just x
116+ | otherwise = go (S.insert x s) xs
src/Hakyll/Core/Runtime.hs+4−3
@@ -6,7 +6,7 @@ module Hakyll.Core.Runtime
6
6
7--------------------------------------------------------------------------------
7--------------------------------------------------------------------------------
8import Control.Applicative ((<$>))
8import Control.Applicative ((<$>))
9−import Control.Monad (filterM, unless)
9+import Control.Monad (unless)
10import Control.Monad.Error (ErrorT, runErrorT, throwError)
10import Control.Monad.Error (ErrorT, runErrorT, throwError)
11import Control.Monad.Reader (ask)
11import Control.Monad.Reader (ask)
12import Control.Monad.RWS (RWST, runRWST)
12import Control.Monad.RWS (RWST, runRWST)
@@ -140,8 +140,9 @@ scheduleOutOfDate = do
140 todo <- runtimeTodo <$> get
140 todo <- runtimeTodo <$> get
141
141
142 let identifiers = M.keys universe
142 let identifiers = M.keys universe
143− modified <- fmap S.fromList $ flip filterM identifiers $
143+ modified = S.fromList $ flip filter identifiers $
144− liftIO . resourceModified provider
144+ resourceModified provider
145+
145 let (ood, facts', msgs) = outOfDate identifiers modified facts
146 let (ood, facts', msgs) = outOfDate identifiers modified facts
146 todo' = M.filterWithKey
147 todo' = M.filterWithKey
147 (\id' _ -> id' `S.member` ood) universe
148 (\id' _ -> id' `S.member` ood) universe
src/Hakyll/Core/Store.hs+8
@@ -5,6 +5,7 @@
5module Hakyll.Core.Store
5module Hakyll.Core.Store
6 ( Store
6 ( Store
7 , Result (..)
7 , Result (..)
8+ , toMaybe
8 , new
9 , new
9 , set
10 , set
10 , get
11 , get
@@ -53,6 +54,13 @@ data Result a
53 deriving (Show, Eq)
54 deriving (Show, Eq)
54
55
55
56
57+--------------------------------------------------------------------------------
58+-- | Convert result to 'Maybe'
59+toMaybe :: Result a -> Maybe a
60+toMaybe (Found x) = Just x
61+toMaybe _ = Nothing
62+
63+
56--------------------------------------------------------------------------------
64--------------------------------------------------------------------------------
57-- | Initialize the store
65-- | Initialize the store
58new :: Bool -- ^ Use in-memory caching
66new :: Bool -- ^ Use in-memory caching
src/Hakyll/Core/UnixFilter.hs+1−1
@@ -57,7 +57,7 @@ unixFilter = unixFilterWith writer reader
57--
57--
58-- > match "music.wav" $ do
58-- > match "music.wav" $ do
59-- > route $ setExtension "ogg"
59-- > route $ setExtension "ogg"
60−-- > compile $ getResourceLBS >>= withItemBody (unixFilter "oggenc" ["-"])
60+-- > compile $ getResourceLBS >>= withItemBody (unixFilterLBS "oggenc" ["-"])
61unixFilterLBS :: String -- ^ Program name
61unixFilterLBS :: String -- ^ Program name
62 -> [String] -- ^ Program args
62 -> [String] -- ^ Program args
63 -> ByteString -- ^ Program input
63 -> ByteString -- ^ Program input
src/Hakyll/Core/Util/File.hs+6−5
@@ -25,12 +25,13 @@ makeDirectories = createDirectoryIfMissing True . takeDirectory
25
25
26--------------------------------------------------------------------------------
26--------------------------------------------------------------------------------
27-- | Get all contents of a directory.
27-- | Get all contents of a directory.
28−getRecursiveContents :: FilePath -- ^ Directory to search
28+getRecursiveContents :: (FilePath -> Bool) -- ^ Ignore this file/directory
29+ -> FilePath -- ^ Directory to search
29− -> IO [FilePath] -- ^ List of files found
30+ -> IO [FilePath] -- ^ List of files found
30−getRecursiveContents top = go ""
31+getRecursiveContents ignore top = go ""
31 where
32 where
32− isProper = (`notElem` [".", ".."])
33+ isProper x = notElem x [".", ".."] && not (ignore x)
33− go dir = do
34+ go dir = do
34 dirExists <- doesDirectoryExist (top </> dir)
35 dirExists <- doesDirectoryExist (top </> dir)
35 if not dirExists
36 if not dirExists
36 then return []
37 then return []
src/Hakyll/Init.hs+1−1
@@ -23,7 +23,7 @@ main = do
23 progName <- getProgName
23 progName <- getProgName
24 args <- getArgs
24 args <- getArgs
25 srcDir <- getDataFileName "example"
25 srcDir <- getDataFileName "example"
26− files <- getRecursiveContents srcDir
26+ files <- getRecursiveContents (const False) srcDir
27
27
28 case args of
28 case args of
29 [dstDir] -> forM_ files $ \file -> do
29 [dstDir] -> forM_ files $ \file -> do
src/Hakyll/Web/Pandoc/FileType.hs+25−21
@@ -8,7 +8,7 @@ module Hakyll.Web.Pandoc.FileType
8
8
9
9
10--------------------------------------------------------------------------------
10--------------------------------------------------------------------------------
11−import System.FilePath (takeExtension)
11+import System.FilePath (splitExtension)
12
12
13
13
14--------------------------------------------------------------------------------
14--------------------------------------------------------------------------------
@@ -36,27 +36,31 @@ data FileType
36--------------------------------------------------------------------------------
36--------------------------------------------------------------------------------
37-- | Get the file type for a certain file. The type is determined by extension.
37-- | Get the file type for a certain file. The type is determined by extension.
38fileType :: FilePath -> FileType
38fileType :: FilePath -> FileType
39−fileType = fileType' . takeExtension
39+fileType = uncurry fileType' . splitExtension
40 where
40 where
41− fileType' ".css" = Css
41+ fileType' _ ".css" = Css
42− fileType' ".htm" = Html
42+ fileType' _ ".htm" = Html
43− fileType' ".html" = Html
43+ fileType' _ ".html" = Html
44− fileType' ".lhs" = LiterateHaskell Markdown
44+ fileType' f ".lhs" = LiterateHaskell $ case fileType f of
45+ -- If no extension is given, default to Markdown + LiterateHaskell
46+ Binary -> Markdown
47+ -- Otherwise, LaTeX + LiterateHaskell or whatever the user specified
48+ x -> x
45− fileType' ".markdown" = Markdown
49+ fileType' _ ".markdown" = Markdown
46− fileType' ".md" = Markdown
50+ fileType' _ ".md" = Markdown
47− fileType' ".mdn" = Markdown
51+ fileType' _ ".mdn" = Markdown
48− fileType' ".mdown" = Markdown
52+ fileType' _ ".mdown" = Markdown
49− fileType' ".mdwn" = Markdown
53+ fileType' _ ".mdwn" = Markdown
50− fileType' ".mkd" = Markdown
54+ fileType' _ ".mkd" = Markdown
51− fileType' ".mkdwn" = Markdown
55+ fileType' _ ".mkdwn" = Markdown
52− fileType' ".org" = OrgMode
56+ fileType' _ ".org" = OrgMode
53− fileType' ".page" = Markdown
57+ fileType' _ ".page" = Markdown
54− fileType' ".rst" = Rst
58+ fileType' _ ".rst" = Rst
55− fileType' ".tex" = LaTeX
59+ fileType' _ ".tex" = LaTeX
56− fileType' ".text" = PlainText
60+ fileType' _ ".text" = PlainText
57− fileType' ".textile" = Textile
61+ fileType' _ ".textile" = Textile
58− fileType' ".txt" = PlainText
62+ fileType' _ ".txt" = PlainText
59− fileType' _ = Binary -- Treat unknown files as binary
63+ fileType' _ _ = Binary -- Treat unknown files as binary
60
64
61
65
62--------------------------------------------------------------------------------
66--------------------------------------------------------------------------------
src/Hakyll/Web/Tags.hs+5−8
@@ -41,12 +41,13 @@
41{-# LANGUAGE GeneralizedNewtypeDeriving #-}
41{-# LANGUAGE GeneralizedNewtypeDeriving #-}
42{-# LANGUAGE OverloadedStrings #-}
42{-# LANGUAGE OverloadedStrings #-}
43module Hakyll.Web.Tags
43module Hakyll.Web.Tags
44− ( Tags
44+ ( Tags (..)
45 , getTags
45 , getTags
46 , buildTagsWith
46 , buildTagsWith
47 , buildTags
47 , buildTags
48 , buildCategories
48 , buildCategories
49 , tagsRules
49 , tagsRules
50+ , renderTags
50 , renderTagCloud
51 , renderTagCloud
51 , renderTagList
52 , renderTagList
52 , tagsField
53 , tagsField
@@ -149,7 +150,7 @@ tagsRules tags rules =
149
150
150
151
151--------------------------------------------------------------------------------
152--------------------------------------------------------------------------------
152−-- | Render tags in HTML
153+-- | Render tags in HTML (the flexible higher-order function)
153renderTags :: (String -> String -> Int -> Int -> Int -> String)
154renderTags :: (String -> String -> Int -> Int -> Int -> String)
154 -- ^ Produce a tag item: tag, url, count, min count, max count
155 -- ^ Produce a tag item: tag, url, count, min count, max count
155 -> ([String] -> String)
156 -> ([String] -> String)
@@ -218,13 +219,9 @@ renderTagList = renderTags makeLink (intercalate ", ")
218
219
219
220
220--------------------------------------------------------------------------------
221--------------------------------------------------------------------------------
221−-- | Render tags with links with custom function to get tags. It is typically
222+-- | Render tags with links with custom function to get tags
222−-- together with 'getTags' like this:
223−--
224−-- > renderTagsFieldWith (customFunction . getTags)
225−-- > "tags" (fromCapture "tags/*")
226tagsFieldWith :: (Identifier -> Compiler [String]) -- ^ Get the tags
223tagsFieldWith :: (Identifier -> Compiler [String]) -- ^ Get the tags
227− -> String -- ^ Destination key
224+ -> String -- ^ Destination field
228 -> Tags -- ^ Tags structure
225 -> Tags -- ^ Tags structure
229 -> Context a -- ^ Resulting context
226 -> Context a -- ^ Resulting context
230tagsFieldWith getTags' key tags = field key $ \item -> do
227tagsFieldWith getTags' key tags = field key $ \item -> do
src/Hakyll/Web/Template/Context.hs+1−2
@@ -207,8 +207,7 @@ modificationTimeFieldWith :: TimeLocale -- ^ Time output locale
207 -> Context a -- ^ Resulting context
207 -> Context a -- ^ Resulting context
208modificationTimeFieldWith locale key fmt = field key $ \i -> do
208modificationTimeFieldWith locale key fmt = field key $ \i -> do
209 provider <- compilerProvider <$> compilerAsk
209 provider <- compilerProvider <$> compilerAsk
210− mtime <- compilerUnsafeIO $
211− resourceModificationTime provider $ itemIdentifier i
210+ let mtime = resourceModificationTime provider $ itemIdentifier i
212 return $ formatTime locale fmt mtime
211 return $ formatTime locale fmt mtime
213
212
214
213
tests/Hakyll/Core/Provider/Tests.hs+1
@@ -37,3 +37,4 @@ case01 = do
37
37
38 doesntExist <- resourceMetadata provider "doesntexist.md"
38 doesntExist <- resourceMetadata provider "doesntexist.md"
39 M.empty @=? doesntExist
39 M.empty @=? doesntExist
40+ cleanTestEnv
tests/Hakyll/Core/Rules/Tests.hs+1
@@ -53,6 +53,7 @@ rulesTest = do
53 checkRoute "example.mv1" (sv "mv1" "example.md")
53 checkRoute "example.mv1" (sv "mv1" "example.md")
54 checkRoute "example.mv2" (sv "mv2" "example.md")
54 checkRoute "example.mv2" (sv "mv2" "example.md")
55 readIORef ioref >>= assert
55 readIORef ioref >>= assert
56+ cleanTestEnv
56 where
57 where
57 sv g = setVersion (Just g)
58 sv g = setVersion (Just g)
58 expected =
59 expected =
tests/Hakyll/Core/Store/Tests.hs+1
@@ -67,3 +67,4 @@ wrongType = do
67 e == typeOf (undefined :: Int) &&
67 e == typeOf (undefined :: Int) &&
68 t == typeOf (undefined :: String)
68 t == typeOf (undefined :: String)
69 _ -> False
69 _ -> False
70+ cleanTestEnv
tests/Hakyll/Web/Pandoc/FileType/Tests.hsnew file+26
@@ -0,0 +1,26 @@
1+--------------------------------------------------------------------------------
2+{-# LANGUAGE OverloadedStrings #-}
3+module Hakyll.Web.Pandoc.FileType.Tests
4+ ( tests
5+ ) where
6+
7+
8+--------------------------------------------------------------------------------
9+import Test.Framework (Test, testGroup)
10+import Test.HUnit ((@=?))
11+
12+
13+--------------------------------------------------------------------------------
14+import Hakyll.Web.Pandoc.FileType
15+import TestSuite.Util
16+
17+
18+--------------------------------------------------------------------------------
19+tests :: Test
20+tests = testGroup "Hakyll.Web.Pandoc.FileType.Tests" $
21+ fromAssertions "fileType"
22+ [ Markdown @=? fileType "index.md"
23+ , Rst @=? fileType "about/foo.rst"
24+ , LiterateHaskell Markdown @=? fileType "posts/bananas.lhs"
25+ , LiterateHaskell LaTeX @=? fileType "posts/bananas.tex.lhs"
26+ ]
tests/Hakyll/Web/Template/Tests.hs+2
@@ -44,6 +44,7 @@ case01 = do
44 pandocCompiler >>= applyTemplate (itemBody tpl) testContext
44 pandocCompiler >>= applyTemplate (itemBody tpl) testContext
45
45
46 out @=? itemBody item
46 out @=? itemBody item
47+ cleanTestEnv
47
48
48
49
49--------------------------------------------------------------------------------
50--------------------------------------------------------------------------------
@@ -63,6 +64,7 @@ testApplyJoinTemplateList = do
63 applyJoinTemplateList ", " tpl defaultContext [i1, i2]
64 applyJoinTemplateList ", " tpl defaultContext [i1, i2]
64
65
65 str @?= "<b>Hello</b>, <b>World</b>"
66 str @?= "<b>Hello</b>, <b>World</b>"
67+ cleanTestEnv
66 where
68 where
67 i1 = Item "item1" "Hello"
69 i1 = Item "item1" "Hello"
68 i2 = Item "item2" "World"
70 i2 = Item "item2" "World"
tests/TestSuite.hs+2
@@ -20,6 +20,7 @@ import qualified Hakyll.Core.UnixFilter.Tests
20import qualified Hakyll.Core.Util.String.Tests
20import qualified Hakyll.Core.Util.String.Tests
21import qualified Hakyll.Web.Html.RelativizeUrls.Tests
21import qualified Hakyll.Web.Html.RelativizeUrls.Tests
22import qualified Hakyll.Web.Html.Tests
22import qualified Hakyll.Web.Html.Tests
23+import qualified Hakyll.Web.Pandoc.FileType.Tests
23import qualified Hakyll.Web.Template.Context.Tests
24import qualified Hakyll.Web.Template.Context.Tests
24import qualified Hakyll.Web.Template.Tests
25import qualified Hakyll.Web.Template.Tests
25
26
@@ -38,6 +39,7 @@ main = defaultMain
38 , Hakyll.Core.Util.String.Tests.tests
39 , Hakyll.Core.Util.String.Tests.tests
39 , Hakyll.Web.Html.RelativizeUrls.Tests.tests
40 , Hakyll.Web.Html.RelativizeUrls.Tests.tests
40 , Hakyll.Web.Html.Tests.tests
41 , Hakyll.Web.Html.Tests.tests
42+ , Hakyll.Web.Pandoc.FileType.Tests.tests
41 , Hakyll.Web.Template.Context.Tests.tests
43 , Hakyll.Web.Template.Context.Tests.tests
42 , Hakyll.Web.Template.Tests.tests
44 , Hakyll.Web.Template.Tests.tests
43 ]
45 ]
web/about.markdown+1
@@ -38,3 +38,4 @@ who still maintains the package. Contributors:
38- [seschwar](https://github.com/seschwar)
38- [seschwar](https://github.com/seschwar)
39- [favonia](https://github.com/favonia)
39- [favonia](https://github.com/favonia)
40- [Robin Windels](https://github.com/rwindelz)
40- [Robin Windels](https://github.com/rwindelz)
41+- [Miikka Koskinen](http://miikka.me/)
web/examples.markdown+4
@@ -17,6 +17,10 @@ this list. This list has no particular ordering.
17 [source](https://github.com/Keruspe/blog/)
17 [source](https://github.com/Keruspe/blog/)
18- <http://blog.clement.delafargue.name/>,
18- <http://blog.clement.delafargue.name/>,
19 [source](https://github.com/divarvel/blog)
19 [source](https://github.com/divarvel/blog)
20+- <http://bneijt.nl>,
21+ [source](https://github.com/bneijt/bneijt.nl)
22+- <http://brianshourd.com/>,
23+ [source](https://github.com/brianshourd/brianshourd.com)
20
24
21## Hakyll 3.X
25## Hakyll 3.X
22
26
web/images/hakyll-system-2.png

Binary file not shown.

web/releases.markdown+12
@@ -4,6 +4,18 @@ title: Releases
4
4
5# Releases
5# Releases
6
6
7+## Hakyll 4.1.4.0
8+
9+*January 26, 2013*
10+
11+- Export the flexible `renderTags` function
12+
13+## Hakyll 4.1.3.0
14+
15+*January 26, 2013*
16+
17+- Export the constructor of the `Tags` datatype
18+
7## Hakyll 4.1.2.0
19## Hakyll 4.1.2.0
8
20
9*January 20, 2013*
21*January 20, 2013*
web/tutorials/01-installation.markdown+3−2
@@ -32,8 +32,9 @@ started:
32
32
33 $ hakyll-init my-site
33 $ hakyll-init my-site
34
34
35+This creates a folder `my-site` in the current directory, with some example
36+content and a generic configuration. If `hakyll-init` is not found, you should
35−If `hakyll-init` is not found, you should make sure `$HOME/.cabal/bin` is in
37+make sure `$HOME/.cabal/bin` is in your `$PATH`.
36−your `$PATH`.
37
38
38The file `site.hs` holds the configuration of your site, as an executable
39The file `site.hs` holds the configuration of your site, as an executable
39haskell program. We can compile and run it like this:
40haskell program. We can compile and run it like this:
web/tutorials/03-rules-routes-compilers.markdown+1−1
@@ -61,7 +61,7 @@ this will become clear soon. The real question here is why we use `create`
61instead of `match`.
61instead of `match`.
62
62
63The answer is simple: there is no `archive.html` file in our project directory!
63The answer is simple: there is no `archive.html` file in our project directory!
64−So if we were to use `match`, no a file would be matched, and hence, nothing
64+So if we were to use `match`, no file would be matched, and hence, nothing
65would appear in the output directory. `create`, however, ensures the items
65would appear in the output directory. `create`, however, ensures the items
66listed are always produced.
66listed are always produced.
67
67
web/tutorials/04-compilers.markdown+9−5
@@ -41,9 +41,11 @@ Templates
41
41
42Let's have a look at a simple template:
42Let's have a look at a simple template:
43
43
44+```html
44− <h1>$title$</h1>
45+<h1>$title$</h1>
45− <div class="info">Posted on $date$</div>
46+<div class="info">Posted on $date$</div>
46− $body$
47+$body$
48+```
47
49
48As you can probably guess, template files just contain text and only the `$`
50As you can probably guess, template files just contain text and only the `$`
49character has special meaning: text between dollar signs ("fields") is replaced
51character has special meaning: text between dollar signs ("fields") is replaced
@@ -53,7 +55,9 @@ use `$$`.
53You usually compile the templates from disk using the aptly named
55You usually compile the templates from disk using the aptly named
54`templateCompiler`:
56`templateCompiler`:
55
57
58+```haskell
56− match "templates/*" $ compile templateCompiler
59+match "templates/*" $ compile templateCompiler
60+```
57
61
58Notice the lack of `route` here: this is because we don't need to write the
62Notice the lack of `route` here: this is because we don't need to write the
59templates to your `_site` folder, we just want to use them elsewhere.
63templates to your `_site` folder, we just want to use them elsewhere.
@@ -86,7 +90,7 @@ And `$title$` like this:
86
90
87```haskell
91```haskell
88titleContext :: Context a
92titleContext :: Context a
89−titleContext :: field "title" $ \item -> do
93+titleContext = field "title" $ \item -> do
90 metadata <- getMetadata (itemIdentifier item)
94 metadata <- getMetadata (itemIdentifier item)
91 return $ fromMaybe "No title" $ M.lookup "title" metadata
95 return $ fromMaybe "No title" $ M.lookup "title" metadata
92```
96```
web/tutorials/faq.markdown+13−1
@@ -31,13 +31,25 @@ recent version of Pandoc (1.9 and onwards). Note that you also need to include
31some CSS in order for this to work! This site, for example, uses the [default
31some CSS in order for this to work! This site, for example, uses the [default
32Pandoc syntax CSS file][syntax-css].
32Pandoc syntax CSS file][syntax-css].
33
33
34+To highlight a code block, you need to use Pandoc's fenced code block syntax to
35+set the block's language. For example, here's how you highlight Haskell code:
36+
37+ ``` haskell
38+ fac n = foldr (*) 1 [1..n]
39+ ```
40+
41+For details, see Pandoc's user guide on [fenced code
42+blocks][pandoc-code-blocks] and [inline code][pandoc-inline-code].
43+
34[syntax-css]: https://github.com/jaspervdj/hakyll/blob/master/web/css/syntax.css
44[syntax-css]: https://github.com/jaspervdj/hakyll/blob/master/web/css/syntax.css
45+[pandoc-code-blocks]: http://johnmacfarlane.net/pandoc/README.html#fenced-code-blocks
46+[pandoc-inline-code]: http://johnmacfarlane.net/pandoc/README.html#verbatim
35
47
36## When should I rebuild and when should I build?
48## When should I rebuild and when should I build?
37
49
38If you execute a `./site build`, Hakyll will build your site incrementally.
50If you execute a `./site build`, Hakyll will build your site incrementally.
39However, we can not detect if you edited `site.hs`. In this case, you first want
51However, we can not detect if you edited `site.hs`. In this case, you first want
40−to compile it again `site.hs` again, and then do a `./site rebuild`.
52+to compile `site.hs` again, and then do a `./site rebuild`.
41
53
42After rebuilding your site, all files will look as "modified" to the filesystem.
54After rebuilding your site, all files will look as "modified" to the filesystem.
43This means that when you upload your site, it will usually transfer all files --
55This means that when you upload your site, it will usually transfer all files --