packages feed

hakyll 4.9.7.0 → 4.9.8.0

raw patch · 106 files changed

+7152/−7148 lines, 106 filesdep +hakylldep ~QuickCheckdep ~optparse-applicativedep ~pandoc-citeprocPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: hakyll

Dependency ranges changed: QuickCheck, optparse-applicative, pandoc-citeproc, tasty-quickcheck

API changes (from Hackage documentation)

- Hakyll: getRecursiveContents :: (FilePath -> IO Bool) -> FilePath -> IO [FilePath]
- Hakyll: makeDirectories :: FilePath -> IO ()
- Hakyll: removeDirectory :: FilePath -> IO ()
+ Hakyll.Core.Compiler.Internal: Compiler :: (CompilerRead -> IO (CompilerResult a)) -> Compiler a
+ Hakyll.Core.Compiler.Internal: CompilerRead :: Configuration -> Identifier -> Provider -> Set Identifier -> Routes -> Store -> Logger -> CompilerRead
+ Hakyll.Core.Compiler.Internal: CompilerWrite :: [Dependency] -> Int -> CompilerWrite
+ Hakyll.Core.Compiler.Internal: [CompilerDone] :: a -> CompilerWrite -> CompilerResult a
+ Hakyll.Core.Compiler.Internal: [CompilerError] :: [String] -> CompilerResult a
+ Hakyll.Core.Compiler.Internal: [CompilerRequire] :: (Identifier, Snapshot) -> Compiler a -> CompilerResult a
+ Hakyll.Core.Compiler.Internal: [CompilerSnapshot] :: Snapshot -> Compiler a -> CompilerResult a
+ Hakyll.Core.Compiler.Internal: [compilerCacheHits] :: CompilerWrite -> Int
+ Hakyll.Core.Compiler.Internal: [compilerConfig] :: CompilerRead -> Configuration
+ Hakyll.Core.Compiler.Internal: [compilerDependencies] :: CompilerWrite -> [Dependency]
+ Hakyll.Core.Compiler.Internal: [compilerLogger] :: CompilerRead -> Logger
+ Hakyll.Core.Compiler.Internal: [compilerProvider] :: CompilerRead -> Provider
+ Hakyll.Core.Compiler.Internal: [compilerRoutes] :: CompilerRead -> Routes
+ Hakyll.Core.Compiler.Internal: [compilerStore] :: CompilerRead -> Store
+ Hakyll.Core.Compiler.Internal: [compilerUnderlying] :: CompilerRead -> Identifier
+ Hakyll.Core.Compiler.Internal: [compilerUniverse] :: CompilerRead -> Set Identifier
+ Hakyll.Core.Compiler.Internal: [unCompiler] :: Compiler a -> CompilerRead -> IO (CompilerResult a)
+ Hakyll.Core.Compiler.Internal: compilerAsk :: Compiler CompilerRead
+ Hakyll.Core.Compiler.Internal: compilerCatch :: Compiler a -> ([String] -> Compiler a) -> Compiler a
+ Hakyll.Core.Compiler.Internal: compilerResult :: CompilerResult a -> Compiler a
+ Hakyll.Core.Compiler.Internal: compilerTell :: CompilerWrite -> Compiler ()
+ Hakyll.Core.Compiler.Internal: compilerTellCacheHits :: Int -> Compiler ()
+ Hakyll.Core.Compiler.Internal: compilerTellDependencies :: [Dependency] -> Compiler ()
+ Hakyll.Core.Compiler.Internal: compilerThrow :: [String] -> Compiler a
+ Hakyll.Core.Compiler.Internal: compilerUnsafeIO :: IO a -> Compiler a
+ Hakyll.Core.Compiler.Internal: data CompilerRead
+ Hakyll.Core.Compiler.Internal: data CompilerResult a
+ Hakyll.Core.Compiler.Internal: data CompilerWrite
+ Hakyll.Core.Compiler.Internal: instance Control.Monad.Error.Class.MonadError [GHC.Base.String] Hakyll.Core.Compiler.Internal.Compiler
+ Hakyll.Core.Compiler.Internal: instance GHC.Base.Alternative Hakyll.Core.Compiler.Internal.Compiler
+ Hakyll.Core.Compiler.Internal: instance GHC.Base.Applicative Hakyll.Core.Compiler.Internal.Compiler
+ Hakyll.Core.Compiler.Internal: instance GHC.Base.Functor Hakyll.Core.Compiler.Internal.Compiler
+ Hakyll.Core.Compiler.Internal: instance GHC.Base.Monad Hakyll.Core.Compiler.Internal.Compiler
+ Hakyll.Core.Compiler.Internal: instance GHC.Base.Monoid Hakyll.Core.Compiler.Internal.CompilerWrite
+ Hakyll.Core.Compiler.Internal: instance GHC.Show.Show Hakyll.Core.Compiler.Internal.CompilerWrite
+ Hakyll.Core.Compiler.Internal: instance Hakyll.Core.Metadata.MonadMetadata Hakyll.Core.Compiler.Internal.Compiler
+ Hakyll.Core.Compiler.Internal: newtype Compiler a
+ Hakyll.Core.Compiler.Internal: runCompiler :: Compiler a -> CompilerRead -> IO (CompilerResult a)
+ Hakyll.Core.Compiler.Internal: type Snapshot = String
+ Hakyll.Core.Logger: Debug :: Verbosity
+ Hakyll.Core.Logger: Error :: Verbosity
+ Hakyll.Core.Logger: Message :: Verbosity
+ Hakyll.Core.Logger: data Logger
+ Hakyll.Core.Logger: data Verbosity
+ Hakyll.Core.Logger: debug :: MonadIO m => Logger -> String -> m ()
+ Hakyll.Core.Logger: error :: MonadIO m => Logger -> String -> m ()
+ Hakyll.Core.Logger: flush :: Logger -> IO ()
+ Hakyll.Core.Logger: header :: MonadIO m => Logger -> String -> m ()
+ Hakyll.Core.Logger: instance GHC.Classes.Eq Hakyll.Core.Logger.Verbosity
+ Hakyll.Core.Logger: instance GHC.Classes.Ord Hakyll.Core.Logger.Verbosity
+ Hakyll.Core.Logger: instance GHC.Show.Show Hakyll.Core.Logger.Verbosity
+ Hakyll.Core.Logger: message :: MonadIO m => Logger -> String -> m ()
+ Hakyll.Core.Logger: new :: Verbosity -> IO Logger
+ Hakyll.Core.Provider: data Provider
+ Hakyll.Core.Provider: newProvider :: Store -> (FilePath -> IO Bool) -> FilePath -> IO Provider
+ Hakyll.Core.Provider: resourceBody :: Provider -> Identifier -> IO String
+ Hakyll.Core.Provider: resourceExists :: Provider -> Identifier -> Bool
+ Hakyll.Core.Provider: resourceFilePath :: Provider -> Identifier -> FilePath
+ Hakyll.Core.Provider: resourceLBS :: Provider -> Identifier -> IO ByteString
+ Hakyll.Core.Provider: resourceList :: Provider -> [Identifier]
+ Hakyll.Core.Provider: resourceMetadata :: Provider -> Identifier -> IO Metadata
+ Hakyll.Core.Provider: resourceModificationTime :: Provider -> Identifier -> UTCTime
+ Hakyll.Core.Provider: resourceModified :: Provider -> Identifier -> Bool
+ Hakyll.Core.Provider: resourceString :: Provider -> Identifier -> IO String
+ Hakyll.Core.Provider.Metadata: MetadataException :: FilePath -> ParseException -> MetadataException
+ Hakyll.Core.Provider.Metadata: data MetadataException
+ Hakyll.Core.Provider.Metadata: instance GHC.Exception.Exception Hakyll.Core.Provider.Metadata.MetadataException
+ Hakyll.Core.Provider.Metadata: instance GHC.Show.Show Hakyll.Core.Provider.Metadata.MetadataException
+ Hakyll.Core.Provider.Metadata: loadMetadata :: Provider -> Identifier -> IO (Metadata, Maybe String)
+ Hakyll.Core.Provider.Metadata: parsePage :: String -> Either ParseException (Metadata, String)
+ Hakyll.Core.Rules.Internal: RuleSet :: Routes -> [(Identifier, Compiler SomeItem)] -> Set Identifier -> Pattern -> RuleSet
+ Hakyll.Core.Rules.Internal: Rules :: RWST RulesRead RuleSet RulesState IO a -> Rules a
+ Hakyll.Core.Rules.Internal: RulesRead :: Provider -> [Identifier] -> Maybe String -> RulesRead
+ Hakyll.Core.Rules.Internal: RulesState :: Maybe Routes -> Maybe (Compiler SomeItem) -> RulesState
+ Hakyll.Core.Rules.Internal: [rulesCompiler] :: RulesState -> Maybe (Compiler SomeItem)
+ Hakyll.Core.Rules.Internal: [rulesCompilers] :: RuleSet -> [(Identifier, Compiler SomeItem)]
+ Hakyll.Core.Rules.Internal: [rulesMatches] :: RulesRead -> [Identifier]
+ Hakyll.Core.Rules.Internal: [rulesPattern] :: RuleSet -> Pattern
+ Hakyll.Core.Rules.Internal: [rulesProvider] :: RulesRead -> Provider
+ Hakyll.Core.Rules.Internal: [rulesResources] :: RuleSet -> Set Identifier
+ Hakyll.Core.Rules.Internal: [rulesRoute] :: RulesState -> Maybe Routes
+ Hakyll.Core.Rules.Internal: [rulesRoutes] :: RuleSet -> Routes
+ Hakyll.Core.Rules.Internal: [rulesVersion] :: RulesRead -> Maybe String
+ Hakyll.Core.Rules.Internal: [unRules] :: Rules a -> RWST RulesRead RuleSet RulesState IO a
+ Hakyll.Core.Rules.Internal: data RuleSet
+ Hakyll.Core.Rules.Internal: data RulesRead
+ Hakyll.Core.Rules.Internal: data RulesState
+ Hakyll.Core.Rules.Internal: emptyRulesState :: RulesState
+ Hakyll.Core.Rules.Internal: instance GHC.Base.Applicative Hakyll.Core.Rules.Internal.Rules
+ Hakyll.Core.Rules.Internal: instance GHC.Base.Functor Hakyll.Core.Rules.Internal.Rules
+ Hakyll.Core.Rules.Internal: instance GHC.Base.Monad Hakyll.Core.Rules.Internal.Rules
+ Hakyll.Core.Rules.Internal: instance GHC.Base.Monoid Hakyll.Core.Rules.Internal.RuleSet
+ Hakyll.Core.Rules.Internal: instance Hakyll.Core.Metadata.MonadMetadata Hakyll.Core.Rules.Internal.Rules
+ Hakyll.Core.Rules.Internal: newtype Rules a
+ Hakyll.Core.Rules.Internal: runRules :: Rules a -> Provider -> IO RuleSet
+ Hakyll.Core.Runtime: run :: Configuration -> Logger -> Rules a -> IO (ExitCode, RuleSet)
+ Hakyll.Core.Store: Found :: a -> Result a
+ Hakyll.Core.Store: NotFound :: Result a
+ Hakyll.Core.Store: WrongType :: TypeRep -> TypeRep -> Result a
+ Hakyll.Core.Store: data Result a
+ Hakyll.Core.Store: data Store
+ Hakyll.Core.Store: delete :: Store -> [String] -> IO ()
+ Hakyll.Core.Store: get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)
+ Hakyll.Core.Store: hash :: [String] -> String
+ Hakyll.Core.Store: instance GHC.Classes.Eq a => GHC.Classes.Eq (Hakyll.Core.Store.Result a)
+ Hakyll.Core.Store: instance GHC.Show.Show Hakyll.Core.Store.Store
+ Hakyll.Core.Store: instance GHC.Show.Show a => GHC.Show.Show (Hakyll.Core.Store.Result a)
+ Hakyll.Core.Store: isMember :: Store -> [String] -> IO Bool
+ Hakyll.Core.Store: new :: Bool -> FilePath -> IO Store
+ Hakyll.Core.Store: set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()
+ Hakyll.Core.Store: toMaybe :: Result a -> Maybe a
+ Hakyll.Core.Util.File: getRecursiveContents :: (FilePath -> IO Bool) -> FilePath -> IO [FilePath]
+ Hakyll.Core.Util.File: makeDirectories :: FilePath -> IO ()
+ Hakyll.Core.Util.File: removeDirectory :: FilePath -> IO ()

Files

CHANGELOG.md view
@@ -4,6 +4,15 @@  # Releases +## Hakyll 4.9.8.0++- Bump pandoc-citeproc to 0.10.5+- Bump optparse-applicative to 0.14+- Bump QuickCheck to 2.10+- Bump tasty-quickcheck to 0.9+- Restructure .cabal to avoid redundant compilation (contribution by+  Christopher League)+ ## Hakyll 4.9.7.0  - Fix compilation trouble with `Options.Applicative`
hakyll.cabal view
@@ -1,5 +1,5 @@ Name:    hakyll-Version: 4.9.7.0+Version: 4.9.8.0  Synopsis: A static website compiler library Description:@@ -96,22 +96,30 @@  Library   Ghc-Options:    -Wall-  Hs-Source-Dirs: src+  Hs-Source-Dirs: lib    Exposed-Modules:     Hakyll     Hakyll.Commands     Hakyll.Core.Compiler+    Hakyll.Core.Compiler.Internal     Hakyll.Core.Configuration     Hakyll.Core.Dependencies     Hakyll.Core.File     Hakyll.Core.Identifier     Hakyll.Core.Identifier.Pattern     Hakyll.Core.Item+    Hakyll.Core.Logger     Hakyll.Core.Metadata+    Hakyll.Core.Provider+    Hakyll.Core.Provider.Metadata     Hakyll.Core.Routes     Hakyll.Core.Rules+    Hakyll.Core.Rules.Internal+    Hakyll.Core.Runtime+    Hakyll.Core.Store     Hakyll.Core.UnixFilter+    Hakyll.Core.Util.File     Hakyll.Core.Util.String     Hakyll.Core.Writable     Hakyll.Main@@ -119,12 +127,12 @@     Hakyll.Web.Feed     Hakyll.Web.Html     Hakyll.Web.Html.RelativizeUrls+    Hakyll.Web.Paginate     Hakyll.Web.Pandoc     Hakyll.Web.Pandoc.Biblio     Hakyll.Web.Pandoc.FileType     Hakyll.Web.Redirect     Hakyll.Web.Tags-    Hakyll.Web.Paginate     Hakyll.Web.Template     Hakyll.Web.Template.Context     Hakyll.Web.Template.Internal@@ -136,18 +144,10 @@     Data.List.Extended     Data.Yaml.Extended     Hakyll.Check-    Hakyll.Core.Compiler.Internal     Hakyll.Core.Compiler.Require     Hakyll.Core.Item.SomeItem-    Hakyll.Core.Logger-    Hakyll.Core.Provider     Hakyll.Core.Provider.Internal-    Hakyll.Core.Provider.Metadata     Hakyll.Core.Provider.MetadataCache-    Hakyll.Core.Rules.Internal-    Hakyll.Core.Runtime-    Hakyll.Core.Store-    Hakyll.Core.Util.File     Hakyll.Core.Util.Parser     Hakyll.Web.Pandoc.Binary     Paths_hakyll@@ -169,7 +169,7 @@     network              >= 2.6    && < 2.7,     network-uri          >= 2.6    && < 2.7,     pandoc               >= 1.14   && < 1.20,-    pandoc-citeproc      >= 0.4    && < 0.10.5,+    pandoc-citeproc      >= 0.10.5 && < 0.11,     parsec               >= 3.0    && < 3.2,     process              >= 1.0    && < 1.6,     random               >= 1.0    && < 1.2,@@ -184,7 +184,7 @@     unordered-containers >= 0.2    && < 0.3,     vector               >= 0.11   && < 0.13,     yaml                 >= 0.8.11 && < 0.9,-    optparse-applicative >= 0.12   && < 0.14+    optparse-applicative >= 0.12   && < 0.15    If flag(previewServer)     Build-depends:@@ -218,9 +218,10 @@  Test-suite hakyll-tests   Type:           exitcode-stdio-1.0-  Hs-source-dirs: src tests+  Hs-source-dirs: tests   Main-is:        TestSuite.hs   Ghc-options:    -Wall+   Other-modules:     Hakyll.Core.Dependencies.Tests     Hakyll.Core.Identifier.Tests@@ -241,10 +242,11 @@     TestSuite.Util    Build-Depends:-    QuickCheck                 >= 2.8  && < 2.10,+    hakyll,+    QuickCheck                 >= 2.8  && < 2.11,     tasty                      >= 0.11 && < 0.12,     tasty-hunit                >= 0.9  && < 0.10,-    tasty-quickcheck           >= 0.8  && < 0.9,+    tasty-quickcheck           >= 0.8  && < 0.10,     -- Copy pasted from hakyll dependencies:     base                 >= 4.8    && < 5,     binary               >= 0.5    && < 0.9,@@ -262,7 +264,7 @@     network              >= 2.6    && < 2.7,     network-uri          >= 2.6    && < 2.7,     pandoc               >= 1.14   && < 1.20,-    pandoc-citeproc      >= 0.4    && < 0.11,+    pandoc-citeproc      >= 0.10.5 && < 0.11,     parsec               >= 3.0    && < 3.2,     process              >= 1.0    && < 1.6,     random               >= 1.0    && < 1.2,@@ -277,7 +279,7 @@     unordered-containers >= 0.2    && < 0.3,     vector               >= 0.11   && < 0.13,     yaml                 >= 0.8.11 && < 0.9,-    optparse-applicative >= 0.12   && < 0.14+    optparse-applicative >= 0.12   && < 0.15    If flag(previewServer)     Build-depends:@@ -289,9 +291,6 @@       system-filepath >= 0.4.6 && <= 0.5     Cpp-options:       -DPREVIEW_SERVER-    Other-modules:-      Hakyll.Preview.Poll-      Hakyll.Preview.Server    If flag(watchServer)     Build-depends:@@ -299,8 +298,6 @@       system-filepath >= 0.4.6 && <= 0.5     Cpp-options:       -DWATCH_SERVER-    Other-modules:-      Hakyll.Preview.Poll    If flag(checkExternal)     Build-depends:@@ -312,13 +309,10 @@ Executable hakyll-init   Ghc-options:    -Wall   Hs-source-dirs: src-  Main-is:        Hakyll/Init.hs+  Main-is:        Init.hs    Build-depends:+    hakyll,     base      >= 4   && < 5,     directory >= 1.0 && < 1.4,     filepath  >= 1.0 && < 1.5--  Other-modules:-    Hakyll.Core.Util.File-    Paths_hakyll
+ lib/Data/List/Extended.hs view
@@ -0,0 +1,15 @@+module Data.List.Extended+    ( module Data.List+    , breakWhen+    ) where++import           Data.List++-- | Like 'break', but can act on the entire tail of the list.+breakWhen :: ([a] -> Bool) -> [a] -> ([a], [a])+breakWhen predicate = go []+  where+    go buf []                = (reverse buf, [])+    go buf (x : xs)+        | predicate (x : xs) = (reverse buf, x : xs)+        | otherwise          = go (x : buf) xs
+ lib/Data/Yaml/Extended.hs view
@@ -0,0 +1,24 @@+module Data.Yaml.Extended+    ( module Data.Yaml+    , toString+    , toList+    ) where++import qualified Data.Text   as T+import qualified Data.Vector as V+import           Data.Yaml+import           Data.Scientific++toString :: Value -> Maybe String+toString (String t)     = Just (T.unpack t)+toString (Bool   True)  = Just "true"+toString (Bool   False) = Just "false"+-- | Make sure that numeric fields containing integer numbers are shown as+-- | integers (i.e., "42" instead of "42.0").+toString (Number d) | isInteger d = Just (formatScientific Fixed (Just 0) d)+                    | otherwise   = Just (show d)+toString _              = Nothing++toList :: Value -> Maybe [Value]+toList (Array a) = Just (V.toList a)+toList _         = Nothing
+ lib/Hakyll.hs view
@@ -0,0 +1,62 @@+--------------------------------------------------------------------------------+-- | Top-level module exporting all modules that are interesting for the user+{-# LANGUAGE CPP #-}+module Hakyll+    ( module Hakyll.Core.Compiler+    , module Hakyll.Core.Configuration+    , module Hakyll.Core.File+    , module Hakyll.Core.Identifier+    , module Hakyll.Core.Identifier.Pattern+    , module Hakyll.Core.Item+    , module Hakyll.Core.Metadata+    , module Hakyll.Core.Routes+    , module Hakyll.Core.Rules+    , module Hakyll.Core.UnixFilter+    , module Hakyll.Core.Util.File+    , module Hakyll.Core.Util.String+    , module Hakyll.Core.Writable+    , module Hakyll.Main+    , module Hakyll.Web.CompressCss+    , module Hakyll.Web.Feed+    , module Hakyll.Web.Html+    , module Hakyll.Web.Html.RelativizeUrls+    , module Hakyll.Web.Pandoc+    , module Hakyll.Web.Paginate+    , module Hakyll.Web.Pandoc.Biblio+    , module Hakyll.Web.Pandoc.FileType+    , module Hakyll.Web.Redirect+    , module Hakyll.Web.Tags+    , module Hakyll.Web.Template+    , module Hakyll.Web.Template.Context+    , module Hakyll.Web.Template.List+    ) where+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Configuration+import           Hakyll.Core.File+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Item+import           Hakyll.Core.Metadata+import           Hakyll.Core.Routes+import           Hakyll.Core.Rules+import           Hakyll.Core.UnixFilter+import           Hakyll.Core.Util.File+import           Hakyll.Core.Util.String+import           Hakyll.Core.Writable+import           Hakyll.Main+import           Hakyll.Web.CompressCss+import           Hakyll.Web.Feed+import           Hakyll.Web.Html+import           Hakyll.Web.Html.RelativizeUrls+import           Hakyll.Web.Paginate+import           Hakyll.Web.Pandoc+import           Hakyll.Web.Pandoc.Biblio+import           Hakyll.Web.Pandoc.FileType+import           Hakyll.Web.Redirect+import           Hakyll.Web.Tags+import           Hakyll.Web.Template+import           Hakyll.Web.Template.Context+import           Hakyll.Web.Template.List
+ lib/Hakyll/Check.hs view
@@ -0,0 +1,290 @@+--------------------------------------------------------------------------------+{-# LANGUAGE CPP               #-}+{-# LANGUAGE OverloadedStrings #-}+module Hakyll.Check+    ( Check (..)+    , check+    ) where+++--------------------------------------------------------------------------------+import           Control.Concurrent.MVar      (MVar, newEmptyMVar, putMVar,+                                               readMVar)+import           Control.Exception            (SomeAsyncException (..),+                                               SomeException (..), throw, try)+import           Control.Monad                (foldM, forM_)+import           Control.Monad.Reader         (ReaderT, ask, runReaderT)+import           Control.Monad.State          (StateT, get, modify, runStateT)+import           Control.Monad.Trans          (liftIO)+import           Control.Monad.Trans.Resource (runResourceT)+import           Data.ByteString.Char8        (unpack)+import           Data.List                    (isPrefixOf)+import qualified Data.Map.Lazy                as Map+import           Network.URI                  (unEscapeString)+import           System.Directory             (doesDirectoryExist,+                                               doesFileExist)+import           System.Exit                  (ExitCode (..))+import           System.FilePath              (takeDirectory, takeExtension,+                                               (</>))+import qualified Text.HTML.TagSoup            as TS+++--------------------------------------------------------------------------------+#ifdef CHECK_EXTERNAL+import           Data.List                    (intercalate)+import           Data.Typeable                (cast)+import           Data.Version                 (versionBranch)+import           GHC.Exts                     (fromString)+import qualified Network.HTTP.Conduit         as Http+import qualified Network.HTTP.Types           as Http+import qualified Paths_hakyll                 as Paths_hakyll+#endif+++--------------------------------------------------------------------------------+import           Hakyll.Core.Configuration+import           Hakyll.Core.Logger           (Logger)+import qualified Hakyll.Core.Logger           as Logger+import           Hakyll.Core.Util.File+import           Hakyll.Web.Html+++--------------------------------------------------------------------------------+data Check = All | InternalLinks+    deriving (Eq, Ord, Show)+++--------------------------------------------------------------------------------+check :: Configuration -> Logger -> Check -> IO ExitCode+check config logger check' = do+    ((), state) <- runChecker checkDestination config logger check'+    failed <- countFailedLinks state+    return $ if failed > 0 then ExitFailure 1 else ExitSuccess+++--------------------------------------------------------------------------------+countFailedLinks :: CheckerState -> IO Int+countFailedLinks state = foldM addIfFailure 0 (Map.elems state)+    where addIfFailure failures mvar = do+              checkerWrite <- readMVar mvar+              return $ failures + checkerFaulty checkerWrite+++--------------------------------------------------------------------------------+data CheckerRead = CheckerRead+    { checkerConfig :: Configuration+    , checkerLogger :: Logger+    , checkerCheck  :: Check+    }+++--------------------------------------------------------------------------------+data CheckerWrite = CheckerWrite+    { checkerFaulty :: Int+    , checkerOk     :: Int+    } deriving (Show)+++--------------------------------------------------------------------------------+instance Monoid CheckerWrite where+    mempty                                            = CheckerWrite 0 0+    mappend (CheckerWrite f1 o1) (CheckerWrite f2 o2) =+        CheckerWrite (f1 + f2) (o1 + o2)+++--------------------------------------------------------------------------------+type CheckerState = Map.Map URL (MVar CheckerWrite)+++--------------------------------------------------------------------------------+type Checker a = ReaderT CheckerRead (StateT CheckerState IO) a+++--------------------------------------------------------------------------------+type URL = String+++--------------------------------------------------------------------------------+runChecker :: Checker a -> Configuration -> Logger -> Check+           -> IO (a, CheckerState)+runChecker checker config logger check' = do+    let read' = CheckerRead+                    { checkerConfig = config+                    , checkerLogger = logger+                    , checkerCheck  = check'+                    }+    Logger.flush logger+    runStateT (runReaderT checker read') Map.empty+++--------------------------------------------------------------------------------+checkDestination :: Checker ()+checkDestination = do+    config <- checkerConfig <$> ask+    files  <- liftIO $ getRecursiveContents+        (const $ return False) (destinationDirectory config)++    let htmls =+            [ destinationDirectory config </> file+            | file <- files+            , takeExtension file == ".html"+            ]++    forM_ htmls checkFile+++--------------------------------------------------------------------------------+checkFile :: FilePath -> Checker ()+checkFile filePath = do+    logger   <- checkerLogger <$> ask+    contents <- liftIO $ readFile filePath+    Logger.header logger $ "Checking file " ++ filePath++    let urls = getUrls $ TS.parseTags contents+    forM_ urls $ \url -> do+        Logger.debug logger $ "Checking link " ++ url+        m <- liftIO newEmptyMVar+        checkUrlIfNeeded filePath (canonicalizeUrl url) m+    where+        -- Check scheme-relative links+        canonicalizeUrl url = if schemeRelative url then "http:" ++ url else url+        schemeRelative = isPrefixOf "//"+++--------------------------------------------------------------------------------+checkUrlIfNeeded :: FilePath -> URL -> MVar CheckerWrite -> Checker ()+checkUrlIfNeeded filepath url m = do+    logger     <- checkerLogger           <$> ask+    needsCheck <- (== All) . checkerCheck <$> ask+    checked    <- (url `Map.member`)      <$> get+    if not needsCheck || checked+        then Logger.debug logger "Already checked, skipping"+        else do modify $ Map.insert url m+                checkUrl filepath url+++--------------------------------------------------------------------------------+checkUrl :: FilePath -> URL -> Checker ()+checkUrl filePath url+    | isExternal url  = checkExternalUrl url+    | hasProtocol url = skip url $ Just "Unknown protocol, skipping"+    | otherwise       = checkInternalUrl filePath url+  where+    validProtoChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "+-."+    hasProtocol str = case break (== ':') str of+        (proto, ':' : _) -> all (`elem` validProtoChars) proto+        _                -> False+++--------------------------------------------------------------------------------+ok :: URL -> Checker ()+ok url = putCheckResult url mempty {checkerOk = 1}+++--------------------------------------------------------------------------------+skip :: URL -> Maybe String -> Checker ()+skip url maybeReason = do+    logger <- checkerLogger <$> ask+    case maybeReason of+        Nothing     -> return ()+        Just reason -> Logger.debug logger reason+    putCheckResult url mempty {checkerOk = 1}+++--------------------------------------------------------------------------------+faulty :: URL -> Maybe String -> Checker ()+faulty url reason = do+    logger <- checkerLogger <$> ask+    Logger.error logger $ "Broken link to " ++ show url ++ explanation+    putCheckResult url mempty {checkerFaulty = 1}+  where+    formatExplanation = (" (" ++) . (++ ")")+    explanation = maybe "" formatExplanation reason+++--------------------------------------------------------------------------------+putCheckResult :: URL -> CheckerWrite -> Checker ()+putCheckResult url result = do+    state <- get+    let maybeMVar = Map.lookup url state+    case maybeMVar of+        Just m -> liftIO $ putMVar m result+        Nothing -> do+            logger <- checkerLogger <$> ask+            Logger.debug logger "Failed to find existing entry for checked URL"+++--------------------------------------------------------------------------------+checkInternalUrl :: FilePath -> URL -> Checker ()+checkInternalUrl base url = case url' of+    "" -> ok url+    _  -> do+        config <- checkerConfig <$> ask+        let dest = destinationDirectory config+            dir  = takeDirectory base+            filePath+                | "/" `isPrefixOf` url' = dest ++ url'+                | otherwise             = dir </> url'++        exists <- checkFileExists filePath+        if exists then ok url else faulty url Nothing+  where+    url' = stripFragments $ unEscapeString url+++--------------------------------------------------------------------------------+checkExternalUrl :: URL -> Checker ()+#ifdef CHECK_EXTERNAL+checkExternalUrl url = do+    result <- requestExternalUrl url+    case result of+        Left (SomeException e) ->+            case (cast e :: Maybe SomeAsyncException) of+                Just ae -> throw ae+                _       -> faulty url (Just $ showException e)+        Right _ -> ok url+    where+        -- Convert exception to a concise form+        showException e = case cast e of+            Just (Http.HttpExceptionRequest _ e') -> show e'+            _                                     -> head $ words $ show e++requestExternalUrl :: URL -> Checker (Either SomeException Bool)+requestExternalUrl url = liftIO $ try $ do+    mgr <- Http.newManager Http.tlsManagerSettings+    runResourceT $ do+        request  <- Http.parseRequest url+        response <- Http.http (settings request) mgr+        let code = Http.statusCode (Http.responseStatus response)+        return $ code >= 200 && code < 300+    where+        -- Add additional request info+        settings r = r+            { Http.method         = "HEAD"+            , Http.redirectCount  = 10+            , Http.requestHeaders = ("User-Agent", ua) : Http.requestHeaders r+            }++        -- Nice user agent info+        ua = fromString $ "hakyll-check/" +++             (intercalate "." $ map show $ versionBranch Paths_hakyll.version)+#else+checkExternalUrl url = skip url Nothing+#endif+++--------------------------------------------------------------------------------+-- | Wraps doesFileExist, also checks for index.html+checkFileExists :: FilePath -> Checker Bool+checkFileExists filePath = liftIO $ do+    file <- doesFileExist filePath+    dir  <- doesDirectoryExist filePath+    case (file, dir) of+        (True, _) -> return True+        (_, True) -> doesFileExist $ filePath </> "index.html"+        _         -> return False+++--------------------------------------------------------------------------------+stripFragments :: String -> String+stripFragments = takeWhile (not . flip elem ['?', '#'])
+ lib/Hakyll/Commands.hs view
@@ -0,0 +1,160 @@+ --------------------------------------------------------------------------------+-- | Implementation of Hakyll commands: build, preview...+{-# LANGUAGE CPP #-}+module Hakyll.Commands+    ( build+    , check+    , clean+    , preview+    , rebuild+    , server+    , deploy+    , watch+    ) where+++--------------------------------------------------------------------------------+import           Control.Concurrent+import           System.Exit                (ExitCode, exitWith)++--------------------------------------------------------------------------------+import qualified Hakyll.Check               as Check+import           Hakyll.Core.Configuration+import           Hakyll.Core.Logger         (Logger)+import qualified Hakyll.Core.Logger         as Logger+import           Hakyll.Core.Rules+import           Hakyll.Core.Rules.Internal+import           Hakyll.Core.Runtime+import           Hakyll.Core.Util.File++--------------------------------------------------------------------------------+#ifdef WATCH_SERVER+import           Hakyll.Preview.Poll        (watchUpdates)+#endif++#ifdef PREVIEW_SERVER+import           Hakyll.Preview.Server+#endif++#ifdef mingw32_HOST_OS+import           Control.Monad              (void)+import           System.IO.Error            (catchIOError)+#endif+++--------------------------------------------------------------------------------+-- | Build the site+build :: Configuration -> Logger -> Rules a -> IO ExitCode+build conf logger rules = fst <$> run conf logger rules+++--------------------------------------------------------------------------------+-- | Run the checker and exit+check :: Configuration -> Logger -> Check.Check -> IO ExitCode+check = Check.check+++--------------------------------------------------------------------------------+-- | Remove the output directories+clean :: Configuration -> Logger -> IO ()+clean conf logger = do+    remove $ destinationDirectory conf+    remove $ storeDirectory conf+    remove $ tmpDirectory conf+  where+    remove dir = do+        Logger.header logger $ "Removing " ++ dir ++ "..."+        removeDirectory dir+++--------------------------------------------------------------------------------+-- | Preview the site+preview :: Configuration -> Logger -> Rules a -> Int -> IO ()+#ifdef PREVIEW_SERVER+preview conf logger rules port  = do+    deprecatedMessage+    watch conf logger "0.0.0.0" port True rules+  where+    deprecatedMessage = mapM_ putStrLn [ "The preview command has been deprecated."+                                       , "Use the watch command for recompilation and serving."+                                       ]+#else+preview _ _ _ _ = previewServerDisabled+#endif+++--------------------------------------------------------------------------------+-- | Watch and recompile for changes++watch :: Configuration -> Logger -> String -> Int -> Bool -> Rules a -> IO ()+#ifdef WATCH_SERVER+watch conf logger host port runServer rules = do+#ifndef mingw32_HOST_OS+    _ <- forkIO $ watchUpdates conf update+#else+    -- Force windows users to compile with -threaded flag, as otherwise+    -- thread is blocked indefinitely.+    catchIOError (void $ forkOS $ watchUpdates conf update) $ do+        fail $ "Hakyll.Commands.watch: Could not start update watching " +++               "thread. Did you compile with -threaded flag?"+#endif+    server'+  where+    update = do+        (_, ruleSet) <- run conf logger rules+        return $ rulesPattern ruleSet+    loop = threadDelay 100000 >> loop+    server' = if runServer then server conf logger host port else loop+#else+watch _ _ _ _ _ _ = watchServerDisabled+#endif++--------------------------------------------------------------------------------+-- | Rebuild the site+rebuild :: Configuration -> Logger -> Rules a -> IO ExitCode+rebuild conf logger rules =+    clean conf logger >> build conf logger rules++--------------------------------------------------------------------------------+-- | Start a server+server :: Configuration -> Logger -> String -> Int -> IO ()+#ifdef PREVIEW_SERVER+server conf logger host port = do+    let destination = destinationDirectory conf+    staticServer logger destination host port+#else+server _ _ _ _ = previewServerDisabled+#endif+++--------------------------------------------------------------------------------+-- | Upload the site+deploy :: Configuration -> IO ExitCode+deploy conf = deploySite conf conf+++--------------------------------------------------------------------------------+-- | Print a warning message about the preview serving not being enabled+#ifndef PREVIEW_SERVER+previewServerDisabled :: IO ()+previewServerDisabled =+    mapM_ putStrLn+        [ "PREVIEW SERVER"+        , ""+        , "The preview server is not enabled in the version of Hakyll. To"+        , "enable it, set the flag to True and recompile Hakyll."+        , "Alternatively, use an external tool to serve your site directory."+        ]+#endif++#ifndef WATCH_SERVER+watchServerDisabled :: IO ()+watchServerDisabled =+    mapM_ putStrLn+      [ "WATCH SERVER"+      , ""+      , "The watch server is not enabled in the version of Hakyll. To"+      , "enable it, set the flag to True and recompile Hakyll."+      , "Alternatively, use an external tool to serve your site directory."+      ]+#endif
+ lib/Hakyll/Core/Compiler.hs view
@@ -0,0 +1,189 @@+--------------------------------------------------------------------------------+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ScopedTypeVariables        #-}+module Hakyll.Core.Compiler+    ( Compiler+    , getUnderlying+    , getUnderlyingExtension+    , makeItem+    , getRoute+    , getResourceBody+    , getResourceString+    , getResourceLBS+    , getResourceFilePath++    , Internal.Snapshot+    , saveSnapshot+    , Internal.load+    , Internal.loadSnapshot+    , Internal.loadBody+    , Internal.loadSnapshotBody+    , Internal.loadAll+    , Internal.loadAllSnapshots++    , cached+    , unsafeCompiler+    , debugCompiler+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad                 (when, unless)+import           Data.Binary                   (Binary)+import           Data.ByteString.Lazy          (ByteString)+import           Data.Typeable                 (Typeable)+import           System.Environment            (getProgName)+import           System.FilePath               (takeExtension)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler.Internal+import qualified Hakyll.Core.Compiler.Require  as Internal+import           Hakyll.Core.Dependencies+import           Hakyll.Core.Identifier+import           Hakyll.Core.Item+import           Hakyll.Core.Logger            as Logger+import           Hakyll.Core.Provider+import           Hakyll.Core.Routes+import qualified Hakyll.Core.Store             as Store+++--------------------------------------------------------------------------------+-- | Get the underlying identifier.+getUnderlying :: Compiler Identifier+getUnderlying = compilerUnderlying <$> compilerAsk+++--------------------------------------------------------------------------------+-- | Get the extension of the underlying identifier. Returns something like+-- @".html"@+getUnderlyingExtension :: Compiler String+getUnderlyingExtension = takeExtension . toFilePath <$> getUnderlying+++--------------------------------------------------------------------------------+makeItem :: a -> Compiler (Item a)+makeItem x = do+    identifier <- getUnderlying+    return $ Item identifier x+++--------------------------------------------------------------------------------+-- | Get the route for a specified item+getRoute :: Identifier -> Compiler (Maybe FilePath)+getRoute identifier = do+    provider <- compilerProvider <$> compilerAsk+    routes   <- compilerRoutes <$> compilerAsk+    -- Note that this makes us dependend on that identifier: when the metadata+    -- of that item changes, the route may change, hence we have to recompile+    (mfp, um) <- compilerUnsafeIO $ runRoutes routes provider identifier+    when um $ compilerTellDependencies [IdentifierDependency identifier]+    return mfp+++--------------------------------------------------------------------------------+-- | Get the full contents of the matched source file as a string,+-- but without metadata preamble, if there was one.+getResourceBody :: Compiler (Item String)+getResourceBody = getResourceWith resourceBody+++--------------------------------------------------------------------------------+-- | Get the full contents of the matched source file as a string.+getResourceString :: Compiler (Item String)+getResourceString = getResourceWith resourceString+++--------------------------------------------------------------------------------+-- | Get the full contents of the matched source file as a lazy bytestring.+getResourceLBS :: Compiler (Item ByteString)+getResourceLBS = getResourceWith resourceLBS+++--------------------------------------------------------------------------------+-- | Get the file path of the resource we are compiling+getResourceFilePath :: Compiler FilePath+getResourceFilePath = do+    provider <- compilerProvider   <$> compilerAsk+    id'      <- compilerUnderlying <$> compilerAsk+    return $ resourceFilePath provider id'+++--------------------------------------------------------------------------------+-- | Overloadable function for 'getResourceString' and 'getResourceLBS'+getResourceWith :: (Provider -> Identifier -> IO a) -> Compiler (Item a)+getResourceWith reader = do+    provider <- compilerProvider   <$> compilerAsk+    id'      <- compilerUnderlying <$> compilerAsk+    let filePath = toFilePath id'+    if resourceExists provider id'+        then compilerUnsafeIO $ Item id' <$> reader provider id'+        else fail $ error' filePath+  where+    error' fp = "Hakyll.Core.Compiler.getResourceWith: resource " +++        show fp ++ " not found"+++--------------------------------------------------------------------------------+-- | Save a snapshot of the item. This function returns the same item, which+-- convenient for building '>>=' chains.+saveSnapshot :: (Binary a, Typeable a)+             => Internal.Snapshot -> Item a -> Compiler (Item a)+saveSnapshot snapshot item = do+    store  <- compilerStore <$> compilerAsk+    logger <- compilerLogger <$> compilerAsk+    compilerUnsafeIO $ do+        Logger.debug logger $ "Storing snapshot: " ++ snapshot+        Internal.saveSnapshot store snapshot item++    -- Signal that we saved the snapshot.+    Compiler $ \_ -> return $ CompilerSnapshot snapshot (return item)+++--------------------------------------------------------------------------------+cached :: (Binary a, Typeable a)+       => String+       -> Compiler a+       -> Compiler a+cached name compiler = do+    id'      <- compilerUnderlying <$> compilerAsk+    store    <- compilerStore      <$> compilerAsk+    provider <- compilerProvider   <$> compilerAsk++    -- Give a better error message when the resource is not there at all.+    unless (resourceExists provider id') $ fail $ itDoesntEvenExist id'++    let modified = resourceModified provider id'+    if modified+        then do+            x <- compiler+            compilerUnsafeIO $ Store.set store [name, show id'] x+            return x+        else do+            compilerTellCacheHits 1+            x        <- compilerUnsafeIO $ Store.get store [name, show id']+            progName <- compilerUnsafeIO getProgName+            case x of Store.Found x' -> return x'+                      _              -> fail $ error' progName+  where+    error' progName =+        "Hakyll.Core.Compiler.cached: Cache corrupt! " +++         "Try running: " ++ progName ++ " clean"++    itDoesntEvenExist id' =+        "Hakyll.Core.Compiler.cached: You are trying to (perhaps "    +++        "indirectly) use `cached` on a non-existing resource: there " +++        "is no file backing " ++ show id'+++--------------------------------------------------------------------------------+unsafeCompiler :: IO a -> Compiler a+unsafeCompiler = compilerUnsafeIO+++--------------------------------------------------------------------------------+-- | Compiler for debugging purposes+debugCompiler :: String -> Compiler ()+debugCompiler msg = do+    logger <- compilerLogger <$> compilerAsk+    compilerUnsafeIO $ Logger.debug logger msg
+ lib/Hakyll/Core/Compiler/Internal.hs view
@@ -0,0 +1,265 @@+--------------------------------------------------------------------------------+-- | Internally used compiler module+{-# LANGUAGE FlexibleInstances          #-}+{-# LANGUAGE GADTs                      #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE MultiParamTypeClasses      #-}+module Hakyll.Core.Compiler.Internal+    ( -- * Types+      Snapshot+    , CompilerRead (..)+    , CompilerWrite (..)+    , CompilerResult (..)+    , Compiler (..)+    , runCompiler++      -- * Core operations+    , compilerTell+    , compilerAsk+    , compilerThrow+    , compilerCatch+    , compilerResult+    , compilerUnsafeIO++      -- * Utilities+    , compilerTellDependencies+    , compilerTellCacheHits+    ) where+++--------------------------------------------------------------------------------+import           Control.Applicative            (Alternative (..))+import           Control.Exception              (SomeException, handle)+import           Control.Monad                  (forM_)+import           Control.Monad.Except            (MonadError (..))+import           Data.Set                       (Set)+import qualified Data.Set                       as S+++--------------------------------------------------------------------------------+import           Hakyll.Core.Configuration+import           Hakyll.Core.Dependencies+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Logger             (Logger)+import qualified Hakyll.Core.Logger             as Logger+import           Hakyll.Core.Metadata+import           Hakyll.Core.Provider+import           Hakyll.Core.Routes+import           Hakyll.Core.Store+++--------------------------------------------------------------------------------+-- | Whilst compiling an item, it possible to save multiple snapshots of it, and+-- not just the final result.+type Snapshot = String+++--------------------------------------------------------------------------------+-- | Environment in which a compiler runs+data CompilerRead = CompilerRead+    { -- | Main configuration+      compilerConfig     :: Configuration+    , -- | Underlying identifier+      compilerUnderlying :: Identifier+    , -- | Resource provider+      compilerProvider   :: Provider+    , -- | List of all known identifiers+      compilerUniverse   :: Set Identifier+    , -- | Site routes+      compilerRoutes     :: Routes+    , -- | Compiler store+      compilerStore      :: Store+    , -- | Logger+      compilerLogger     :: Logger+    }+++--------------------------------------------------------------------------------+data CompilerWrite = CompilerWrite+    { compilerDependencies :: [Dependency]+    , compilerCacheHits    :: Int+    } deriving (Show)+++--------------------------------------------------------------------------------+instance Monoid CompilerWrite where+    mempty = CompilerWrite [] 0+    mappend (CompilerWrite d1 h1) (CompilerWrite d2 h2) =+        CompilerWrite (d1 ++ d2) (h1 + h2)+++--------------------------------------------------------------------------------+data CompilerResult a where+    CompilerDone     :: a -> CompilerWrite -> CompilerResult a+    CompilerSnapshot :: Snapshot -> Compiler a -> CompilerResult a+    CompilerError    :: [String] -> CompilerResult a+    CompilerRequire  :: (Identifier, Snapshot) -> Compiler a -> CompilerResult a+++--------------------------------------------------------------------------------+-- | A monad which lets you compile items and takes care of dependency tracking+-- for you.+newtype Compiler a = Compiler+    { unCompiler :: CompilerRead -> IO (CompilerResult a)+    }+++--------------------------------------------------------------------------------+instance Functor Compiler where+    fmap f (Compiler c) = Compiler $ \r -> do+        res <- c r+        return $ case res of+            CompilerDone x w      -> CompilerDone (f x) w+            CompilerSnapshot s c' -> CompilerSnapshot s (fmap f c')+            CompilerError e       -> CompilerError e+            CompilerRequire i c'  -> CompilerRequire i (fmap f c')+    {-# INLINE fmap #-}+++--------------------------------------------------------------------------------+instance Monad Compiler where+    return x = Compiler $ \_ -> return $ CompilerDone x mempty+    {-# INLINE return #-}++    Compiler c >>= f = Compiler $ \r -> do+        res <- c r+        case res of+            CompilerDone x w    -> do+                res' <- unCompiler (f x) r+                return $ case res' of+                    CompilerDone y w'     -> CompilerDone y (w `mappend` w')+                    CompilerSnapshot s c' -> CompilerSnapshot s $ do+                        compilerTell w  -- Save dependencies!+                        c'+                    CompilerError e       -> CompilerError e+                    CompilerRequire i c'  -> CompilerRequire i $ do+                        compilerTell w  -- Save dependencies!+                        c'++            CompilerSnapshot s c' -> return $ CompilerSnapshot s (c' >>= f)+            CompilerError e       -> return $ CompilerError e+            CompilerRequire i c'  -> return $ CompilerRequire i (c' >>= f)+    {-# INLINE (>>=) #-}++    fail = compilerThrow . return+    {-# INLINE fail #-}+++--------------------------------------------------------------------------------+instance Applicative Compiler where+    pure x = return x+    {-# INLINE pure #-}++    f <*> x = f >>= \f' -> fmap f' x+    {-# INLINE (<*>) #-}+++--------------------------------------------------------------------------------+instance MonadMetadata Compiler where+    getMetadata = compilerGetMetadata+    getMatches  = compilerGetMatches+++--------------------------------------------------------------------------------+instance MonadError [String] Compiler where+  throwError = compilerThrow+  catchError = compilerCatch+++--------------------------------------------------------------------------------+runCompiler :: Compiler a -> CompilerRead -> IO (CompilerResult a)+runCompiler compiler read' = handle handler $ unCompiler compiler read'+  where+    handler :: SomeException -> IO (CompilerResult a)+    handler e = return $ CompilerError [show e]+++--------------------------------------------------------------------------------+instance Alternative Compiler where+    empty   = compilerThrow []+    x <|> y = compilerCatch x $ \es -> do+        logger <- compilerLogger <$> compilerAsk+        forM_ es $ \e -> compilerUnsafeIO $ Logger.debug logger $+            "Hakyll.Core.Compiler.Internal: Alternative failed: " ++ e+        y+    {-# INLINE (<|>) #-}+++--------------------------------------------------------------------------------+compilerAsk :: Compiler CompilerRead+compilerAsk = Compiler $ \r -> return $ CompilerDone r mempty+{-# INLINE compilerAsk #-}+++--------------------------------------------------------------------------------+compilerTell :: CompilerWrite -> Compiler ()+compilerTell deps = Compiler $ \_ -> return $ CompilerDone () deps+{-# INLINE compilerTell #-}+++--------------------------------------------------------------------------------+compilerThrow :: [String] -> Compiler a+compilerThrow es = Compiler $ \_ -> return $ CompilerError es+{-# INLINE compilerThrow #-}+++--------------------------------------------------------------------------------+compilerCatch :: Compiler a -> ([String] -> Compiler a) -> Compiler a+compilerCatch (Compiler x) f = Compiler $ \r -> do+    res <- x r+    case res of+        CompilerDone res' w  -> return (CompilerDone res' w)+        CompilerSnapshot s c -> return (CompilerSnapshot s (compilerCatch c f))+        CompilerError e      -> unCompiler (f e) r+        CompilerRequire i c  -> return (CompilerRequire i (compilerCatch c f))+{-# INLINE compilerCatch #-}+++--------------------------------------------------------------------------------+-- | Put the result back in a compiler+compilerResult :: CompilerResult a -> Compiler a+compilerResult x = Compiler $ \_ -> return x+{-# INLINE compilerResult #-}+++--------------------------------------------------------------------------------+compilerUnsafeIO :: IO a -> Compiler a+compilerUnsafeIO io = Compiler $ \_ -> do+    x <- io+    return $ CompilerDone x mempty+{-# INLINE compilerUnsafeIO #-}+++--------------------------------------------------------------------------------+compilerTellDependencies :: [Dependency] -> Compiler ()+compilerTellDependencies ds = do+  logger <- compilerLogger <$> compilerAsk+  forM_ ds $ \d -> compilerUnsafeIO $ Logger.debug logger $+      "Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d+  compilerTell mempty {compilerDependencies = ds}+{-# INLINE compilerTellDependencies #-}+++--------------------------------------------------------------------------------+compilerTellCacheHits :: Int -> Compiler ()+compilerTellCacheHits ch = compilerTell mempty {compilerCacheHits = ch}+{-# INLINE compilerTellCacheHits #-}+++--------------------------------------------------------------------------------+compilerGetMetadata :: Identifier -> Compiler Metadata+compilerGetMetadata identifier = do+    provider <- compilerProvider <$> compilerAsk+    compilerTellDependencies [IdentifierDependency identifier]+    compilerUnsafeIO $ resourceMetadata provider identifier+++--------------------------------------------------------------------------------+compilerGetMatches :: Pattern -> Compiler [Identifier]+compilerGetMatches pattern = do+    universe <- compilerUniverse <$> compilerAsk+    let matching = filterMatches pattern $ S.toList universe+        set'     = S.fromList matching+    compilerTellDependencies [PatternDependency pattern set']+    return matching
+ lib/Hakyll/Core/Compiler/Require.hs view
@@ -0,0 +1,121 @@+--------------------------------------------------------------------------------+module Hakyll.Core.Compiler.Require+    ( Snapshot+    , save+    , saveSnapshot+    , load+    , loadSnapshot+    , loadBody+    , loadSnapshotBody+    , loadAll+    , loadAllSnapshots+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad                  (when)+import           Data.Binary                    (Binary)+import qualified Data.Set                       as S+import           Data.Typeable+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Dependencies+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Item+import           Hakyll.Core.Metadata+import           Hakyll.Core.Store              (Store)+import qualified Hakyll.Core.Store              as Store+++--------------------------------------------------------------------------------+save :: (Binary a, Typeable a) => Store -> Item a -> IO ()+save store item = saveSnapshot store final item+++--------------------------------------------------------------------------------+-- | Save a specific snapshot of an item, so you can load it later using+-- 'loadSnapshot'.+saveSnapshot :: (Binary a, Typeable a)+             => Store -> Snapshot -> Item a -> IO ()+saveSnapshot store snapshot item =+    Store.set store (key (itemIdentifier item) snapshot) (itemBody item)+++--------------------------------------------------------------------------------+-- | Load an item compiled elsewhere. If the required item is not yet compiled,+-- the build system will take care of that automatically.+load :: (Binary a, Typeable a) => Identifier -> Compiler (Item a)+load id' = loadSnapshot id' final+++--------------------------------------------------------------------------------+-- | Require a specific snapshot of an item.+loadSnapshot :: (Binary a, Typeable a)+             => Identifier -> Snapshot -> Compiler (Item a)+loadSnapshot id' snapshot = do+    store    <- compilerStore <$> compilerAsk+    universe <- compilerUniverse <$> compilerAsk++    -- Quick check for better error messages+    when (id' `S.notMember` universe) $ fail notFound++    compilerTellDependencies [IdentifierDependency id']+    compilerResult $ CompilerRequire (id', snapshot) $ do+        result <- compilerUnsafeIO $ Store.get store (key id' snapshot)+        case result of+            Store.NotFound      -> fail notFound+            Store.WrongType e r -> fail $ wrongType e r+            Store.Found x       -> return $ Item id' x+  where+    notFound =+        "Hakyll.Core.Compiler.Require.load: " ++ show id' +++        " (snapshot " ++ snapshot ++ ") was not found in the cache, " +++        "the cache might be corrupted or " +++        "the item you are referring to might not exist"+    wrongType e r =+        "Hakyll.Core.Compiler.Require.load: " ++ show id' +++        " (snapshot " ++ snapshot ++ ") was found in the cache, " +++        "but does not have the right type: expected " ++ show e +++        " but got " ++ show r+++--------------------------------------------------------------------------------+-- | A shortcut for only requiring the body of an item.+--+-- > loadBody = fmap itemBody . load+loadBody :: (Binary a, Typeable a) => Identifier -> Compiler a+loadBody id' = loadSnapshotBody id' final+++--------------------------------------------------------------------------------+loadSnapshotBody :: (Binary a, Typeable a)+                 => Identifier -> Snapshot -> Compiler a+loadSnapshotBody id' snapshot = fmap itemBody $ loadSnapshot id' snapshot+++--------------------------------------------------------------------------------+-- | This function allows you to 'load' a dynamic list of items+loadAll :: (Binary a, Typeable a) => Pattern -> Compiler [Item a]+loadAll pattern = loadAllSnapshots pattern final+++--------------------------------------------------------------------------------+loadAllSnapshots :: (Binary a, Typeable a)+                 => Pattern -> Snapshot -> Compiler [Item a]+loadAllSnapshots pattern snapshot = do+    matching <- getMatches pattern+    mapM (\i -> loadSnapshot i snapshot) matching+++--------------------------------------------------------------------------------+key :: Identifier -> String -> [String]+key identifier snapshot =+    ["Hakyll.Core.Compiler.Require", show identifier, snapshot]+++--------------------------------------------------------------------------------+final :: Snapshot+final = "_final"
+ lib/Hakyll/Core/Configuration.hs view
@@ -0,0 +1,134 @@+--------------------------------------------------------------------------------+-- | Exports a datastructure for the top-level hakyll configuration+module Hakyll.Core.Configuration+    ( Configuration (..)+    , shouldIgnoreFile+    , defaultConfiguration+    ) where+++--------------------------------------------------------------------------------+import           Data.Default     (Default (..))+import           Data.List        (isPrefixOf, isSuffixOf)+import           System.Directory (canonicalizePath)+import           System.Exit      (ExitCode)+import           System.FilePath  (isAbsolute, normalise, takeFileName)+import           System.IO.Error  (catchIOError)+import           System.Process   (system)+++--------------------------------------------------------------------------------+data Configuration = Configuration+    { -- | Directory in which the output written+      destinationDirectory :: FilePath+    , -- | Directory where hakyll's internal store is kept+      storeDirectory       :: FilePath+    , -- | Directory in which some temporary files will be kept+      tmpDirectory         :: FilePath+    , -- | Directory where hakyll finds the files to compile. This is @.@ by+      -- default.+      providerDirectory    :: FilePath+    , -- | Function to determine ignored files+      --+      -- In 'defaultConfiguration', the following files are ignored:+      --+      -- * files starting with a @.@+      --+      -- * files starting with a @#@+      --+      -- * files ending with a @~@+      --+      -- * files ending with @.swp@+      --+      -- Note that the files in 'destinationDirectory' and 'storeDirectory' will+      -- also be ignored. Note that this is the configuration parameter, if you+      -- want to use the test, you should use 'shouldIgnoreFile'.+      --+      ignoreFile           :: FilePath -> Bool+    , -- | Here, you can plug in a system command to upload/deploy your site.+      --+      -- Example:+      --+      -- > rsync -ave 'ssh -p 2217' _site jaspervdj@jaspervdj.be:hakyll+      --+      -- You can execute this by using+      --+      -- > ./site deploy+      --+      deployCommand        :: String+    , -- | Function to deploy the site from Haskell.+      --+      -- By default, this command executes the shell command stored in+      -- 'deployCommand'. If you override it, 'deployCommand' will not+      -- be used implicitely.+      --+      -- The 'Configuration' object is passed as a parameter to this+      -- function.+      --+      deploySite           :: Configuration -> IO ExitCode+    , -- | Use an in-memory cache for items. This is faster but uses more+      -- memory.+      inMemoryCache        :: Bool+    , -- | Override default host for preview server. Default is "127.0.0.1",+      -- which binds only on the loopback address.+      -- One can also override the host as a command line argument:+      -- ./site preview -h "0.0.0.0"+      previewHost          :: String+    , -- | Override default port for preview server. Default is 8000.+      -- One can also override the port as a command line argument:+      -- ./site preview -p 1234+      previewPort          :: Int+    }++--------------------------------------------------------------------------------+instance Default Configuration where+    def = defaultConfiguration++--------------------------------------------------------------------------------+-- | Default configuration for a hakyll application+defaultConfiguration :: Configuration+defaultConfiguration = Configuration+    { destinationDirectory = "_site"+    , storeDirectory       = "_cache"+    , tmpDirectory         = "_cache/tmp"+    , providerDirectory    = "."+    , ignoreFile           = ignoreFile'+    , deployCommand        = "echo 'No deploy command specified' && exit 1"+    , deploySite           = system . deployCommand+    , inMemoryCache        = True+    , previewHost          = "127.0.0.1"+    , previewPort          = 8000+    }+  where+    ignoreFile' path+        | "."    `isPrefixOf` fileName = True+        | "#"    `isPrefixOf` fileName = True+        | "~"    `isSuffixOf` fileName = True+        | ".swp" `isSuffixOf` fileName = True+        | otherwise                    = False+      where+        fileName = takeFileName path+++--------------------------------------------------------------------------------+-- | Check if a file should be ignored+shouldIgnoreFile :: Configuration -> FilePath -> IO Bool+shouldIgnoreFile conf path = orM+    [ inDir (destinationDirectory conf)+    , inDir (storeDirectory conf)+    , inDir (tmpDirectory conf)+    , return (ignoreFile conf path')+    ]+  where+    path'    = normalise path+    absolute = isAbsolute path++    inDir dir+        | absolute  = do+            dir' <- catchIOError (canonicalizePath dir) (const $ return dir)+            return $ dir' `isPrefixOf` path'+        | otherwise = return $ dir `isPrefixOf` path'++    orM :: [IO Bool] -> IO Bool+    orM []       = return False+    orM (x : xs) = x >>= \b -> if b then return True else orM xs
+ lib/Hakyll/Core/Dependencies.hs view
@@ -0,0 +1,146 @@+--------------------------------------------------------------------------------+{-# LANGUAGE DeriveDataTypeable #-}+module Hakyll.Core.Dependencies+    ( Dependency (..)+    , DependencyFacts+    , outOfDate+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad                  (foldM, forM_, unless, when)+import           Control.Monad.Reader           (ask)+import           Control.Monad.RWS              (RWS, runRWS)+import qualified Control.Monad.State            as State+import           Control.Monad.Writer           (tell)+import           Data.Binary                    (Binary (..), getWord8,+                                                 putWord8)+import           Data.List                      (find)+import           Data.Map                       (Map)+import qualified Data.Map                       as M+import           Data.Maybe                     (fromMaybe)+import           Data.Set                       (Set)+import qualified Data.Set                       as S+import           Data.Typeable                  (Typeable)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+++--------------------------------------------------------------------------------+data Dependency+    = PatternDependency Pattern (Set Identifier)+    | IdentifierDependency Identifier+    deriving (Show, Typeable)+++--------------------------------------------------------------------------------+instance Binary Dependency where+    put (PatternDependency p is) = putWord8 0 >> put p >> put is+    put (IdentifierDependency i) = putWord8 1 >> put i+    get = getWord8 >>= \t -> case t of+        0 -> PatternDependency <$> get <*> get+        1 -> IdentifierDependency <$> get+        _ -> error "Data.Binary.get: Invalid Dependency"+++--------------------------------------------------------------------------------+type DependencyFacts = Map Identifier [Dependency]+++--------------------------------------------------------------------------------+outOfDate+    :: [Identifier]     -- ^ All known identifiers+    -> Set Identifier   -- ^ Initially out-of-date resources+    -> DependencyFacts  -- ^ Old dependency facts+    -> (Set Identifier, DependencyFacts, [String])+outOfDate universe ood oldFacts =+    let (_, state, logs) = runRWS rws universe (DependencyState oldFacts ood)+    in (dependencyOod state, dependencyFacts state, logs)+  where+    rws = do+        checkNew+        checkChangedPatterns+        bruteForce+++--------------------------------------------------------------------------------+data DependencyState = DependencyState+    { dependencyFacts :: DependencyFacts+    , dependencyOod   :: Set Identifier+    } deriving (Show)+++--------------------------------------------------------------------------------+type DependencyM a = RWS [Identifier] [String] DependencyState a+++--------------------------------------------------------------------------------+markOod :: Identifier -> DependencyM ()+markOod id' = State.modify $ \s ->+    s {dependencyOod = S.insert id' $ dependencyOod s}+++--------------------------------------------------------------------------------+dependenciesFor :: Identifier -> DependencyM [Identifier]+dependenciesFor id' = do+    facts <- dependencyFacts <$> State.get+    return $ concatMap dependenciesFor' $ fromMaybe [] $ M.lookup id' facts+  where+    dependenciesFor' (IdentifierDependency i) = [i]+    dependenciesFor' (PatternDependency _ is) = S.toList is+++--------------------------------------------------------------------------------+checkNew :: DependencyM ()+checkNew = do+    universe <- ask+    facts    <- dependencyFacts <$> State.get+    forM_ universe $ \id' -> unless (id' `M.member` facts) $ do+        tell [show id' ++ " is out-of-date because it is new"]+        markOod id'+++--------------------------------------------------------------------------------+checkChangedPatterns :: DependencyM ()+checkChangedPatterns = do+    facts <- M.toList . dependencyFacts <$> State.get+    forM_ facts $ \(id', deps) -> do+        deps' <- foldM (go id') [] deps+        State.modify $ \s -> s+            {dependencyFacts = M.insert id' deps' $ dependencyFacts s}+  where+    go _   ds (IdentifierDependency i) = return $ IdentifierDependency i : ds+    go id' ds (PatternDependency p ls) = do+        universe <- ask+        let ls' = S.fromList $ filterMatches p universe+        if ls == ls'+            then return $ PatternDependency p ls : ds+            else do+                tell [show id' ++ " is out-of-date because a pattern changed"]+                markOod id'+                return $ PatternDependency p ls' : ds+++--------------------------------------------------------------------------------+bruteForce :: DependencyM ()+bruteForce = do+    todo <- ask+    go todo+  where+    go todo = do+        (todo', changed) <- foldM check ([], False) todo+        when changed (go todo')++    check (todo, changed) id' = do+        deps <- dependenciesFor id'+        ood  <- dependencyOod <$> State.get+        case find (`S.member` ood) deps of+            Nothing -> return (id' : todo, changed)+            Just d  -> do+                tell [show id' ++ " is out-of-date because " +++                    show d ++ " is out-of-date"]+                markOod id'+                return (todo, True)
+ lib/Hakyll/Core/File.hs view
@@ -0,0 +1,93 @@+--------------------------------------------------------------------------------+-- | Exports simple compilers to just copy files+{-# LANGUAGE CPP                        #-}+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Hakyll.Core.File+    ( CopyFile (..)+    , copyFileCompiler+    , TmpFile (..)+    , newTmpFile+    ) where+++--------------------------------------------------------------------------------+import           Data.Binary                   (Binary (..))+import           Data.Typeable                 (Typeable)+#if MIN_VERSION_directory(1,2,6)+import           System.Directory              (copyFileWithMetadata)+#else+import           System.Directory              (copyFile)+#endif+import           System.Directory              (doesFileExist,+                                                renameFile)+import           System.FilePath               ((</>))+import           System.Random                 (randomIO)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Configuration+import           Hakyll.Core.Item+import           Hakyll.Core.Provider+import qualified Hakyll.Core.Store             as Store+import           Hakyll.Core.Util.File+import           Hakyll.Core.Writable+++--------------------------------------------------------------------------------+-- | This will copy any file directly by using a system call+newtype CopyFile = CopyFile FilePath+    deriving (Binary, Eq, Ord, Show, Typeable)+++--------------------------------------------------------------------------------+instance Writable CopyFile where+#if MIN_VERSION_directory(1,2,6)+    write dst (Item _ (CopyFile src)) = copyFileWithMetadata src dst+#else+    write dst (Item _ (CopyFile src)) = copyFile src dst+#endif+--------------------------------------------------------------------------------+copyFileCompiler :: Compiler (Item CopyFile)+copyFileCompiler = do+    identifier <- getUnderlying+    provider   <- compilerProvider <$> compilerAsk+    makeItem $ CopyFile $ resourceFilePath provider identifier+++--------------------------------------------------------------------------------+newtype TmpFile = TmpFile FilePath+    deriving (Typeable)+++--------------------------------------------------------------------------------+instance Binary TmpFile where+    put _ = return ()+    get   = error $+        "Hakyll.Core.File.TmpFile: You tried to load a TmpFile, however, " +++        "this is not possible since these are deleted as soon as possible."+++--------------------------------------------------------------------------------+instance Writable TmpFile where+    write dst (Item _ (TmpFile fp)) = renameFile fp dst+++--------------------------------------------------------------------------------+-- | Create a tmp file+newTmpFile :: String            -- ^ Suffix and extension+           -> Compiler TmpFile  -- ^ Resulting tmp path+newTmpFile suffix = do+    path <- mkPath+    compilerUnsafeIO $ makeDirectories path+    debugCompiler $ "newTmpFile " ++ path+    return $ TmpFile path+  where+    mkPath = do+        rand <- compilerUnsafeIO $ randomIO :: Compiler Int+        tmp  <- tmpDirectory . compilerConfig <$> compilerAsk+        let path = tmp </> Store.hash [show rand] ++ "-" ++ suffix+        exists <- compilerUnsafeIO $ doesFileExist path+        if exists then mkPath else return path
+ lib/Hakyll/Core/Identifier.hs view
@@ -0,0 +1,80 @@+--------------------------------------------------------------------------------+-- | An identifier is a type used to uniquely identify an item. An identifier is+-- conceptually similar to a file path. Examples of identifiers are:+--+-- * @posts/foo.markdown@+--+-- * @index@+--+-- * @error/404@+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Hakyll.Core.Identifier+    ( Identifier+    , fromFilePath+    , toFilePath+    , identifierVersion+    , setVersion+    ) where+++--------------------------------------------------------------------------------+import           Control.DeepSeq     (NFData (..))+import           Data.List           (intercalate)+import           System.FilePath     (dropTrailingPathSeparator, splitPath)+++--------------------------------------------------------------------------------+import           Data.Binary         (Binary (..))+import           Data.Typeable       (Typeable)+import           GHC.Exts            (IsString, fromString)+++--------------------------------------------------------------------------------+data Identifier = Identifier+    { identifierVersion :: Maybe String+    , identifierPath    :: String+    } deriving (Eq, Ord, Typeable)+++--------------------------------------------------------------------------------+instance Binary Identifier where+    put (Identifier v p) = put v >> put p+    get = Identifier <$> get <*> get+++--------------------------------------------------------------------------------+instance IsString Identifier where+    fromString = fromFilePath+++--------------------------------------------------------------------------------+instance NFData Identifier where+    rnf (Identifier v p) = rnf v `seq` rnf p `seq` ()+++--------------------------------------------------------------------------------+instance Show Identifier where+    show i = case identifierVersion i of+        Nothing -> toFilePath i+        Just v  -> toFilePath i ++ " (" ++ v ++ ")"+++--------------------------------------------------------------------------------+-- | Parse an identifier from a string+fromFilePath :: String -> Identifier+fromFilePath = Identifier Nothing .+    intercalate "/" . filter (not . null) . split'+  where+    split' = map dropTrailingPathSeparator . splitPath+++--------------------------------------------------------------------------------+-- | Convert an identifier to a relative 'FilePath'+toFilePath :: Identifier -> FilePath+toFilePath = identifierPath+++--------------------------------------------------------------------------------+setVersion :: Maybe String -> Identifier -> Identifier+setVersion v i = i {identifierVersion = v}
+ lib/Hakyll/Core/Identifier/Pattern.hs view
@@ -0,0 +1,322 @@+--------------------------------------------------------------------------------+-- | As 'Identifier' is used to specify a single item, a 'Pattern' is used to+-- specify a list of items.+--+-- In most cases, globs are used for patterns.+--+-- A very simple pattern of such a pattern is @\"foo\/bar\"@. This pattern will+-- only match the exact @foo\/bar@ identifier.+--+-- To match more than one identifier, there are different captures that one can+-- use:+--+-- * @\"*\"@: matches at most one element of an identifier;+--+-- * @\"**\"@: matches one or more elements of an identifier.+--+-- Some examples:+--+-- * @\"foo\/*\"@ will match @\"foo\/bar\"@ and @\"foo\/foo\"@, but not+--   @\"foo\/bar\/qux\"@;+--+-- * @\"**\"@ will match any identifier;+--+-- * @\"foo\/**\"@ will match @\"foo\/bar\"@ and @\"foo\/bar\/qux\"@, but not+--   @\"bar\/foo\"@;+--+-- * @\"foo\/*.html\"@ will match all HTML files in the @\"foo\/\"@ directory.+--+-- The 'capture' function allows the user to get access to the elements captured+-- by the capture elements in the pattern.+module Hakyll.Core.Identifier.Pattern+    ( -- * The pattern type+      Pattern++      -- * Creating patterns+    , fromGlob+    , fromList+    , fromRegex+    , fromVersion+    , hasVersion+    , hasNoVersion++      -- * Composing patterns+    , (.&&.)+    , (.||.)+    , complement++      -- * Applying patterns+    , matches+    , filterMatches++      -- * Capturing strings+    , capture+    , fromCapture+    , fromCaptures+    ) where+++--------------------------------------------------------------------------------+import           Control.Arrow          ((&&&), (>>>))+import           Control.Monad          (msum)+import           Data.Binary            (Binary (..), getWord8, putWord8)+import           Data.List              (inits, isPrefixOf, tails)+import           Data.Maybe             (isJust)+import           Data.Set               (Set)+import qualified Data.Set               as S+++--------------------------------------------------------------------------------+import           GHC.Exts               (IsString, fromString)+import           Text.Regex.TDFA        ((=~))+++--------------------------------------------------------------------------------+import           Hakyll.Core.Identifier+++--------------------------------------------------------------------------------+-- | Elements of a glob pattern+data GlobComponent+    = Capture+    | CaptureMany+    | Literal String+    deriving (Eq, Show)+++--------------------------------------------------------------------------------+instance Binary GlobComponent where+    put Capture     = putWord8 0+    put CaptureMany = putWord8 1+    put (Literal s) = putWord8 2 >> put s++    get = getWord8 >>= \t -> case t of+        0 -> pure Capture+        1 -> pure CaptureMany+        2 -> Literal <$> get+        _ -> error "Data.Binary.get: Invalid GlobComponent"+++--------------------------------------------------------------------------------+-- | Type that allows matching on identifiers+data Pattern+    = Everything+    | Complement Pattern+    | And Pattern Pattern+    | Glob [GlobComponent]+    | List (Set Identifier)+    | Regex String+    | Version (Maybe String)+    deriving (Show)+++--------------------------------------------------------------------------------+instance Binary Pattern where+    put Everything     = putWord8 0+    put (Complement p) = putWord8 1 >> put p+    put (And x y)      = putWord8 2 >> put x >> put y+    put (Glob g)       = putWord8 3 >> put g+    put (List is)      = putWord8 4 >> put is+    put (Regex r)      = putWord8 5 >> put r+    put (Version v)    = putWord8 6 >> put v++    get = getWord8 >>= \t -> case t of+        0 -> pure Everything+        1 -> Complement <$> get+        2 -> And <$> get <*> get+        3 -> Glob <$> get+        4 -> List <$> get+        5 -> Regex <$> get+        _ -> Version <$> get+++--------------------------------------------------------------------------------+instance IsString Pattern where+    fromString = fromGlob+++--------------------------------------------------------------------------------+instance Monoid Pattern where+    mempty  = Everything+    mappend = (.&&.)+++--------------------------------------------------------------------------------+-- | Parse a pattern from a string+fromGlob :: String -> Pattern+fromGlob = Glob . parse'+  where+    parse' str =+        let (chunk, rest) = break (`elem` "\\*") str+        in case rest of+            ('\\' : x   : xs) -> Literal (chunk ++ [x]) : parse' xs+            ('*'  : '*' : xs) -> Literal chunk : CaptureMany : parse' xs+            ('*'  : xs)       -> Literal chunk : Capture : parse' xs+            xs                -> Literal chunk : Literal xs : []+++--------------------------------------------------------------------------------+-- | Create a 'Pattern' from a list of 'Identifier's it should match.+--+-- /Warning/: use this carefully with 'hasNoVersion' and 'hasVersion'. The+-- 'Identifier's in the list /already/ have versions assigned, and the pattern+-- will then only match the intersection of both versions.+--+-- A more concrete example,+--+-- > fromList ["foo.markdown"] .&&. hasVersion "pdf"+--+-- will not match anything! The @"foo.markdown"@ 'Identifier' has no version+-- assigned, so the LHS of '.&&.' will only match this 'Identifier' with no+-- version. The RHS only matches 'Identifier's with version set to @"pdf"@ --+-- hence, this pattern matches nothing.+--+-- The correct way to use this is:+--+-- > fromList $ map (setVersion $ Just "pdf") ["foo.markdown"]+fromList :: [Identifier] -> Pattern+fromList = List . S.fromList+++--------------------------------------------------------------------------------+-- | Create a 'Pattern' from a regex+--+-- Example:+--+-- > regex "^foo/[^x]*$+fromRegex :: String -> Pattern+fromRegex = Regex+++--------------------------------------------------------------------------------+-- | Create a pattern which matches all items with the given version.+fromVersion :: Maybe String -> Pattern+fromVersion = Version+++--------------------------------------------------------------------------------+-- | Specify a version, e.g.+--+-- > "foo/*.markdown" .&&. hasVersion "pdf"+hasVersion :: String -> Pattern+hasVersion = fromVersion . Just+++--------------------------------------------------------------------------------+-- | Match only if the identifier has no version set, e.g.+--+-- > "foo/*.markdown" .&&. hasNoVersion+hasNoVersion :: Pattern+hasNoVersion = fromVersion Nothing+++--------------------------------------------------------------------------------+-- | '&&' for patterns: the given identifier must match both subterms+(.&&.) :: Pattern -> Pattern -> Pattern+x .&&. y = And x y+infixr 3 .&&.+++--------------------------------------------------------------------------------+-- | '||' for patterns: the given identifier must match any subterm+(.||.) :: Pattern -> Pattern -> Pattern+x .||. y = complement (complement x `And` complement y)  -- De Morgan's law+infixr 2 .||.+++--------------------------------------------------------------------------------+-- | Inverts a pattern, e.g.+--+-- > complement "foo/bar.html"+--+-- will match /anything/ except @\"foo\/bar.html\"@+complement :: Pattern -> Pattern+complement = Complement+++--------------------------------------------------------------------------------+-- | Check if an identifier matches a pattern+matches :: Pattern -> Identifier -> Bool+matches Everything     _ = True+matches (Complement p) i = not $ matches p i+matches (And x y)      i = matches x i && matches y i+matches (Glob p)       i = isJust $ capture (Glob p) i+matches (List l)       i = i `S.member` l+matches (Regex r)      i = toFilePath i =~ r+matches (Version v)    i = identifierVersion i == v+++--------------------------------------------------------------------------------+-- | Given a list of identifiers, retain only those who match the given pattern+filterMatches :: Pattern -> [Identifier] -> [Identifier]+filterMatches = filter . matches+++--------------------------------------------------------------------------------+-- | Split a list at every possible point, generate a list of (init, tail)+-- cases. The result is sorted with inits decreasing in length.+splits :: [a] -> [([a], [a])]+splits = inits &&& tails >>> uncurry zip >>> reverse+++--------------------------------------------------------------------------------+-- | Match a glob against a pattern, generating a list of captures+capture :: Pattern -> Identifier -> Maybe [String]+capture (Glob p) i = capture' p (toFilePath i)+capture _        _ = Nothing+++--------------------------------------------------------------------------------+-- | Internal verion of 'capture'+capture' :: [GlobComponent] -> String -> Maybe [String]+capture' [] [] = Just []  -- An empty match+capture' [] _  = Nothing  -- No match+capture' (Literal l : ms) str+    -- Match the literal against the string+    | l `isPrefixOf` str = capture' ms $ drop (length l) str+    | otherwise          = Nothing+capture' (Capture : ms) str =+    -- Match until the next /+    let (chunk, rest) = break (== '/') str+    in msum $ [ fmap (i :) (capture' ms (t ++ rest)) | (i, t) <- splits chunk ]+capture' (CaptureMany : ms) str =+    -- Match everything+    msum $ [ fmap (i :) (capture' ms t) | (i, t) <- splits str ]+++--------------------------------------------------------------------------------+-- | Create an identifier from a pattern by filling in the captures with a given+-- string+--+-- Example:+--+-- > fromCapture (fromGlob "tags/*") "foo"+--+-- Result:+--+-- > "tags/foo"+fromCapture :: Pattern -> String -> Identifier+fromCapture pattern = fromCaptures pattern . repeat+++--------------------------------------------------------------------------------+-- | Create an identifier from a pattern by filling in the captures with the+-- given list of strings+fromCaptures :: Pattern -> [String] -> Identifier+fromCaptures (Glob p) = fromFilePath . fromCaptures' p+fromCaptures _        = error $+    "Hakyll.Core.Identifier.Pattern.fromCaptures: fromCaptures only works " +++    "on simple globs!"+++--------------------------------------------------------------------------------+-- | Internally used version of 'fromCaptures'+fromCaptures' :: [GlobComponent] -> [String] -> String+fromCaptures' []        _ = mempty+fromCaptures' (m : ms) [] = case m of+    Literal l -> l `mappend` fromCaptures' ms []+    _         -> error $  "Hakyll.Core.Identifier.Pattern.fromCaptures': "+                       ++ "identifier list exhausted"+fromCaptures' (m : ms) ids@(i : is) = case m of+    Literal l -> l `mappend` fromCaptures' ms ids+    _         -> i `mappend` fromCaptures' ms is
+ lib/Hakyll/Core/Item.hs view
@@ -0,0 +1,63 @@+--------------------------------------------------------------------------------+-- | An item is a combination of some content and its 'Identifier'. This way, we+-- can still use the 'Identifier' to access metadata.+{-# LANGUAGE DeriveDataTypeable #-}+module Hakyll.Core.Item+    ( Item (..)+    , itemSetBody+    , withItemBody+    ) where+++--------------------------------------------------------------------------------+import           Data.Binary                   (Binary (..))+import           Data.Foldable                 (Foldable (..))+import           Data.Typeable                 (Typeable)+import           Prelude                       hiding (foldr)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Identifier+++--------------------------------------------------------------------------------+data Item a = Item+    { itemIdentifier :: Identifier+    , itemBody       :: a+    } deriving (Show, Typeable)+++--------------------------------------------------------------------------------+instance Functor Item where+    fmap f (Item i x) = Item i (f x)+++--------------------------------------------------------------------------------+instance Foldable Item where+    foldr f z (Item _ x) = f x z+++--------------------------------------------------------------------------------+instance Traversable Item where+    traverse f (Item i x) = Item i <$> f x+++--------------------------------------------------------------------------------+instance Binary a => Binary (Item a) where+    put (Item i x) = put i >> put x+    get            = Item <$> get <*> get+++--------------------------------------------------------------------------------+itemSetBody :: a -> Item b -> Item a+itemSetBody x (Item i _) = Item i x+++--------------------------------------------------------------------------------+-- | Perform a compiler action on the item body. This is the same as 'traverse',+-- but looks less intimidating.+--+-- > withItemBody = traverse+withItemBody :: (a -> Compiler b) -> Item a -> Compiler (Item b)+withItemBody = traverse
+ lib/Hakyll/Core/Item/SomeItem.hs view
@@ -0,0 +1,23 @@+--------------------------------------------------------------------------------+{-# LANGUAGE DeriveDataTypeable        #-}+{-# LANGUAGE ExistentialQuantification #-}+module Hakyll.Core.Item.SomeItem+    ( SomeItem (..)+    ) where+++--------------------------------------------------------------------------------+import           Data.Binary          (Binary)+import           Data.Typeable        (Typeable)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Item+import           Hakyll.Core.Writable+++--------------------------------------------------------------------------------+-- | An existential type, mostly for internal usage.+data SomeItem = forall a.+    (Binary a, Typeable a, Writable a) => SomeItem (Item a)+    deriving (Typeable)
+ lib/Hakyll/Core/Logger.hs view
@@ -0,0 +1,97 @@+--------------------------------------------------------------------------------+-- | Produce pretty, thread-safe logs+module Hakyll.Core.Logger+    ( Verbosity (..)+    , Logger+    , new+    , flush+    , error+    , header+    , message+    , debug+    ) where+++--------------------------------------------------------------------------------+import           Control.Concurrent      (forkIO)+import           Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)+import           Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)+import           Control.Monad           (forever)+import           Control.Monad.Trans     (MonadIO, liftIO)+import           Prelude                 hiding (error)+++--------------------------------------------------------------------------------+data Verbosity+    = Error+    | Message+    | Debug+    deriving (Eq, Ord, Show)+++--------------------------------------------------------------------------------+-- | Logger structure. Very complicated.+data Logger = Logger+    { loggerChan      :: Chan (Maybe String)  -- ^ Nothing marks the end+    , loggerSync      :: MVar ()              -- ^ Used for sync on quit+    , loggerSink      :: String -> IO ()      -- ^ Out sink+    , loggerVerbosity :: Verbosity            -- ^ Verbosity+    }+++--------------------------------------------------------------------------------+-- | Create a new logger+new :: Verbosity -> IO Logger+new vbty = do+    logger <- Logger <$>+        newChan <*> newEmptyMVar <*> pure putStrLn <*> pure vbty+    _      <- forkIO $ loggerThread logger+    return logger+  where+    loggerThread logger = forever $ do+        msg <- readChan $ loggerChan logger+        case msg of+            -- Stop: sync+            Nothing -> putMVar (loggerSync logger) ()+            -- Print and continue+            Just m  -> loggerSink logger m+++--------------------------------------------------------------------------------+-- | Flush the logger (blocks until flushed)+flush :: Logger -> IO ()+flush logger = do+    writeChan (loggerChan logger) Nothing+    () <- takeMVar $ loggerSync logger+    return ()+++--------------------------------------------------------------------------------+string :: MonadIO m+       => Logger     -- ^ Logger+       -> Verbosity  -- ^ Verbosity of the string+       -> String     -- ^ Section name+       -> m ()       -- ^ No result+string l v m+    | loggerVerbosity l >= v = liftIO $ writeChan (loggerChan l) (Just m)+    | otherwise              = return ()+++--------------------------------------------------------------------------------+error :: MonadIO m => Logger -> String -> m ()+error l m = string l Error $ "  [ERROR] " ++ m+++--------------------------------------------------------------------------------+header :: MonadIO m => Logger -> String -> m ()+header l = string l Message+++--------------------------------------------------------------------------------+message :: MonadIO m => Logger -> String -> m ()+message l m = string l Message $ "  " ++ m+++--------------------------------------------------------------------------------+debug :: MonadIO m => Logger -> String -> m ()+debug l m = string l Debug $ "  [DEBUG] " ++ m
+ lib/Hakyll/Core/Metadata.hs view
@@ -0,0 +1,138 @@+--------------------------------------------------------------------------------+module Hakyll.Core.Metadata+    ( Metadata+    , lookupString+    , lookupStringList++    , MonadMetadata (..)+    , getMetadataField+    , getMetadataField'+    , makePatternDependency++    , BinaryMetadata (..)+    ) where+++--------------------------------------------------------------------------------+import           Control.Arrow                  (second)+import           Control.Monad                  (forM)+import           Data.Binary                    (Binary (..), getWord8,+                                                 putWord8, Get)+import qualified Data.HashMap.Strict            as HMS+import qualified Data.Set                       as S+import qualified Data.Text                      as T+import qualified Data.Vector                    as V+import qualified Data.Yaml.Extended                      as Yaml+import           Hakyll.Core.Dependencies+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+++--------------------------------------------------------------------------------+type Metadata = Yaml.Object+++--------------------------------------------------------------------------------+lookupString :: String -> Metadata -> Maybe String+lookupString key meta = HMS.lookup (T.pack key) meta >>= Yaml.toString+++--------------------------------------------------------------------------------+lookupStringList :: String -> Metadata -> Maybe [String]+lookupStringList key meta =+    HMS.lookup (T.pack key) meta >>= Yaml.toList >>= mapM Yaml.toString+++--------------------------------------------------------------------------------+class Monad m => MonadMetadata m where+    getMetadata    :: Identifier -> m Metadata+    getMatches     :: Pattern -> m [Identifier]++    getAllMetadata :: Pattern -> m [(Identifier, Metadata)]+    getAllMetadata pattern = do+        matches' <- getMatches pattern+        forM matches' $ \id' -> do+            metadata <- getMetadata id'+            return (id', metadata)+++--------------------------------------------------------------------------------+getMetadataField :: MonadMetadata m => Identifier -> String -> m (Maybe String)+getMetadataField identifier key = do+    metadata <- getMetadata identifier+    return $ lookupString key metadata+++--------------------------------------------------------------------------------+-- | Version of 'getMetadataField' which throws an error if the field does not+-- exist.+getMetadataField' :: MonadMetadata m => Identifier -> String -> m String+getMetadataField' identifier key = do+    field <- getMetadataField identifier key+    case field of+        Just v  -> return v+        Nothing -> fail $ "Hakyll.Core.Metadata.getMetadataField': " +++            "Item " ++ show identifier ++ " has no metadata field " ++ show key+++--------------------------------------------------------------------------------+makePatternDependency :: MonadMetadata m => Pattern -> m Dependency+makePatternDependency pattern = do+    matches' <- getMatches pattern+    return $ PatternDependency pattern (S.fromList matches')+++--------------------------------------------------------------------------------+-- | Newtype wrapper for serialization.+newtype BinaryMetadata = BinaryMetadata+    {unBinaryMetadata :: Metadata}+++instance Binary BinaryMetadata where+    put (BinaryMetadata obj) = put (BinaryYaml $ Yaml.Object obj)+    get = do+        BinaryYaml (Yaml.Object obj) <- get+        return $ BinaryMetadata obj+++--------------------------------------------------------------------------------+newtype BinaryYaml = BinaryYaml {unBinaryYaml :: Yaml.Value}+++--------------------------------------------------------------------------------+instance Binary BinaryYaml where+    put (BinaryYaml yaml) = case yaml of+        Yaml.Object obj -> do+            putWord8 0+            let list :: [(T.Text, BinaryYaml)]+                list = map (second BinaryYaml) $ HMS.toList obj+            put list++        Yaml.Array arr -> do+            putWord8 1+            let list = map BinaryYaml (V.toList arr) :: [BinaryYaml]+            put list++        Yaml.String s -> putWord8 2 >> put s+        Yaml.Number n -> putWord8 3 >> put n+        Yaml.Bool   b -> putWord8 4 >> put b+        Yaml.Null     -> putWord8 5++    get = do+        tag <- getWord8+        case tag of+            0 -> do+                list <- get :: Get [(T.Text, BinaryYaml)]+                return $ BinaryYaml $ Yaml.Object $+                    HMS.fromList $ map (second unBinaryYaml) list++            1 -> do+                list <- get :: Get [BinaryYaml]+                return $ BinaryYaml $+                    Yaml.Array $ V.fromList $ map unBinaryYaml list++            2 -> BinaryYaml . Yaml.String <$> get+            3 -> BinaryYaml . Yaml.Number <$> get+            4 -> BinaryYaml . Yaml.Bool   <$> get+            5 -> return $ BinaryYaml Yaml.Null+            _ -> fail "Data.Binary.get: Invalid Binary Metadata"
+ lib/Hakyll/Core/Provider.hs view
@@ -0,0 +1,43 @@+--------------------------------------------------------------------------------+-- | This module provides an wrapper API around the file system which does some+-- caching.+module Hakyll.Core.Provider+    ( -- * Constructing resource providers+      Internal.Provider+    , newProvider++      -- * Querying resource properties+    , Internal.resourceList+    , Internal.resourceExists+    , Internal.resourceFilePath+    , Internal.resourceModified+    , Internal.resourceModificationTime++      -- * Access to raw resource content+    , Internal.resourceString+    , Internal.resourceLBS++      -- * Access to metadata and body content+    , Internal.resourceMetadata+    , Internal.resourceBody+    ) where+++--------------------------------------------------------------------------------+import qualified Hakyll.Core.Provider.Internal      as Internal+import qualified Hakyll.Core.Provider.MetadataCache as Internal+import           Hakyll.Core.Store                  (Store)+++--------------------------------------------------------------------------------+-- | Create a resource provider+newProvider :: Store                   -- ^ Store to use+            -> (FilePath -> IO Bool)   -- ^ Should we ignore this file?+            -> FilePath                -- ^ Search directory+            -> IO Internal.Provider    -- ^ Resulting provider+newProvider store ignore directory = do+    -- Delete metadata cache where necessary+    p <- Internal.newProvider store ignore directory+    mapM_ (Internal.resourceInvalidateMetadataCache p) $+        filter (Internal.resourceModified p) $ Internal.resourceList p+    return p
+ lib/Hakyll/Core/Provider/Internal.hs view
@@ -0,0 +1,202 @@+--------------------------------------------------------------------------------+{-# LANGUAGE CPP                        #-}+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Hakyll.Core.Provider.Internal+    ( ResourceInfo (..)+    , Provider (..)+    , newProvider++    , resourceList+    , resourceExists++    , resourceFilePath+    , resourceString+    , resourceLBS++    , resourceModified+    , resourceModificationTime+    ) where+++--------------------------------------------------------------------------------+import           Control.DeepSeq        (NFData (..), deepseq)+import           Control.Monad          (forM)+import           Data.Binary            (Binary (..))+import qualified Data.ByteString.Lazy   as BL+import           Data.Map               (Map)+import qualified Data.Map               as M+import           Data.Maybe             (fromMaybe)+import           Data.Set               (Set)+import qualified Data.Set               as S+import           Data.Time              (Day (..), UTCTime (..))+import           Data.Typeable          (Typeable)+import           System.Directory       (getModificationTime)+import           System.FilePath        (addExtension, (</>))+++--------------------------------------------------------------------------------+#if !MIN_VERSION_directory(1,2,0)+import           Data.Time              (readTime)+import           System.Locale          (defaultTimeLocale)+import           System.Time            (formatCalendarTime, toCalendarTime)+#endif+++--------------------------------------------------------------------------------+import           Hakyll.Core.Identifier+import           Hakyll.Core.Store      (Store)+import qualified Hakyll.Core.Store      as Store+import           Hakyll.Core.Util.File+++--------------------------------------------------------------------------------+-- | Because UTCTime doesn't have a Binary instance...+newtype BinaryTime = BinaryTime {unBinaryTime :: UTCTime}+    deriving (Eq, NFData, Ord, Show, Typeable)+++--------------------------------------------------------------------------------+instance Binary BinaryTime where+    put (BinaryTime (UTCTime (ModifiedJulianDay d) dt)) =+        put d >> put (toRational dt)++    get = fmap BinaryTime $ UTCTime+        <$> (ModifiedJulianDay <$> get)+        <*> (fromRational <$> get)+++--------------------------------------------------------------------------------+data ResourceInfo = ResourceInfo+    { resourceInfoModified :: BinaryTime+    , resourceInfoMetadata :: Maybe Identifier+    } deriving (Show, Typeable)+++--------------------------------------------------------------------------------+instance Binary ResourceInfo where+    put (ResourceInfo mtime meta) = put mtime >> put meta+    get = ResourceInfo <$> get <*> get+++--------------------------------------------------------------------------------+instance NFData ResourceInfo where+    rnf (ResourceInfo mtime meta) = rnf mtime `seq` rnf meta `seq` ()+++--------------------------------------------------------------------------------+-- | Responsible for retrieving and listing resources+data Provider = Provider+    { -- Top of the provided directory+      providerDirectory :: FilePath+    , -- | A list of all files found+      providerFiles     :: Map Identifier ResourceInfo+    , -- | A list of the files from the previous run+      providerOldFiles  :: Map Identifier ResourceInfo+    , -- | Underlying persistent store for caching+      providerStore     :: Store+    } deriving (Show)+++--------------------------------------------------------------------------------+-- | Create a resource provider+newProvider :: Store                  -- ^ Store to use+            -> (FilePath -> IO Bool)  -- ^ Should we ignore this file?+            -> FilePath               -- ^ Search directory+            -> IO Provider            -- ^ Resulting provider+newProvider store ignore directory = do+    list <- map fromFilePath <$> getRecursiveContents ignore directory+    let universe = S.fromList list+    files <- fmap (maxmtime . M.fromList) $ forM list $ \identifier -> do+        rInfo <- getResourceInfo directory universe identifier+        return (identifier, rInfo)++    -- Get the old files from the store, and then immediately replace them by+    -- the new files.+    oldFiles <- fromMaybe mempty . Store.toMaybe <$> Store.get store oldKey+    oldFiles `deepseq` Store.set store oldKey files++    return $ Provider directory files oldFiles store+  where+    oldKey = ["Hakyll.Core.Provider.Internal.newProvider", "oldFiles"]++    -- Update modified if metadata is modified+    maxmtime files = flip M.map files $ \rInfo@(ResourceInfo mtime meta) ->+        let metaMod = fmap resourceInfoModified $ meta >>= flip M.lookup files+        in rInfo {resourceInfoModified = maybe mtime (max mtime) metaMod}+++--------------------------------------------------------------------------------+getResourceInfo :: FilePath -> Set Identifier -> Identifier -> IO ResourceInfo+getResourceInfo directory universe identifier = do+    mtime <- fileModificationTime $ directory </> toFilePath identifier+    return $ ResourceInfo (BinaryTime mtime) $+        if mdRsc `S.member` universe then Just mdRsc else Nothing+  where+    mdRsc = fromFilePath $ flip addExtension "metadata" $ toFilePath identifier+++--------------------------------------------------------------------------------+resourceList :: Provider -> [Identifier]+resourceList = M.keys . providerFiles+++--------------------------------------------------------------------------------+-- | Check if a given resource exists+resourceExists :: Provider -> Identifier -> Bool+resourceExists provider =+    (`M.member` providerFiles provider) . setVersion Nothing+++--------------------------------------------------------------------------------+resourceFilePath :: Provider -> Identifier -> FilePath+resourceFilePath p i = providerDirectory p </> toFilePath i+++--------------------------------------------------------------------------------+-- | Get the raw body of a resource as string+resourceString :: Provider -> Identifier -> IO String+resourceString p i = readFile $ resourceFilePath p i+++--------------------------------------------------------------------------------+-- | Get the raw body of a resource of a lazy bytestring+resourceLBS :: Provider -> Identifier -> IO BL.ByteString+resourceLBS p i = BL.readFile $ resourceFilePath p i+++--------------------------------------------------------------------------------+-- | A resource is modified if it or its metadata has changed+resourceModified :: Provider -> Identifier -> Bool+resourceModified p r = case (ri, oldRi) of+    (Nothing, _)      -> False+    (Just _, Nothing) -> True+    (Just n, Just o)  ->+        resourceInfoModified n >  resourceInfoModified o ||+        resourceInfoMetadata n /= resourceInfoMetadata o+  where+    normal = setVersion Nothing r+    ri     = M.lookup normal (providerFiles p)+    oldRi  = M.lookup normal (providerOldFiles p)+++--------------------------------------------------------------------------------+resourceModificationTime :: Provider -> Identifier -> UTCTime+resourceModificationTime p i =+    case M.lookup (setVersion Nothing i) (providerFiles p) of+        Just ri -> unBinaryTime $ resourceInfoModified ri+        Nothing -> error $+            "Hakyll.Core.Provider.Internal.resourceModificationTime: " +++            "resource " ++ show i ++ " does not exist"+++--------------------------------------------------------------------------------+fileModificationTime :: FilePath -> IO UTCTime+fileModificationTime fp = do+#if MIN_VERSION_directory(1,2,0)+    getModificationTime fp+#else+    ct <- toCalendarTime =<< getModificationTime fp+    let str = formatCalendarTime defaultTimeLocale "%s" ct+    return $ readTime defaultTimeLocale "%s" str+#endif
+ lib/Hakyll/Core/Provider/Metadata.hs view
@@ -0,0 +1,151 @@+--------------------------------------------------------------------------------+-- | Internal module to parse metadata+{-# LANGUAGE BangPatterns    #-}+{-# LANGUAGE RecordWildCards #-}+module Hakyll.Core.Provider.Metadata+    ( loadMetadata+    , parsePage++    , MetadataException (..)+    ) where+++--------------------------------------------------------------------------------+import           Control.Arrow                 (second)+import           Control.Exception             (Exception, throwIO)+import           Control.Monad                 (guard)+import qualified Data.ByteString               as B+import qualified Data.ByteString.Char8         as BC+import           Data.List.Extended            (breakWhen)+import qualified Data.Map                      as M+import           Data.Maybe                    (fromMaybe)+import           Data.Monoid                   ((<>))+import qualified Data.Text                     as T+import qualified Data.Text.Encoding            as T+import qualified Data.Yaml                     as Yaml+import           Hakyll.Core.Identifier+import           Hakyll.Core.Metadata+import           Hakyll.Core.Provider.Internal+import           System.IO                     as IO+++--------------------------------------------------------------------------------+loadMetadata :: Provider -> Identifier -> IO (Metadata, Maybe String)+loadMetadata p identifier = do+    hasHeader  <- probablyHasMetadataHeader fp+    (md, body) <- if hasHeader+        then second Just <$> loadMetadataHeader fp+        else return (mempty, Nothing)++    emd <- case mi of+        Nothing  -> return mempty+        Just mi' -> loadMetadataFile $ resourceFilePath p mi'++    return (md <> emd, body)+  where+    normal = setVersion Nothing identifier+    fp     = resourceFilePath p identifier+    mi     = M.lookup normal (providerFiles p) >>= resourceInfoMetadata+++--------------------------------------------------------------------------------+loadMetadataHeader :: FilePath -> IO (Metadata, String)+loadMetadataHeader fp = do+    fileContent <- readFile fp+    case parsePage fileContent of+        Right x   -> return x+        Left  err -> throwIO $ MetadataException fp err+++--------------------------------------------------------------------------------+loadMetadataFile :: FilePath -> IO Metadata+loadMetadataFile fp = do+    fileContent <- B.readFile fp+    let errOrMeta = Yaml.decodeEither' fileContent+    either (fail . show) return errOrMeta+++--------------------------------------------------------------------------------+-- | Check if a file "probably" has a metadata header. The main goal of this is+-- to exclude binary files (which are unlikely to start with "---").+probablyHasMetadataHeader :: FilePath -> IO Bool+probablyHasMetadataHeader fp = do+    handle <- IO.openFile fp IO.ReadMode+    bs     <- BC.hGet handle 1024+    IO.hClose handle+    return $ isMetadataHeader bs+  where+    isMetadataHeader bs =+        let pre = BC.takeWhile (\x -> x /= '\n' && x /= '\r') bs+        in  BC.length pre >= 3 && BC.all (== '-') pre+++--------------------------------------------------------------------------------+-- | Parse the page metadata and body.+splitMetadata :: String -> (Maybe String, String)+splitMetadata str0 = fromMaybe (Nothing, str0) $ do+    guard $ leading >= 3+    let !str1 = drop leading str0+    guard $ all isNewline (take 1 str1)+    let !(!meta, !content0) = breakWhen isTrailing str1+    guard $ not $ null content0+    let !content1 = drop (leading + 1) content0+        !content2 = dropWhile isNewline $ dropWhile isInlineSpace content1+    -- Adding this newline fixes the line numbers reported by the YAML parser.+    -- It's a bit ugly but it works.+    return (Just ('\n' : meta), content2)+  where+    -- Parse the leading "---"+    !leading = length $ takeWhile (== '-') str0++    -- Predicate to recognize the trailing "---" or "..."+    isTrailing []       = False+    isTrailing (x : xs) =+        isNewline x && length (takeWhile isDash xs) == leading++    -- Characters+    isNewline     c = c == '\n' || c == '\r'+    isDash        c = c == '-'  || c == '.'+    isInlineSpace c = c == '\t' || c == ' '+++--------------------------------------------------------------------------------+parseMetadata :: String -> Either Yaml.ParseException Metadata+parseMetadata = Yaml.decodeEither' . T.encodeUtf8 . T.pack+++--------------------------------------------------------------------------------+parsePage :: String -> Either Yaml.ParseException (Metadata, String)+parsePage fileContent = case mbMetaBlock of+    Nothing        -> return (mempty, content)+    Just metaBlock -> case parseMetadata metaBlock of+        Left  err  -> Left   err+        Right meta -> return (meta, content)+  where+    !(!mbMetaBlock, !content) = splitMetadata fileContent+++--------------------------------------------------------------------------------+-- | Thrown in the IO monad if things go wrong. Provides a nice-ish error+-- message.+data MetadataException = MetadataException FilePath Yaml.ParseException+++--------------------------------------------------------------------------------+instance Exception MetadataException+++--------------------------------------------------------------------------------+instance Show MetadataException where+    show (MetadataException fp err) =+        fp ++ ": " ++ Yaml.prettyPrintParseException err ++ hint++      where+        hint = case err of+            Yaml.InvalidYaml (Just (Yaml.YamlParseException {..}))+                | yamlProblem == problem -> "\n" +++                    "Hint: if the metadata value contains characters such\n" +++                    "as ':' or '-', try enclosing it in quotes."+            _ -> ""++        problem = "mapping values are not allowed in this context"
+ lib/Hakyll/Core/Provider/MetadataCache.hs view
@@ -0,0 +1,62 @@+--------------------------------------------------------------------------------+module Hakyll.Core.Provider.MetadataCache+    ( resourceMetadata+    , resourceBody+    , resourceInvalidateMetadataCache+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad                 (unless)+import           Hakyll.Core.Identifier+import           Hakyll.Core.Metadata+import           Hakyll.Core.Provider.Internal+import           Hakyll.Core.Provider.Metadata+import qualified Hakyll.Core.Store             as Store+++--------------------------------------------------------------------------------+resourceMetadata :: Provider -> Identifier -> IO Metadata+resourceMetadata p r+    | not (resourceExists p r) = return mempty+    | otherwise                = do+        -- TODO keep time in md cache+        load p r+        Store.Found (BinaryMetadata md) <- Store.get (providerStore p)+            [name, toFilePath r, "metadata"]+        return md+++--------------------------------------------------------------------------------+resourceBody :: Provider -> Identifier -> IO String+resourceBody p r = do+    load p r+    Store.Found bd <- Store.get (providerStore p)+        [name, toFilePath r, "body"]+    maybe (resourceString p r) return bd+++--------------------------------------------------------------------------------+resourceInvalidateMetadataCache :: Provider -> Identifier -> IO ()+resourceInvalidateMetadataCache p r = do+    Store.delete (providerStore p) [name, toFilePath r, "metadata"]+    Store.delete (providerStore p) [name, toFilePath r, "body"]+++--------------------------------------------------------------------------------+load :: Provider -> Identifier -> IO ()+load p r = do+    mmof <- Store.isMember store mdk+    unless mmof $ do+        (md, body) <- loadMetadata p r+        Store.set store mdk (BinaryMetadata md)+        Store.set store bk  body+  where+    store = providerStore p+    mdk   = [name, toFilePath r, "metadata"]+    bk    = [name, toFilePath r, "body"]+++--------------------------------------------------------------------------------+name :: String+name = "Hakyll.Core.Resource.Provider.MetadataCache"
+ lib/Hakyll/Core/Routes.hs view
@@ -0,0 +1,194 @@+--------------------------------------------------------------------------------+-- | Once a target is compiled, the user usually wants to save it to the disk.+-- This is where the 'Routes' type comes in; it determines where a certain+-- target should be written.+--+-- Suppose we have an item @foo\/bar.markdown@. We can render this to+-- @foo\/bar.html@ using:+--+-- > route "foo/bar.markdown" (setExtension ".html")+--+-- If we do not want to change the extension, we can use 'idRoute', the simplest+-- route available:+--+-- > route "foo/bar.markdown" idRoute+--+-- That will route @foo\/bar.markdown@ to @foo\/bar.markdown@.+--+-- Note that the extension says nothing about the content! If you set the+-- extension to @.html@, it is your own responsibility to ensure that the+-- content is indeed HTML.+--+-- Finally, some special cases:+--+-- * If there is no route for an item, this item will not be routed, so it will+--   not appear in your site directory.+--+-- * If an item matches multiple routes, the first rule will be chosen.+{-# LANGUAGE Rank2Types #-}+module Hakyll.Core.Routes+    ( UsedMetadata+    , Routes+    , runRoutes+    , idRoute+    , setExtension+    , matchRoute+    , customRoute+    , constRoute+    , gsubRoute+    , metadataRoute+    , composeRoutes+    ) where+++--------------------------------------------------------------------------------+import           System.FilePath                (replaceExtension)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Metadata+import           Hakyll.Core.Provider+import           Hakyll.Core.Util.String+++--------------------------------------------------------------------------------+-- | When you ran a route, it's useful to know whether or not this used+-- metadata. This allows us to do more granular dependency analysis.+type UsedMetadata = Bool+++--------------------------------------------------------------------------------+data RoutesRead = RoutesRead+    { routesProvider   :: Provider+    , routesUnderlying :: Identifier+    }+++--------------------------------------------------------------------------------+-- | Type used for a route+newtype Routes = Routes+    { unRoutes :: RoutesRead -> Identifier -> IO (Maybe FilePath, UsedMetadata)+    }+++--------------------------------------------------------------------------------+instance Monoid Routes where+    mempty = Routes $ \_ _ -> return (Nothing, False)+    mappend (Routes f) (Routes g) = Routes $ \p id' -> do+        (mfp, um) <- f p id'+        case mfp of+            Nothing -> g p id'+            Just _  -> return (mfp, um)+++--------------------------------------------------------------------------------+-- | Apply a route to an identifier+runRoutes :: Routes -> Provider -> Identifier+          -> IO (Maybe FilePath, UsedMetadata)+runRoutes routes provider identifier =+    unRoutes routes (RoutesRead provider identifier) identifier+++--------------------------------------------------------------------------------+-- | A route that uses the identifier as filepath. For example, the target with+-- ID @foo\/bar@ will be written to the file @foo\/bar@.+idRoute :: Routes+idRoute = customRoute toFilePath+++--------------------------------------------------------------------------------+-- | Set (or replace) the extension of a route.+--+-- Example:+--+-- > runRoutes (setExtension "html") "foo/bar"+--+-- Result:+--+-- > Just "foo/bar.html"+--+-- Example:+--+-- > runRoutes (setExtension "html") "posts/the-art-of-trolling.markdown"+--+-- Result:+--+-- > Just "posts/the-art-of-trolling.html"+setExtension :: String -> Routes+setExtension extension = customRoute $+    (`replaceExtension` extension) . toFilePath+++--------------------------------------------------------------------------------+-- | Apply the route if the identifier matches the given pattern, fail+-- otherwise+matchRoute :: Pattern -> Routes -> Routes+matchRoute pattern (Routes route) = Routes $ \p id' ->+    if matches pattern id' then route p id' else return (Nothing, False)+++--------------------------------------------------------------------------------+-- | Create a custom route. This should almost always be used with+-- 'matchRoute'+customRoute :: (Identifier -> FilePath) -> Routes+customRoute f = Routes $ const $ \id' -> return (Just (f id'), False)+++--------------------------------------------------------------------------------+-- | A route that always gives the same result. Obviously, you should only use+-- this for a single compilation rule.+constRoute :: FilePath -> Routes+constRoute = customRoute . const+++--------------------------------------------------------------------------------+-- | Create a gsub route+--+-- Example:+--+-- > runRoutes (gsubRoute "rss/" (const "")) "tags/rss/bar.xml"+--+-- Result:+--+-- > Just "tags/bar.xml"+gsubRoute :: String              -- ^ Pattern+          -> (String -> String)  -- ^ Replacement+          -> Routes              -- ^ Resulting route+gsubRoute pattern replacement = customRoute $+    replaceAll pattern replacement . toFilePath+++--------------------------------------------------------------------------------+-- | Get access to the metadata in order to determine the route+metadataRoute :: (Metadata -> Routes) -> Routes+metadataRoute f = Routes $ \r i -> do+    metadata <- resourceMetadata (routesProvider r) (routesUnderlying r)+    unRoutes (f metadata) r i+++--------------------------------------------------------------------------------+-- | Compose routes so that @f \`composeRoutes\` g@ is more or less equivalent+-- with @g . f@.+--+-- Example:+--+-- > let routes = gsubRoute "rss/" (const "") `composeRoutes` setExtension "xml"+-- > in runRoutes routes "tags/rss/bar"+--+-- Result:+--+-- > Just "tags/bar.xml"+--+-- If the first route given fails, Hakyll will not apply the second route.+composeRoutes :: Routes  -- ^ First route to apply+              -> Routes  -- ^ Second route to apply+              -> Routes  -- ^ Resulting route+composeRoutes (Routes f) (Routes g) = Routes $ \p i -> do+    (mfp, um) <- f p i+    case mfp of+        Nothing -> return (Nothing, um)+        Just fp -> do+            (mfp', um') <- g p (fromFilePath fp)+            return (mfp', um || um')
+ lib/Hakyll/Core/Rules.hs view
@@ -0,0 +1,223 @@+--------------------------------------------------------------------------------+-- | This module provides a declarative DSL in which the user can specify the+-- different rules used to run the compilers.+--+-- The convention is to just list all items in the 'Rules' monad, routes and+-- compilation rules.+--+-- A typical usage example would be:+--+-- > main = hakyll $ do+-- >     match "posts/*" $ do+-- >         route   (setExtension "html")+-- >         compile someCompiler+-- >     match "css/*" $ do+-- >         route   idRoute+-- >         compile compressCssCompiler+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+module Hakyll.Core.Rules+    ( Rules+    , match+    , matchMetadata+    , create+    , version+    , compile+    , route++      -- * Advanced usage+    , preprocess+    , Dependency (..)+    , rulesExtraDependencies+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad.Reader           (ask, local)+import           Control.Monad.State            (get, modify, put)+import           Control.Monad.Trans            (liftIO)+import           Control.Monad.Writer           (censor, tell)+import           Data.Maybe                     (fromMaybe)+import qualified Data.Set                       as S+++--------------------------------------------------------------------------------+import           Data.Binary                    (Binary)+import           Data.Typeable                  (Typeable)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Dependencies+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Item+import           Hakyll.Core.Item.SomeItem+import           Hakyll.Core.Metadata+import           Hakyll.Core.Routes+import           Hakyll.Core.Rules.Internal+import           Hakyll.Core.Writable+++--------------------------------------------------------------------------------+-- | Add a route+tellRoute :: Routes -> Rules ()+tellRoute route' = Rules $ tell $ RuleSet route' mempty mempty mempty+++--------------------------------------------------------------------------------+-- | Add a number of compilers+tellCompilers :: [(Identifier, Compiler SomeItem)] -> Rules ()+tellCompilers compilers = Rules $ tell $ RuleSet mempty compilers mempty mempty+++--------------------------------------------------------------------------------+-- | Add resources+tellResources :: [Identifier] -> Rules ()+tellResources resources' = Rules $ tell $+    RuleSet mempty mempty (S.fromList resources') mempty+++--------------------------------------------------------------------------------+-- | Add a pattern+tellPattern :: Pattern -> Rules ()+tellPattern pattern = Rules $ tell $ RuleSet mempty mempty mempty pattern+++--------------------------------------------------------------------------------+flush :: Rules ()+flush = Rules $ do+    mcompiler <- rulesCompiler <$> get+    case mcompiler of+        Nothing       -> return ()+        Just compiler -> do+            matches' <- rulesMatches                  <$> ask+            version' <- rulesVersion                  <$> ask+            route'   <- fromMaybe mempty . rulesRoute <$> get++            -- The version is possibly not set correctly at this point (yet)+            let ids = map (setVersion version') matches'++            {-+            ids      <- case fromLiteral pattern of+                Just id' -> return [setVersion version' id']+                Nothing  -> do+                    ids <- unRules $ getMatches pattern+                    unRules $ tellResources ids+                    return $ map (setVersion version') ids+            -}++            -- Create a fast pattern for routing that matches exactly the+            -- compilers created in the block given to match+            let fastPattern = fromList ids++            -- Write out the compilers and routes+            unRules $ tellRoute $ matchRoute fastPattern route'+            unRules $ tellCompilers $ [(id', compiler) | id' <- ids]++    put $ emptyRulesState+++--------------------------------------------------------------------------------+matchInternal :: Pattern -> Rules [Identifier] -> Rules () -> Rules ()+matchInternal pattern getIDs rules = do+    tellPattern pattern+    flush+    ids <- getIDs+    tellResources ids+    Rules $ local (setMatches ids) $ unRules $ rules >> flush+  where+    setMatches ids env = env {rulesMatches = ids}++--------------------------------------------------------------------------------+match :: Pattern -> Rules () -> Rules ()+match pattern = matchInternal pattern $ getMatches pattern+++--------------------------------------------------------------------------------+matchMetadata :: Pattern -> (Metadata -> Bool) -> Rules () -> Rules ()+matchMetadata pattern metadataPred = matchInternal pattern $+    map fst . filter (metadataPred . snd) <$> getAllMetadata pattern+++--------------------------------------------------------------------------------+create :: [Identifier] -> Rules () -> Rules ()+create ids rules = do+    flush+    -- TODO Maybe check if the resources exist and call tellResources on that+    Rules $ local setMatches $ unRules $ rules >> flush+  where+    setMatches env = env {rulesMatches = ids}+++--------------------------------------------------------------------------------+version :: String -> Rules () -> Rules ()+version v rules = do+    flush+    Rules $ local setVersion' $ unRules $ rules >> flush+  where+    setVersion' env = env {rulesVersion = Just v}+++--------------------------------------------------------------------------------+-- | Add a compilation rule to the rules.+--+-- This instructs all resources to be compiled using the given compiler.+compile :: (Binary a, Typeable a, Writable a) => Compiler (Item a) -> Rules ()+compile compiler = Rules $ modify $ \s ->+    s {rulesCompiler = Just (fmap SomeItem compiler)}+++--------------------------------------------------------------------------------+-- | Add a route.+--+-- This adds a route for all items matching the current pattern.+route :: Routes -> Rules ()+route route' = Rules $ modify $ \s -> s {rulesRoute = Just route'}+++--------------------------------------------------------------------------------+-- | Execute an 'IO' action immediately while the rules are being evaluated.+-- This should be avoided if possible, but occasionally comes in useful.+preprocess :: IO a -> Rules a+preprocess = Rules . liftIO+++--------------------------------------------------------------------------------+-- | Advanced usage: add extra dependencies to compilers. Basically this is+-- needed when you're doing unsafe tricky stuff in the rules monad, but you+-- still want correct builds.+--+-- A useful utility for this purpose is 'makePatternDependency'.+rulesExtraDependencies :: [Dependency] -> Rules a -> Rules a+rulesExtraDependencies deps rules =+    -- Note that we add the dependencies seemingly twice here. However, this is+    -- done so that 'rulesExtraDependencies' works both if we have something+    -- like:+    --+    -- > match "*.css" $ rulesExtraDependencies [foo] $ ...+    --+    -- and something like:+    --+    -- > rulesExtraDependencies [foo] $ match "*.css" $ ...+    --+    -- (1) takes care of the latter and (2) of the former.+    Rules $ censor fixRuleSet $ do+        x <- unRules rules+        fixCompiler+        return x+  where+    -- (1) Adds the dependencies to the compilers we are yet to create+    fixCompiler = modify $ \s -> case rulesCompiler s of+        Nothing -> s+        Just c  -> s+            { rulesCompiler = Just $ compilerTellDependencies deps >> c+            }++    -- (2) Adds the dependencies to the compilers that are already in the ruleset+    fixRuleSet ruleSet = ruleSet+        { rulesCompilers =+            [ (i, compilerTellDependencies deps >> c)+            | (i, c) <- rulesCompilers ruleSet+            ]+        }
+ lib/Hakyll/Core/Rules/Internal.hs view
@@ -0,0 +1,109 @@+--------------------------------------------------------------------------------+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE Rank2Types                 #-}+module Hakyll.Core.Rules.Internal+    ( RulesRead (..)+    , RuleSet (..)+    , RulesState (..)+    , emptyRulesState+    , Rules (..)+    , runRules+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad.Reader           (ask)+import           Control.Monad.RWS              (RWST, runRWST)+import           Control.Monad.Trans            (liftIO)+import qualified Data.Map                       as M+import           Data.Set                       (Set)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Item.SomeItem+import           Hakyll.Core.Metadata+import           Hakyll.Core.Provider+import           Hakyll.Core.Routes+++--------------------------------------------------------------------------------+data RulesRead = RulesRead+    { rulesProvider :: Provider+    , rulesMatches  :: [Identifier]+    , rulesVersion  :: Maybe String+    }+++--------------------------------------------------------------------------------+data RuleSet = RuleSet+    { -- | Accumulated routes+      rulesRoutes    :: Routes+    , -- | Accumulated compilers+      rulesCompilers :: [(Identifier, Compiler SomeItem)]+    , -- | A set of the actually used files+      rulesResources :: Set Identifier+    , -- | A pattern we can use to check if a file *would* be used. This is+      -- needed for the preview server.+      rulesPattern   :: Pattern+    }+++--------------------------------------------------------------------------------+instance Monoid RuleSet where+    mempty = RuleSet mempty mempty mempty mempty+    mappend (RuleSet r1 c1 s1 p1) (RuleSet r2 c2 s2 p2) =+        RuleSet (mappend r1 r2) (mappend c1 c2) (mappend s1 s2) (p1 .||. p2)+++--------------------------------------------------------------------------------+data RulesState = RulesState+    { rulesRoute    :: Maybe Routes+    , rulesCompiler :: Maybe (Compiler SomeItem)+    }+++--------------------------------------------------------------------------------+emptyRulesState :: RulesState+emptyRulesState = RulesState Nothing Nothing+++--------------------------------------------------------------------------------+-- | The monad used to compose rules+newtype Rules a = Rules+    { unRules :: RWST RulesRead RuleSet RulesState IO a+    } deriving (Monad, Functor, Applicative)+++--------------------------------------------------------------------------------+instance MonadMetadata Rules where+    getMetadata identifier = Rules $ do+        provider <- rulesProvider <$> ask+        liftIO $ resourceMetadata provider identifier++    getMatches pattern = Rules $ do+        provider <- rulesProvider <$> ask+        return $ filterMatches pattern $ resourceList provider+++--------------------------------------------------------------------------------+-- | Run a Rules monad, resulting in a 'RuleSet'+runRules :: Rules a -> Provider -> IO RuleSet+runRules rules provider = do+    (_, _, ruleSet) <- runRWST (unRules rules) env emptyRulesState++    -- Ensure compiler uniqueness+    let ruleSet' = ruleSet+            { rulesCompilers = M.toList $+                M.fromListWith (flip const) (rulesCompilers ruleSet)+            }++    return ruleSet'+  where+    env = RulesRead+        { rulesProvider = provider+        , rulesMatches  = []+        , rulesVersion  = Nothing+        }
+ lib/Hakyll/Core/Runtime.hs view
@@ -0,0 +1,276 @@+--------------------------------------------------------------------------------+module Hakyll.Core.Runtime+    ( run+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad                 (unless)+import           Control.Monad.Except          (ExceptT, runExceptT, throwError)+import           Control.Monad.Reader          (ask)+import           Control.Monad.RWS             (RWST, runRWST)+import           Control.Monad.State           (get, modify)+import           Control.Monad.Trans           (liftIO)+import           Data.List                     (intercalate)+import           Data.Map                      (Map)+import qualified Data.Map                      as M+import           Data.Set                      (Set)+import qualified Data.Set                      as S+import           System.Exit                   (ExitCode (..))+import           System.FilePath               ((</>))+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Compiler.Require+import           Hakyll.Core.Configuration+import           Hakyll.Core.Dependencies+import           Hakyll.Core.Identifier+import           Hakyll.Core.Item+import           Hakyll.Core.Item.SomeItem+import           Hakyll.Core.Logger            (Logger)+import qualified Hakyll.Core.Logger            as Logger+import           Hakyll.Core.Provider+import           Hakyll.Core.Routes+import           Hakyll.Core.Rules.Internal+import           Hakyll.Core.Store             (Store)+import qualified Hakyll.Core.Store             as Store+import           Hakyll.Core.Util.File+import           Hakyll.Core.Writable+++--------------------------------------------------------------------------------+run :: Configuration -> Logger -> Rules a -> IO (ExitCode, RuleSet)+run config logger rules = do+    -- Initialization+    Logger.header logger "Initialising..."+    Logger.message logger "Creating store..."+    store <- Store.new (inMemoryCache config) $ storeDirectory config+    Logger.message logger "Creating provider..."+    provider <- newProvider store (shouldIgnoreFile config) $+        providerDirectory config+    Logger.message logger "Running rules..."+    ruleSet  <- runRules rules provider++    -- Get old facts+    mOldFacts <- Store.get store factsKey+    let (oldFacts) = case mOldFacts of Store.Found f -> f+                                       _             -> mempty++    -- Build runtime read/state+    let compilers = rulesCompilers ruleSet+        read'     = RuntimeRead+            { runtimeConfiguration = config+            , runtimeLogger        = logger+            , runtimeProvider      = provider+            , runtimeStore         = store+            , runtimeRoutes        = rulesRoutes ruleSet+            , runtimeUniverse      = M.fromList compilers+            }+        state     = RuntimeState+            { runtimeDone      = S.empty+            , runtimeSnapshots = S.empty+            , runtimeTodo      = M.empty+            , runtimeFacts     = oldFacts+            }++    -- Run the program and fetch the resulting state+    result <- runExceptT $ runRWST build read' state+    case result of+        Left e          -> do+            Logger.error logger e+            Logger.flush logger+            return (ExitFailure 1, ruleSet)++        Right (_, s, _) -> do+            Store.set store factsKey $ runtimeFacts s++            Logger.debug logger "Removing tmp directory..."+            removeDirectory $ tmpDirectory config++            Logger.flush logger+            return (ExitSuccess, ruleSet)+  where+    factsKey = ["Hakyll.Core.Runtime.run", "facts"]+++--------------------------------------------------------------------------------+data RuntimeRead = RuntimeRead+    { runtimeConfiguration :: Configuration+    , runtimeLogger        :: Logger+    , runtimeProvider      :: Provider+    , runtimeStore         :: Store+    , runtimeRoutes        :: Routes+    , runtimeUniverse      :: Map Identifier (Compiler SomeItem)+    }+++--------------------------------------------------------------------------------+data RuntimeState = RuntimeState+    { runtimeDone      :: Set Identifier+    , runtimeSnapshots :: Set (Identifier, Snapshot)+    , runtimeTodo      :: Map Identifier (Compiler SomeItem)+    , runtimeFacts     :: DependencyFacts+    }+++--------------------------------------------------------------------------------+type Runtime a = RWST RuntimeRead () RuntimeState (ExceptT String IO) a+++--------------------------------------------------------------------------------+build :: Runtime ()+build = do+    logger <- runtimeLogger <$> ask+    Logger.header logger "Checking for out-of-date items"+    scheduleOutOfDate+    Logger.header logger "Compiling"+    pickAndChase+    Logger.header logger "Success"+++--------------------------------------------------------------------------------+scheduleOutOfDate :: Runtime ()+scheduleOutOfDate = do+    logger   <- runtimeLogger   <$> ask+    provider <- runtimeProvider <$> ask+    universe <- runtimeUniverse <$> ask+    facts    <- runtimeFacts    <$> get+    todo     <- runtimeTodo     <$> get++    let identifiers = M.keys universe+        modified    = S.fromList $ flip filter identifiers $+            resourceModified provider++    let (ood, facts', msgs) = outOfDate identifiers modified facts+        todo'               = M.filterWithKey+            (\id' _ -> id' `S.member` ood) universe++    -- Print messages+    mapM_ (Logger.debug logger) msgs++    -- Update facts and todo items+    modify $ \s -> s+        { runtimeDone  = runtimeDone s `S.union`+            (S.fromList identifiers `S.difference` ood)+        , runtimeTodo  = todo `M.union` todo'+        , runtimeFacts = facts'+        }+++--------------------------------------------------------------------------------+pickAndChase :: Runtime ()+pickAndChase = do+    todo <- runtimeTodo <$> get+    case M.minViewWithKey todo of+        Nothing            -> return ()+        Just ((id', _), _) -> do+            chase [] id'+            pickAndChase+++--------------------------------------------------------------------------------+chase :: [Identifier] -> Identifier -> Runtime ()+chase trail id'+    | id' `elem` trail = throwError $ "Hakyll.Core.Runtime.chase: " +++        "Dependency cycle detected: " ++ intercalate " depends on "+            (map show $ dropWhile (/= id') (reverse trail) ++ [id'])+    | otherwise        = do+        logger   <- runtimeLogger        <$> ask+        todo     <- runtimeTodo          <$> get+        provider <- runtimeProvider      <$> ask+        universe <- runtimeUniverse      <$> ask+        routes   <- runtimeRoutes        <$> ask+        store    <- runtimeStore         <$> ask+        config   <- runtimeConfiguration <$> ask+        Logger.debug logger $ "Processing " ++ show id'++        let compiler = todo M.! id'+            read' = CompilerRead+                { compilerConfig     = config+                , compilerUnderlying = id'+                , compilerProvider   = provider+                , compilerUniverse   = M.keysSet universe+                , compilerRoutes     = routes+                , compilerStore      = store+                , compilerLogger     = logger+                }++        result <- liftIO $ runCompiler compiler read'+        case result of+            -- Rethrow error+            CompilerError [] -> throwError+                "Compiler failed but no info given, try running with -v?"+            CompilerError es -> throwError $ intercalate "; " es++            -- Signal that a snapshot was saved ->+            CompilerSnapshot snapshot c -> do+                -- Update info. The next 'chase' will pick us again at some+                -- point so we can continue then.+                modify $ \s -> s+                    { runtimeSnapshots =+                        S.insert (id', snapshot) (runtimeSnapshots s)+                    , runtimeTodo      = M.insert id' c (runtimeTodo s)+                    }++            -- Huge success+            CompilerDone (SomeItem item) cwrite -> do+                -- Print some info+                let facts = compilerDependencies cwrite+                    cacheHits+                        | compilerCacheHits cwrite <= 0 = "updated"+                        | otherwise                     = "cached "+                Logger.message logger $ cacheHits ++ " " ++ show id'++                -- Sanity check+                unless (itemIdentifier item == id') $ throwError $+                    "The compiler yielded an Item with Identifier " +++                    show (itemIdentifier item) ++ ", but we were expecting " +++                    "an Item with Identifier " ++ show id' ++ " " +++                    "(you probably want to call makeItem to solve this problem)"++                -- Write if necessary+                (mroute, _) <- liftIO $ runRoutes routes provider id'+                case mroute of+                    Nothing    -> return ()+                    Just route -> do+                        let path = destinationDirectory config </> route+                        liftIO $ makeDirectories path+                        liftIO $ write path item+                        Logger.debug logger $ "Routed to " ++ path++                -- Save! (For load)+                liftIO $ save store item++                -- Update state+                modify $ \s -> s+                    { runtimeDone  = S.insert id' (runtimeDone s)+                    , runtimeTodo  = M.delete id' (runtimeTodo s)+                    , runtimeFacts = M.insert id' facts (runtimeFacts s)+                    }++            -- Try something else first+            CompilerRequire dep c -> do+                -- Update the compiler so we don't execute it twice+                let (depId, depSnapshot) = dep+                done      <- runtimeDone <$> get+                snapshots <- runtimeSnapshots <$> get++                -- Done if we either completed the entire item (runtimeDone) or+                -- if we previously saved the snapshot (runtimeSnapshots).+                let depDone =+                        depId `S.member` done ||+                        (depId, depSnapshot) `S.member` snapshots++                modify $ \s -> s+                    { runtimeTodo = M.insert id'+                        (if depDone then c else compilerResult result)+                        (runtimeTodo s)+                    }++                -- If the required item is already compiled, continue, or, start+                -- chasing that+                Logger.debug logger $ "Require " ++ show depId +++                    " (snapshot " ++ depSnapshot ++ "): " +++                    (if depDone then "OK" else "chasing")+                if depDone then chase trail id' else chase (id' : trail) depId
+ lib/Hakyll/Core/Store.hs view
@@ -0,0 +1,197 @@+--------------------------------------------------------------------------------+-- | A store for storing and retreiving items+{-# LANGUAGE ExistentialQuantification #-}+{-# LANGUAGE ScopedTypeVariables       #-}+module Hakyll.Core.Store+    ( Store+    , Result (..)+    , toMaybe+    , new+    , set+    , get+    , isMember+    , delete+    , hash+    ) where+++--------------------------------------------------------------------------------+import           Control.Exception    (IOException, handle)+import qualified Crypto.Hash.MD5      as MD5+import           Data.Binary          (Binary, decode, encodeFile)+import qualified Data.ByteString      as B+import qualified Data.ByteString.Lazy as BL+import qualified Data.Cache.LRU.IO    as Lru+import           Data.List            (intercalate)+import           Data.Maybe           (isJust)+import qualified Data.Text            as T+import qualified Data.Text.Encoding   as T+import           Data.Typeable        (TypeRep, Typeable, cast, typeOf)+import           System.Directory     (createDirectoryIfMissing)+import           System.Directory     (doesFileExist, removeFile)+import           System.FilePath      ((</>))+import           System.IO            (IOMode (..), hClose, openFile)+import           Text.Printf          (printf)+++--------------------------------------------------------------------------------+-- | Simple wrapper type+data Box = forall a. Typeable a => Box a+++--------------------------------------------------------------------------------+data Store = Store+    { -- | All items are stored on the filesystem+      storeDirectory :: FilePath+    , -- | Optionally, items are also kept in-memory+      storeMap       :: Maybe (Lru.AtomicLRU FilePath Box)+    }+++--------------------------------------------------------------------------------+instance Show Store where+    show _ = "<Store>"+++--------------------------------------------------------------------------------+-- | Result of a store query+data Result a+    = Found a                    -- ^ Found, result+    | NotFound                   -- ^ Not found+    | WrongType TypeRep TypeRep  -- ^ Expected, true type+    deriving (Show, Eq)+++--------------------------------------------------------------------------------+-- | Convert result to 'Maybe'+toMaybe :: Result a -> Maybe a+toMaybe (Found x) = Just x+toMaybe _         = Nothing+++--------------------------------------------------------------------------------+-- | Initialize the store+new :: Bool      -- ^ Use in-memory caching+    -> FilePath  -- ^ Directory to use for hard disk storage+    -> IO Store  -- ^ Store+new inMemory directory = do+    createDirectoryIfMissing True directory+    ref <- if inMemory then Just <$> Lru.newAtomicLRU csize else return Nothing+    return Store+        { storeDirectory = directory+        , storeMap       = ref+        }+  where+    csize = Just 500+++--------------------------------------------------------------------------------+-- | Auxiliary: add an item to the in-memory cache+cacheInsert :: Typeable a => Store -> String -> a -> IO ()+cacheInsert (Store _ Nothing)    _   _     = return ()+cacheInsert (Store _ (Just lru)) key x =+    Lru.insert key (Box x) lru+++--------------------------------------------------------------------------------+-- | Auxiliary: get an item from the in-memory cache+cacheLookup :: forall a. Typeable a => Store -> String -> IO (Result a)+cacheLookup (Store _ Nothing)    _   = return NotFound+cacheLookup (Store _ (Just lru)) key = do+    res <- Lru.lookup key lru+    return $ case res of+        Nothing      -> NotFound+        Just (Box x) -> case cast x of+            Just x' -> Found x'+            Nothing -> WrongType (typeOf (undefined :: a)) (typeOf x)+++--------------------------------------------------------------------------------+cacheIsMember :: Store -> String -> IO Bool+cacheIsMember (Store _ Nothing)    _   = return False+cacheIsMember (Store _ (Just lru)) key = isJust <$> Lru.lookup key lru+++--------------------------------------------------------------------------------+-- | Auxiliary: delete an item from the in-memory cache+cacheDelete :: Store -> String -> IO ()+cacheDelete (Store _ Nothing)    _   = return ()+cacheDelete (Store _ (Just lru)) key = do+    _ <- Lru.delete key lru+    return ()+++--------------------------------------------------------------------------------+-- | Store an item+set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()+set store identifier value = do+    encodeFile (storeDirectory store </> key) value+    cacheInsert store key value+  where+    key = hash identifier+++--------------------------------------------------------------------------------+-- | Load an item+get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)+get store identifier = do+    -- First check the in-memory map+    ref <- cacheLookup store key+    case ref of+        -- Not found in the map, try the filesystem+        NotFound -> do+            exists <- doesFileExist path+            if not exists+                -- Not found in the filesystem either+                then return NotFound+                -- Found in the filesystem+                else do+                    v <- decodeClose+                    cacheInsert store key v+                    return $ Found v+        -- Found in the in-memory map (or wrong type), just return+        s -> return s+  where+    key  = hash identifier+    path = storeDirectory store </> key++    -- 'decodeFile' from Data.Binary which closes the file ASAP+    decodeClose = do+        h   <- openFile path ReadMode+        lbs <- BL.hGetContents h+        BL.length lbs `seq` hClose h+        return $ decode lbs+++--------------------------------------------------------------------------------+-- | Strict function+isMember :: Store -> [String] -> IO Bool+isMember store identifier = do+    inCache <- cacheIsMember store key+    if inCache then return True else doesFileExist path+  where+    key  = hash identifier+    path = storeDirectory store </> key+++--------------------------------------------------------------------------------+-- | Delete an item+delete :: Store -> [String] -> IO ()+delete store identifier = do+    cacheDelete store key+    deleteFile $ storeDirectory store </> key+  where+    key  = hash identifier+++--------------------------------------------------------------------------------+-- | Delete a file unless it doesn't exist...+deleteFile :: FilePath -> IO ()+deleteFile = handle (\(_ :: IOException) -> return ()) . removeFile+++--------------------------------------------------------------------------------+-- | Mostly meant for internal usage+hash :: [String] -> String+hash = concatMap (printf "%02x") . B.unpack .+    MD5.hash . T.encodeUtf8 . T.pack . intercalate "/"
+ lib/Hakyll/Core/UnixFilter.hs view
@@ -0,0 +1,159 @@+{-# LANGUAGE CPP #-}++--------------------------------------------------------------------------------+-- | A Compiler that supports unix filters.+module Hakyll.Core.UnixFilter+    ( unixFilter+    , unixFilterLBS+    ) where+++--------------------------------------------------------------------------------+import           Control.Concurrent      (forkIO)+import           Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)+import           Control.DeepSeq         (deepseq)+import           Control.Monad           (forM_)+import           Data.ByteString.Lazy    (ByteString)+import qualified Data.ByteString.Lazy    as LB+import           Data.IORef              (newIORef, readIORef, writeIORef)+import           System.Exit             (ExitCode (..))+import           System.IO               (Handle, hClose, hFlush, hGetContents,+                                          hPutStr, hSetEncoding, localeEncoding)+import           System.Process++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+++--------------------------------------------------------------------------------+-- | Use a unix filter as compiler. For example, we could use the 'rev' program+-- as a compiler.+--+-- > rev :: Compiler (Item String)+-- > rev = getResourceString >>= withItemBody (unixFilter "rev" [])+--+-- A more realistic example: one can use this to call, for example, the sass+-- compiler on CSS files. More information about sass can be found here:+--+-- <http://sass-lang.com/>+--+-- The code is fairly straightforward, given that we use @.scss@ for sass:+--+-- > match "style.scss" $ do+-- >     route   $ setExtension "css"+-- >     compile $ getResourceString >>=+-- >         withItemBody (unixFilter "sass" ["-s", "--scss"]) >>=+-- >         return . fmap compressCss+unixFilter :: String           -- ^ Program name+           -> [String]         -- ^ Program args+           -> String           -- ^ Program input+           -> Compiler String  -- ^ Program output+unixFilter = unixFilterWith writer reader+  where+    writer handle input = do+        hSetEncoding handle localeEncoding+        hPutStr handle input+    reader handle = do+        hSetEncoding handle localeEncoding+        out <- hGetContents handle+        deepseq out (return out)+++--------------------------------------------------------------------------------+-- | Variant of 'unixFilter' that should be used for binary files+--+-- > match "music.wav" $ do+-- >     route   $ setExtension "ogg"+-- >     compile $ getResourceLBS >>= withItemBody (unixFilterLBS "oggenc" ["-"])+unixFilterLBS :: String               -- ^ Program name+              -> [String]             -- ^ Program args+              -> ByteString           -- ^ Program input+              -> Compiler ByteString  -- ^ Program output+unixFilterLBS = unixFilterWith LB.hPutStr $ \handle -> do+    out <- LB.hGetContents handle+    LB.length out `seq` return out+++--------------------------------------------------------------------------------+-- | Overloaded compiler+unixFilterWith :: Monoid o+               => (Handle -> i -> IO ())  -- ^ Writer+               -> (Handle -> IO o)        -- ^ Reader+               -> String                  -- ^ Program name+               -> [String]                -- ^ Program args+               -> i                       -- ^ Program input+               -> Compiler o              -- ^ Program output+unixFilterWith writer reader programName args input = do+    debugCompiler ("Executing external program " ++ programName)+    (output, err, exitCode) <- unsafeCompiler $+        unixFilterIO writer reader programName args input+    forM_ (lines err) debugCompiler+    case exitCode of+        ExitSuccess   -> return output+        ExitFailure e -> fail $+            "Hakyll.Core.UnixFilter.unixFilterWith: " +++            unwords (programName : args) ++ " gave exit code " ++ show e+++--------------------------------------------------------------------------------+-- | Internally used function+unixFilterIO :: Monoid o+             => (Handle -> i -> IO ())+             -> (Handle -> IO o)+             -> String+             -> [String]+             -> i+             -> IO (o, String, ExitCode)+unixFilterIO writer reader programName args input = do+    -- The problem on Windows is that `proc` is unable to execute+    -- batch stubs (eg. anything created using 'gem install ...') even if its in+    -- `$PATH`. A solution to this issue is to execute the batch file explicitly+    -- using `cmd /c batchfile` but there is no rational way to know where said+    -- batchfile is on the system. Hence, we detect windows using the+    -- CPP and instead of using `proc` to create the process, use `shell`+    -- which will be able to execute everything `proc` can+    -- as well as batch files.+#ifdef mingw32_HOST_OS+    let pr = shell $ unwords (programName : args)+#else+    let pr = proc programName args+#endif++    (Just inh, Just outh, Just errh, pid) <-+        createProcess pr+                { std_in  = CreatePipe+                , std_out = CreatePipe+                , std_err = CreatePipe+                }++    -- Create boxes+    lock   <- newEmptyMVar+    outRef <- newIORef mempty+    errRef <- newIORef ""++    -- Write the input to the child pipe+    _ <- forkIO $ writer inh input >> hFlush inh >> hClose inh++    -- Read from stdout+    _ <- forkIO $ do+        out <- reader outh+        hClose outh+        writeIORef outRef out+        putMVar lock ()++    -- Read from stderr+    _ <- forkIO $ do+        hSetEncoding errh localeEncoding+        err <- hGetContents errh+        _   <- deepseq err (return err)+        hClose errh+        writeIORef errRef err+        putMVar lock ()++    -- Get exit code & return+    takeMVar lock+    takeMVar lock+    exitCode <- waitForProcess pid+    out      <- readIORef outRef+    err      <- readIORef errRef+    return (out, err, exitCode)
+ lib/Hakyll/Core/Util/File.hs view
@@ -0,0 +1,56 @@+--------------------------------------------------------------------------------+-- | A module containing various file utility functions+module Hakyll.Core.Util.File+    ( makeDirectories+    , getRecursiveContents+    , removeDirectory+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad       (filterM, forM, when)+import           System.Directory    (createDirectoryIfMissing,+                                      doesDirectoryExist, getDirectoryContents,+                                      removeDirectoryRecursive)+import           System.FilePath     (takeDirectory, (</>))+++--------------------------------------------------------------------------------+-- | Given a path to a file, try to make the path writable by making+--   all directories on the path.+makeDirectories :: FilePath -> IO ()+makeDirectories = createDirectoryIfMissing True . takeDirectory+++--------------------------------------------------------------------------------+-- | Get all contents of a directory.+getRecursiveContents :: (FilePath -> IO Bool)  -- ^ Ignore this file/directory+                     -> FilePath               -- ^ Directory to search+                     -> IO [FilePath]          -- ^ List of files found+getRecursiveContents ignore top = go ""+  where+    isProper x+        | x `elem` [".", ".."] = return False+        | otherwise            = not <$> ignore x++    go dir     = do+        dirExists <- doesDirectoryExist (top </> dir)+        if not dirExists+            then return []+            else do+                names <- filterM isProper =<< getDirectoryContents (top </> dir)+                paths <- forM names $ \name -> do+                    let rel = dir </> name+                    isDirectory <- doesDirectoryExist (top </> rel)+                    if isDirectory+                        then go rel+                        else return [rel]++                return $ concat paths+++--------------------------------------------------------------------------------+removeDirectory :: FilePath -> IO ()+removeDirectory fp = do+    e <- doesDirectoryExist fp+    when e $ removeDirectoryRecursive fp
+ lib/Hakyll/Core/Util/Parser.hs view
@@ -0,0 +1,32 @@+--------------------------------------------------------------------------------+-- | Parser utilities+module Hakyll.Core.Util.Parser+    ( metadataKey+    , reservedKeys+    ) where+++--------------------------------------------------------------------------------+import           Control.Applicative ((<|>))+import           Control.Monad       (guard, mzero, void)+import qualified Text.Parsec         as P+import           Text.Parsec.String  (Parser)+++--------------------------------------------------------------------------------+metadataKey :: Parser String+metadataKey = do+    -- Ensure trailing '-' binds to '$' if present.+    let hyphon = P.try $ do+            void $ P.char '-'+            x <- P.lookAhead P.anyChar+            guard $ x /= '$'+            pure '-'++    i <- (:) <$> P.letter <*> P.many (P.alphaNum <|> P.oneOf "_." <|> hyphon)+    if i `elem` reservedKeys then mzero else return i+++--------------------------------------------------------------------------------+reservedKeys :: [String]+reservedKeys = ["if", "else", "endif", "for", "sep", "endfor", "partial"]
+ lib/Hakyll/Core/Util/String.hs view
@@ -0,0 +1,78 @@+{-# LANGUAGE FlexibleContexts #-}+--------------------------------------------------------------------------------+-- | Miscellaneous string manipulation functions.+module Hakyll.Core.Util.String+    ( trim+    , replaceAll+    , splitAll+    , needlePrefix+    ) where+++--------------------------------------------------------------------------------+import Data.Char (isSpace)+import Data.List (isPrefixOf)+import Data.Maybe (listToMaybe)+import Text.Regex.TDFA ((=~~))+++--------------------------------------------------------------------------------+-- | Trim a string (drop spaces, tabs and newlines at both sides).+trim :: String -> String+trim = reverse . trim' . reverse . trim'+  where+    trim' = dropWhile isSpace+++--------------------------------------------------------------------------------+-- | A simple (but inefficient) regex replace funcion+replaceAll :: String              -- ^ Pattern+           -> (String -> String)  -- ^ Replacement (called on capture)+           -> String              -- ^ Source string+           -> String              -- ^ Result+replaceAll pattern f source = replaceAll' source+  where+    replaceAll' src = case listToMaybe (src =~~ pattern) of+        Nothing     -> src+        Just (o, l) ->+            let (before, tmp) = splitAt o src+                (capture, after) = splitAt l tmp+            in before ++ f capture ++ replaceAll' after+++--------------------------------------------------------------------------------+-- | A simple regex split function. The resulting list will contain no empty+-- strings.+splitAll :: String    -- ^ Pattern+         -> String    -- ^ String to split+         -> [String]  -- ^ Result+splitAll pattern = filter (not . null) . splitAll'+  where+    splitAll' src = case listToMaybe (src =~~ pattern) of+        Nothing     -> [src]+        Just (o, l) ->+            let (before, tmp) = splitAt o src+            in before : splitAll' (drop l tmp)++++--------------------------------------------------------------------------------+-- | Find the first instance of needle (must be non-empty) in haystack. We+-- return the prefix of haystack before needle is matched.+--+-- Examples:+--+-- > needlePrefix "cd" "abcde" = "ab"+--+-- > needlePrefix "ab" "abc" = ""+--+-- > needlePrefix "ab" "xxab" = "xx"+--+-- > needlePrefix "a" "xx" = "xx"+needlePrefix :: String -> String -> Maybe String+needlePrefix needle haystack = go [] haystack+  where+    go _   []                     = Nothing+    go acc xss@(x:xs)+        | needle `isPrefixOf` xss = Just $ reverse acc+        | otherwise               = go (x : acc) xs
+ lib/Hakyll/Core/Writable.hs view
@@ -0,0 +1,56 @@+--------------------------------------------------------------------------------+-- | Describes writable items; items that can be saved to the disk+{-# LANGUAGE FlexibleInstances    #-}+{-# LANGUAGE TypeSynonymInstances #-}+module Hakyll.Core.Writable+    ( Writable (..)+    ) where+++--------------------------------------------------------------------------------+import qualified Data.ByteString                 as SB+import qualified Data.ByteString.Lazy            as LB+import           Data.Word                       (Word8)+import           Text.Blaze.Html                 (Html)+import           Text.Blaze.Html.Renderer.String (renderHtml)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Item+++--------------------------------------------------------------------------------+-- | Describes an item that can be saved to the disk+class Writable a where+    -- | Save an item to the given filepath+    write :: FilePath -> Item a -> IO ()+++--------------------------------------------------------------------------------+instance Writable () where+    write _ _ = return ()+++--------------------------------------------------------------------------------+instance Writable [Char] where+    write p = writeFile p . itemBody+++--------------------------------------------------------------------------------+instance Writable SB.ByteString where+    write p = SB.writeFile p . itemBody+++--------------------------------------------------------------------------------+instance Writable LB.ByteString where+    write p = LB.writeFile p . itemBody+++--------------------------------------------------------------------------------+instance Writable [Word8] where+    write p = write p . fmap SB.pack+++--------------------------------------------------------------------------------+instance Writable Html where+    write p = write p . fmap renderHtml
+ lib/Hakyll/Main.hs view
@@ -0,0 +1,165 @@+--------------------------------------------------------------------------------+-- | Module providing the main hakyll function and command-line argument parsing+{-# LANGUAGE CPP #-}++module Hakyll.Main+    ( hakyll+    , hakyllWith+    , hakyllWithArgs+    , hakyllWithExitCode+    ) where+++--------------------------------------------------------------------------------+import           System.Environment        (getProgName)+import           System.Exit               (ExitCode (ExitSuccess), exitWith)+import           System.IO.Unsafe          (unsafePerformIO)+++--------------------------------------------------------------------------------+import           Data.Monoid               ((<>))+import qualified Options.Applicative       as OA+++--------------------------------------------------------------------------------+import qualified Hakyll.Check              as Check+import qualified Hakyll.Commands           as Commands+import qualified Hakyll.Core.Configuration as Config+import qualified Hakyll.Core.Logger        as Logger+import           Hakyll.Core.Rules+++--------------------------------------------------------------------------------+-- | This usually is the function with which the user runs the hakyll compiler+hakyll :: Rules a -> IO ()+hakyll = hakyllWith Config.defaultConfiguration++--------------------------------------------------------------------------------+-- | A variant of 'hakyll' which allows the user to specify a custom+-- configuration+hakyllWith :: Config.Configuration -> Rules a -> IO ()+hakyllWith conf rules = hakyllWithExitCode conf rules >>= exitWith++--------------------------------------------------------------------------------+-- | A variant of 'hakyll' which returns an 'ExitCode'+hakyllWithExitCode :: Config.Configuration -> Rules a -> IO ExitCode+hakyllWithExitCode conf rules =  do+    args <- defaultParser conf+    hakyllWithExitCodeAndArgs conf args rules++--------------------------------------------------------------------------------+-- | A variant of 'hakyll' which expects a 'Configuration' and command-line+-- 'Options'. This gives freedom to implement your own parsing.+hakyllWithArgs :: Config.Configuration -> Options -> Rules a -> IO ()+hakyllWithArgs conf args rules =+    hakyllWithExitCodeAndArgs conf args rules >>= exitWith++--------------------------------------------------------------------------------+hakyllWithExitCodeAndArgs :: Config.Configuration ->+                              Options -> Rules a -> IO ExitCode+hakyllWithExitCodeAndArgs conf args rules = do+    let args' = optCommand args+        verbosity' = if verbosity args then Logger.Debug else Logger.Message+        check     =+            if internal_links args' then Check.InternalLinks else Check.All++    logger <- Logger.new verbosity'+    invokeCommands args' conf check logger rules++--------------------------------------------------------------------------------+defaultParser :: Config.Configuration -> IO Options+defaultParser conf =+    OA.customExecParser (OA.prefs OA.showHelpOnError)+        (OA.info (OA.helper <*> optionParser conf)+        (OA.fullDesc <> OA.progDesc+        (progName ++ " - Static site compiler created with Hakyll")))+++--------------------------------------------------------------------------------+invokeCommands :: Command -> Config.Configuration ->+                  Check.Check -> Logger.Logger -> Rules a -> IO ExitCode+invokeCommands args conf check logger rules =+    case args of+        Build          -> Commands.build conf logger rules+        Check   _      -> Commands.check conf logger check >> ok+        Clean          -> Commands.clean conf logger >> ok+        Deploy         -> Commands.deploy conf+        Preview p      -> Commands.preview conf logger rules p >> ok+        Rebuild        -> Commands.rebuild conf logger rules+        Server  _ _    -> Commands.server conf logger (host args) (port args) >> ok+        Watch   _ p s  -> Commands.watch conf logger (host args) p (not s) rules >> ok+    where+        ok = return ExitSuccess+++--------------------------------------------------------------------------------++data Options = Options {verbosity :: Bool, optCommand :: Command}+    deriving (Show)++data Command+    = Build+    | Check   {internal_links :: Bool}+    | Clean+    | Deploy+    | Preview {port :: Int}+    | Rebuild+    | Server  {host :: String, port :: Int}+    | Watch   {host :: String, port :: Int, no_server :: Bool }+    deriving (Show)++optionParser :: Config.Configuration -> OA.Parser Options+optionParser conf = Options <$> verboseParser <*> commandParser conf+    where+    verboseParser = OA.switch (OA.long "verbose" <> OA.short 'v' <> OA.help "Run in verbose mode")+++commandParser :: Config.Configuration -> OA.Parser Command+commandParser conf = OA.subparser $ foldr ((<>) . produceCommand) mempty commands+    where+    portParser = OA.option OA.auto (OA.long "port" <> OA.help "Port to listen on" <> OA.value (Config.previewPort conf))+    hostParser = OA.strOption (OA.long "host" <> OA.help "Host to bind on" <> OA.value (Config.previewHost conf))++    produceCommand (c,a,b) = OA.command c (OA.info (OA.helper <*> a) (b))++    commands =+        [ ( "build"+          , pure Build+          , OA.fullDesc <> OA.progDesc "Generate the site"+          )+        , ( "check"+          , pure Check <*> OA.switch (OA.long "internal-links" <> OA.help "Check internal links only")+          , OA.fullDesc <> OA.progDesc "Validate the site output"+          )+        , ( "clean"+          , pure Clean+          , OA.fullDesc <> OA.progDesc "Clean up and remove cache"+          )+        , ( "deploy"+          , pure Deploy+          , OA.fullDesc <> OA.progDesc "Upload/deploy your site"+           )+        , ( "preview"+          , pure Preview <*> portParser+          , OA.fullDesc <> OA.progDesc "[DEPRECATED] Please use the watch command"+          )+        , ( "rebuild"+          , pure Rebuild+          , OA.fullDesc <> OA.progDesc "Clean and build again"+          )+        , ( "server"+          , pure Server <*> hostParser <*> portParser+          , OA.fullDesc <> OA.progDesc "Start a preview server"+          )+        , ( "watch"+          , pure Watch <*> hostParser <*> portParser <*> OA.switch (OA.long "no-server" <> OA.help "Disable the built-in web server")+          , OA.fullDesc <> OA.progDesc "Autocompile on changes and start a preview server.  You can watch and recompile without running a server with --no-server."+          )+        ]+++--------------------------------------------------------------------------------+-- | This is necessary because not everyone calls their program the same...+progName :: String+progName = unsafePerformIO getProgName+{-# NOINLINE progName #-}
+ lib/Hakyll/Preview/Poll.hs view
@@ -0,0 +1,119 @@+--------------------------------------------------------------------------------+{-# LANGUAGE CPP #-}+module Hakyll.Preview.Poll+    ( watchUpdates+    ) where+++--------------------------------------------------------------------------------+import           Control.Concurrent             (forkIO)+import           Control.Concurrent.MVar        (newEmptyMVar, takeMVar,+                                                 tryPutMVar)+import           Control.Exception              (AsyncException, fromException,+                                                 handle, throw)+import           Control.Monad                  (forever, void, when)+import           System.Directory               (canonicalizePath)+import           System.FilePath                (pathSeparators)+import           System.FSNotify                (Event (..), startManager,+                                                 watchTree)++#ifdef mingw32_HOST_OS+import           Control.Concurrent             (threadDelay)+import           Control.Exception              (IOException, throw, try)+import           System.Directory               (doesFileExist)+import           System.Exit                    (exitFailure)+import           System.FilePath                ((</>))+import           System.IO                      (Handle, IOMode (ReadMode),+                                                 hClose, openFile)+import           System.IO.Error                (isPermissionError)+#endif+++--------------------------------------------------------------------------------+import           Hakyll.Core.Configuration+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+++--------------------------------------------------------------------------------+-- | A thread that watches for updates in a 'providerDirectory' and recompiles+-- a site as soon as any changes occur+watchUpdates :: Configuration -> IO Pattern -> IO ()+watchUpdates conf update = do+    let providerDir = providerDirectory conf+    shouldBuild     <- newEmptyMVar+    pattern         <- update+    fullProviderDir <- canonicalizePath $ providerDirectory conf+    manager         <- startManager++    let allowed event = do+            -- Absolute path of the changed file. This must be inside provider+            -- dir, since that's the only dir we're watching.+            let path       = eventPath event+                relative   = dropWhile (`elem` pathSeparators) $+                    drop (length fullProviderDir) path+                identifier = fromFilePath relative++            shouldIgnore <- shouldIgnoreFile conf path+            return $ not shouldIgnore && matches pattern identifier++    -- This thread continually watches the `shouldBuild` MVar and builds+    -- whenever a value is present.+    _ <- forkIO $ forever $ do+        event <- takeMVar shouldBuild+        handle+            (\e -> case fromException e of+                Nothing    -> putStrLn (show e)+                Just async -> throw (async :: AsyncException))+            (update' event providerDir)++    -- Send an event whenever something occurs so that the thread described+    -- above will do a build.+    void $ watchTree manager providerDir (not . isRemove) $ \event -> do+        allowed' <- allowed event+        when allowed' $ void $ tryPutMVar shouldBuild event+  where+#ifndef mingw32_HOST_OS+    update' _     _        = void update+#else+    update' event provider = do+        let path = provider </> eventPath event+        -- on windows, a 'Modified' event is also sent on file deletion+        fileExists <- doesFileExist path++        when fileExists . void $ waitOpen path ReadMode (\_ -> update) 10++    -- continuously attempts to open the file in between sleep intervals+    -- handler is run only once it is able to open the file+    waitOpen :: FilePath -> IOMode -> (Handle -> IO r) -> Integer -> IO r+    waitOpen _    _    _       0 = do+        putStrLn "[ERROR] Failed to retrieve modified file for regeneration"+        exitFailure+    waitOpen path mode handler retries = do+        res <- try $ openFile path mode :: IO (Either IOException Handle)+        case res of+            Left ex -> if isPermissionError ex+                       then do+                           threadDelay 100000+                           waitOpen path mode handler (retries - 1)+                       else throw ex+            Right h -> do+                handled <- handler h+                hClose h+                return handled+#endif+++--------------------------------------------------------------------------------+eventPath :: Event -> FilePath+eventPath evt = evtPath evt+  where+    evtPath (Added p _)    = p+    evtPath (Modified p _) = p+    evtPath (Removed p _)  = p+++--------------------------------------------------------------------------------+isRemove :: Event -> Bool+isRemove (Removed _ _) = True+isRemove _             = False
+ lib/Hakyll/Preview/Server.hs view
@@ -0,0 +1,35 @@+--------------------------------------------------------------------------------+-- | Implements a basic static file server for previewing options+{-# LANGUAGE OverloadedStrings #-}+module Hakyll.Preview.Server+    ( staticServer+    ) where+++--------------------------------------------------------------------------------+import           Data.String+import qualified Network.Wai.Handler.Warp       as Warp+import qualified Network.Wai.Application.Static as Static+import qualified Network.Wai                    as Wai+import           Network.HTTP.Types.Status      (Status)++--------------------------------------------------------------------------------+import           Hakyll.Core.Logger    (Logger)+import qualified Hakyll.Core.Logger    as Logger++staticServer :: Logger               -- ^ Logger+             -> FilePath             -- ^ Directory to serve+             -> String               -- ^ Host to bind on+             -> Int                  -- ^ Port to listen on+             -> IO ()                -- ^ Blocks forever+staticServer logger directory host port = do+    Logger.header logger $ "Listening on http://" ++ host ++ ":" ++ show port+    Warp.runSettings warpSettings $+        Static.staticApp (Static.defaultFileServerSettings directory)+  where+    warpSettings = Warp.setLogger noLog+        $ Warp.setHost (fromString host)+        $ Warp.setPort port Warp.defaultSettings++noLog :: Wai.Request -> Status -> Maybe Integer -> IO ()+noLog _ _ _ = return ()
+ lib/Hakyll/Web/CompressCss.hs view
@@ -0,0 +1,86 @@+--------------------------------------------------------------------------------+-- | Module used for CSS compression. The compression is currently in a simple+-- state, but would typically reduce the number of bytes by about 25%.+module Hakyll.Web.CompressCss+    ( compressCssCompiler+    , compressCss+    ) where+++--------------------------------------------------------------------------------+import           Data.List               (isPrefixOf)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Item+++--------------------------------------------------------------------------------+-- | Compiler form of 'compressCss'+compressCssCompiler :: Compiler (Item String)+compressCssCompiler = fmap compressCss <$> getResourceString+++--------------------------------------------------------------------------------+-- | Compress CSS to speed up your site.+compressCss :: String -> String+compressCss = compressSeparators . stripComments . compressWhitespace+++--------------------------------------------------------------------------------+-- | Compresses certain forms of separators.+compressSeparators :: String -> String+compressSeparators [] = []+compressSeparators str+    | isConstant  = head str : retainConstants compressSeparators (head str) (drop 1 str)+    | stripFirst  = compressSeparators (drop 1 str)+    | stripSecond = compressSeparators (head str : (drop 2 str))+    | otherwise   = head str : compressSeparators (drop 1 str)+  where+    isConstant  = or $ map (isOfPrefix str) ["\"", "'"]+    stripFirst  = or $ map (isOfPrefix str) $ [";;", ";}"] ++ (map (\c -> " " ++ c) separators)+    stripSecond = or $ map (isOfPrefix str) $ map (\c -> c ++ " ") separators+    separators  = [" ", "{", "}", ":", ";", ",", ">", "+", "!"]++--------------------------------------------------------------------------------+-- | Compresses all whitespace.+compressWhitespace :: String -> String+compressWhitespace [] = []+compressWhitespace str+    | isConstant = head str : retainConstants compressWhitespace (head str) (drop 1 str)+    | replaceOne = compressWhitespace (' ' : (drop 1 str))+    | replaceTwo = compressWhitespace (' ' : (drop 2 str))+    | otherwise  = head str : compressWhitespace (drop 1 str)+  where+    isConstant = or $ map (isOfPrefix str) ["\"", "'"]+    replaceOne = or $ map (isOfPrefix str) ["\t", "\n", "\r"]+    replaceTwo = or $ map (isOfPrefix str) [" \t", " \n", " \r", "  "]++--------------------------------------------------------------------------------+-- | Function that strips CSS comments away.+stripComments :: String -> String+stripComments [] = []+stripComments str+    | isConstant          = head str : retainConstants stripComments (head str) (drop 1 str)+    | isPrefixOf "/*" str = stripComments $ eatComments $ drop 2 str+    | otherwise           = head str : stripComments (drop 1 str)+  where+    isConstant  = or $ map (isOfPrefix str) ["\"", "'"]+    eatComments str'+        | null str' = []+        | isPrefixOf "*/" str' = drop 2 str'+        | otherwise = eatComments $ drop 1 str'++--------------------------------------------------------------------------------+-- | Helper function to handle string constants correctly.+retainConstants :: (String -> String) -> Char -> String -> String+retainConstants f delim str+    | null str = []+    | isPrefixOf [delim] str = head str : f (drop 1 str)+    | otherwise = head str : retainConstants f delim (drop 1 str)++--------------------------------------------------------------------------------+-- | Helper function to determine whether a string is a substring.+isOfPrefix :: String -> String -> Bool+isOfPrefix = flip isPrefixOf
+ lib/Hakyll/Web/Feed.hs view
@@ -0,0 +1,135 @@+--------------------------------------------------------------------------------+-- | A Module that allows easy rendering of RSS feeds.+--+-- The main rendering functions (@renderRss@, @renderAtom@) all assume that+-- you pass the list of items so that the most recent entry in the feed is the+-- first item in the list.+--+-- Also note that the context should have (at least) the following fields to+-- produce a correct feed:+--+-- - @$title$@: Title of the item+--+-- - @$description$@: Description to appear in the feed+--+-- - @$url$@: URL to the item - this is usually set automatically.+--+-- In addition, the posts should be named according to the rules for+-- 'Hakyll.Web.Template.Context.dateField'+module Hakyll.Web.Feed+    ( FeedConfiguration (..)+    , renderRss+    , renderAtom+    ) where+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Item+import           Hakyll.Core.Util.String (replaceAll)+import           Hakyll.Web.Template+import           Hakyll.Web.Template.Context+import           Hakyll.Web.Template.List+++--------------------------------------------------------------------------------+import           Paths_hakyll+++--------------------------------------------------------------------------------+-- | This is a data structure to keep the configuration of a feed.+data FeedConfiguration = FeedConfiguration+    { -- | Title of the feed.+      feedTitle       :: String+    , -- | Description of the feed.+      feedDescription :: String+    , -- | Name of the feed author.+      feedAuthorName  :: String+    , -- | Email of the feed author.+      feedAuthorEmail :: String+    , -- | Absolute root URL of the feed site (e.g. @http://jaspervdj.be@)+      feedRoot        :: String+    } deriving (Show, Eq)+++--------------------------------------------------------------------------------+-- | Abstract function to render any feed.+renderFeed :: FilePath                -- ^ Feed template+           -> FilePath                -- ^ Item template+           -> FeedConfiguration       -- ^ Feed configuration+           -> Context String          -- ^ Context for the items+           -> [Item String]           -- ^ Input items+           -> Compiler (Item String)  -- ^ Resulting item+renderFeed feedPath itemPath config itemContext items = do+    feedTpl <- loadTemplate feedPath+    itemTpl <- loadTemplate itemPath++    protectedItems <- mapM (applyFilter protectCDATA) items+    body <- makeItem =<< applyTemplateList itemTpl itemContext' protectedItems+    applyTemplate feedTpl feedContext body+  where+    applyFilter :: (Monad m,Functor f) => (String -> String) -> f String -> m (f String)+    applyFilter tr str = return $ fmap tr str+    protectCDATA :: String -> String+    protectCDATA = replaceAll "]]>" (const "]]&gt;")+    -- Auxiliary: load a template from a datafile+    loadTemplate path = do+        file <- compilerUnsafeIO $ getDataFileName path+        unsafeReadTemplateFile file++    itemContext' = mconcat+        [ itemContext+        , constField "root" (feedRoot config)+        , constField "authorName"  (feedAuthorName config)+        , constField "authorEmail" (feedAuthorEmail config)+        ]++    feedContext = mconcat+         [ bodyField  "body"+         , constField "title"       (feedTitle config)+         , constField "description" (feedDescription config)+         , constField "authorName"  (feedAuthorName config)+         , constField "authorEmail" (feedAuthorEmail config)+         , constField "root"        (feedRoot config)+         , urlField   "url"+         , updatedField+         , missingField+         ]++    -- Take the first "updated" field from all items -- this should be the most+    -- recent.+    updatedField = field "updated" $ \_ -> case items of+        []      -> return "Unknown"+        (x : _) -> unContext itemContext' "updated" [] x >>= \cf -> case cf of+            ListField _ _ -> fail "Hakyll.Web.Feed.renderFeed: Internal error"+            StringField s -> return s+++--------------------------------------------------------------------------------+-- | Render an RSS feed with a number of items.+renderRss :: FeedConfiguration       -- ^ Feed configuration+          -> Context String          -- ^ Item context+          -> [Item String]           -- ^ Feed items+          -> Compiler (Item String)  -- ^ Resulting feed+renderRss config context = renderFeed+    "templates/rss.xml" "templates/rss-item.xml" config+    (makeItemContext "%a, %d %b %Y %H:%M:%S UT" context)+++--------------------------------------------------------------------------------+-- | Render an Atom feed with a number of items.+renderAtom :: FeedConfiguration       -- ^ Feed configuration+           -> Context String          -- ^ Item context+           -> [Item String]           -- ^ Feed items+           -> Compiler (Item String)  -- ^ Resulting feed+renderAtom config context = renderFeed+    "templates/atom.xml" "templates/atom-item.xml" config+    (makeItemContext "%Y-%m-%dT%H:%M:%SZ" context)+++--------------------------------------------------------------------------------+-- | Copies @$updated$@ from @$published$@ if it is not already set.+makeItemContext :: String -> Context a -> Context a+makeItemContext fmt context = mconcat+    [dateField "published" fmt, context, dateField "updated" fmt]
+ lib/Hakyll/Web/Html.hs view
@@ -0,0 +1,184 @@+--------------------------------------------------------------------------------+-- | Provides utilities to manipulate HTML pages+module Hakyll.Web.Html+    ( -- * Generic+      withTags++      -- * Headers+    , demoteHeaders++      -- * Url manipulation+    , getUrls+    , withUrls+    , toUrl+    , toSiteRoot+    , isExternal++      -- * Stripping/escaping+    , stripTags+    , escapeHtml+    ) where+++--------------------------------------------------------------------------------+import           Data.Char                       (digitToInt, intToDigit,+                                                  isDigit, toLower)+import           Data.List                       (isPrefixOf)+import qualified Data.Set                        as S+import           System.FilePath.Posix           (joinPath, splitPath,+                                                  takeDirectory)+import           Text.Blaze.Html                 (toHtml)+import           Text.Blaze.Html.Renderer.String (renderHtml)+import qualified Text.HTML.TagSoup               as TS+import           Network.URI                     (isUnreserved, escapeURIString)+++--------------------------------------------------------------------------------+-- | Map over all tags in the document+withTags :: (TS.Tag String -> TS.Tag String) -> String -> String+withTags f = renderTags' . map f . parseTags'+++--------------------------------------------------------------------------------+-- | Map every @h1@ to an @h2@, @h2@ to @h3@, etc.+demoteHeaders :: String -> String+demoteHeaders = withTags $ \tag -> case tag of+    TS.TagOpen t a -> TS.TagOpen (demote t) a+    TS.TagClose t  -> TS.TagClose (demote t)+    t              -> t+  where+    demote t@['h', n]+        | isDigit n = ['h', intToDigit (min 6 $ digitToInt n + 1)]+        | otherwise = t+    demote t        = t+++--------------------------------------------------------------------------------+isUrlAttribute :: String -> Bool+isUrlAttribute = (`elem` ["src", "href", "data", "poster"])+++--------------------------------------------------------------------------------+getUrls :: [TS.Tag String] -> [String]+getUrls tags = [v | TS.TagOpen _ as <- tags, (k, v) <- as, isUrlAttribute k]+++--------------------------------------------------------------------------------+-- | Apply a function to each URL on a webpage+withUrls :: (String -> String) -> String -> String+withUrls f = withTags tag+  where+    tag (TS.TagOpen s a) = TS.TagOpen s $ map attr a+    tag x                = x+    attr (k, v)          = (k, if isUrlAttribute k then f v else v)+++--------------------------------------------------------------------------------+-- | Customized TagSoup renderer. The default TagSoup renderer escape CSS+-- within style tags, and doesn't properly minimize.+renderTags' :: [TS.Tag String] -> String+renderTags' = TS.renderTagsOptions TS.RenderOptions+    { TS.optRawTag   = (`elem` ["script", "style"]) . map toLower+    , TS.optMinimize = (`S.member` minimize) . map toLower+    , TS.optEscape   = id+    }+  where+    -- A list of elements which must be minimized+    minimize = S.fromList+        [ "area", "br", "col", "embed", "hr", "img", "input", "meta", "link"+        , "param"+        ]+++--------------------------------------------------------------------------------+-- | Customized TagSoup parser: do not decode any entities.+parseTags' :: String -> [TS.Tag String]+parseTags' = TS.parseTagsOptions (TS.parseOptions :: TS.ParseOptions String)+    { TS.optEntityData   = \(str, b) -> [TS.TagText $ "&" ++ str ++ [';' | b]]+    , TS.optEntityAttrib = \(str, b) -> ("&" ++ str ++ [';' | b], [])+    }+++--------------------------------------------------------------------------------+-- | Convert a filepath to an URL starting from the site root+--+-- Example:+--+-- > toUrl "foo/bar.html"+--+-- Result:+--+-- > "/foo/bar.html"+--+-- This also sanitizes the URL, e.g. converting spaces into '%20'+toUrl :: FilePath -> String+toUrl url = case url of+    ('/' : xs) -> '/' : sanitize xs+    xs         -> '/' : sanitize xs+  where+    -- Everything but unreserved characters should be escaped as we are+    -- sanitising the path therefore reserved characters which have a+    -- meaning in URI does not appear. Special casing for `/`, because it has+    -- a special meaning in FilePath as well as in URI.+    sanitize = escapeURIString (\c -> c == '/' || isUnreserved c)+++--------------------------------------------------------------------------------+-- | Get the relative url to the site root, for a given (absolute) url+toSiteRoot :: String -> String+toSiteRoot = emptyException . joinPath . map parent+           . filter relevant . splitPath . takeDirectory+  where+    parent            = const ".."+    emptyException [] = "."+    emptyException x  = x+    relevant "."      = False+    relevant "/"      = False+    relevant "./"     = False+    relevant _        = True+++--------------------------------------------------------------------------------+-- | Check if an URL links to an external HTTP(S) source+isExternal :: String -> Bool+isExternal url = any (flip isPrefixOf url) ["http://", "https://", "//"]+++--------------------------------------------------------------------------------+-- | Strip all HTML tags from a string+--+-- Example:+--+-- > stripTags "<p>foo</p>"+--+-- Result:+--+-- > "foo"+--+-- This also works for incomplete tags+--+-- Example:+--+-- > stripTags "<p>foo</p"+--+-- Result:+--+-- > "foo"+stripTags :: String -> String+stripTags []         = []+stripTags ('<' : xs) = stripTags $ drop 1 $ dropWhile (/= '>') xs+stripTags (x : xs)   = x : stripTags xs+++--------------------------------------------------------------------------------+-- | HTML-escape a string+--+-- Example:+--+-- > escapeHtml "Me & Dean"+--+-- Result:+--+-- > "Me &amp; Dean"+escapeHtml :: String -> String+escapeHtml = renderHtml . toHtml
+ lib/Hakyll/Web/Html/RelativizeUrls.hs view
@@ -0,0 +1,52 @@+--------------------------------------------------------------------------------+-- | This module exposes a function which can relativize URL's on a webpage.+--+-- This means that one can deploy the resulting site on+-- @http:\/\/example.com\/@, but also on @http:\/\/example.com\/some-folder\/@+-- without having to change anything (simply copy over the files).+--+-- To use it, you should use absolute URL's from the site root everywhere. For+-- example, use+--+-- > <img src="/images/lolcat.png" alt="Funny zomgroflcopter" />+--+-- in a blogpost. When running this through the relativize URL's module, this+-- will result in (suppose your blogpost is located at @\/posts\/foo.html@:+--+-- > <img src="../images/lolcat.png" alt="Funny zomgroflcopter" />+module Hakyll.Web.Html.RelativizeUrls+    ( relativizeUrls+    , relativizeUrlsWith+    ) where+++--------------------------------------------------------------------------------+import           Data.List            (isPrefixOf)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Item+import           Hakyll.Web.Html+++--------------------------------------------------------------------------------+-- | Compiler form of 'relativizeUrls' which automatically picks the right root+-- path+relativizeUrls :: Item String -> Compiler (Item String)+relativizeUrls item = do+    route <- getRoute $ itemIdentifier item+    return $ case route of+        Nothing -> item+        Just r  -> fmap (relativizeUrlsWith $ toSiteRoot r) item+++--------------------------------------------------------------------------------+-- | Relativize URL's in HTML+relativizeUrlsWith :: String  -- ^ Path to the site root+                   -> String  -- ^ HTML to relativize+                   -> String  -- ^ Resulting HTML+relativizeUrlsWith root = withUrls rel+  where+    isRel x = "/" `isPrefixOf` x && not ("//" `isPrefixOf` x)+    rel x   = if isRel x then root ++ x else x
+ lib/Hakyll/Web/Paginate.hs view
@@ -0,0 +1,153 @@+--------------------------------------------------------------------------------+{-# LANGUAGE OverloadedStrings #-}+module Hakyll.Web.Paginate+    ( PageNumber+    , Paginate (..)+    , buildPaginateWith+    , paginateEvery+    , paginateRules+    , paginateContext+    ) where+++--------------------------------------------------------------------------------+import           Control.Applicative            (empty)+import           Control.Monad                  (forM_, forM)+import qualified Data.Map                       as M+import qualified Data.Set                       as S+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Item+import           Hakyll.Core.Metadata+import           Hakyll.Core.Rules+import           Hakyll.Web.Html+import           Hakyll.Web.Template.Context+++--------------------------------------------------------------------------------+type PageNumber = Int+++--------------------------------------------------------------------------------+-- | Data about paginators+data Paginate = Paginate+    { paginateMap        :: M.Map PageNumber [Identifier]+    , paginateMakeId     :: PageNumber -> Identifier+    , paginateDependency :: Dependency+    }+++--------------------------------------------------------------------------------+paginateNumPages :: Paginate -> Int+paginateNumPages = M.size . paginateMap+++--------------------------------------------------------------------------------+paginateEvery :: Int -> [a] -> [[a]]+paginateEvery n = go+  where+    go [] = []+    go xs = let (y, ys) = splitAt n xs in y : go ys+++--------------------------------------------------------------------------------+buildPaginateWith+    :: MonadMetadata m+    => ([Identifier] -> m [[Identifier]])  -- ^ Group items into pages+    -> Pattern                             -- ^ Select items to paginate+    -> (PageNumber -> Identifier)          -- ^ Identifiers for the pages+    -> m Paginate+buildPaginateWith grouper pattern makeId = do+    ids      <- getMatches pattern+    idGroups <- grouper ids+    let idsSet = S.fromList ids+    return Paginate+        { paginateMap        = M.fromList (zip [1 ..] idGroups)+        , paginateMakeId     = makeId+        , paginateDependency = PatternDependency pattern idsSet+        }+++--------------------------------------------------------------------------------+paginateRules :: Paginate -> (PageNumber -> Pattern -> Rules ()) -> Rules ()+paginateRules paginator rules =+    forM_ (M.toList $ paginateMap paginator) $ \(idx, identifiers) ->+        rulesExtraDependencies [paginateDependency paginator] $+            create [paginateMakeId paginator idx] $+                rules idx $ fromList identifiers+++--------------------------------------------------------------------------------+-- | Get the identifier for a certain page by passing in the page number.+paginatePage :: Paginate -> PageNumber -> Maybe Identifier+paginatePage pag pageNumber+    | pageNumber < 1                      = Nothing+    | pageNumber > (paginateNumPages pag) = Nothing+    | otherwise                           = Just $ paginateMakeId pag pageNumber+++--------------------------------------------------------------------------------+-- | A default paginate context which provides the following keys:+--+--+-- * @firstPageNum@+-- * @firstPageUrl@+-- * @previousPageNum@+-- * @previousPageUrl@+-- * @nextPageNum@+-- * @nextPageUrl@+-- * @lastPageNum@+-- * @lastPageUrl@+-- * @currentPageNum@+-- * @currentPageUrl@+-- * @numPages@+-- * @allPages@+paginateContext :: Paginate -> PageNumber -> Context a+paginateContext pag currentPage = mconcat+    [ field "firstPageNum"    $ \_ -> otherPage 1                 >>= num+    , field "firstPageUrl"    $ \_ -> otherPage 1                 >>= url+    , field "previousPageNum" $ \_ -> otherPage (currentPage - 1) >>= num+    , field "previousPageUrl" $ \_ -> otherPage (currentPage - 1) >>= url+    , field "nextPageNum"     $ \_ -> otherPage (currentPage + 1) >>= num+    , field "nextPageUrl"     $ \_ -> otherPage (currentPage + 1) >>= url+    , field "lastPageNum"     $ \_ -> otherPage lastPage          >>= num+    , field "lastPageUrl"     $ \_ -> otherPage lastPage          >>= url+    , field "currentPageNum"  $ \i -> thisPage i                  >>= num+    , field "currentPageUrl"  $ \i -> thisPage i                  >>= url+    , constField "numPages"   $ show $ paginateNumPages pag+    , Context $ \k _ i -> case k of+        "allPages" -> do+            let ctx =+                    field "isCurrent" (\n -> if fst (itemBody n) == currentPage then return "true" else empty) `mappend`+                    field "num" (num . itemBody) `mappend`+                    field "url" (url . itemBody)++            list <- forM [1 .. lastPage] $+                \n -> if n == currentPage then thisPage i else otherPage n+            items <- mapM makeItem list+            return $ ListField ctx items+        _          -> do+            empty++    ]+  where+    lastPage = paginateNumPages pag++    thisPage i = return (currentPage, itemIdentifier i)+    otherPage n+        | n == currentPage = fail $ "This is the current page: " ++ show n+        | otherwise        = case paginatePage pag n of+            Nothing -> fail $ "No such page: " ++ show n+            Just i  -> return (n, i)++    num :: (Int, Identifier) -> Compiler String+    num = return . show . fst++    url :: (Int, Identifier) -> Compiler String+    url (n, i) = getRoute i >>= \mbR -> case mbR of+        Just r  -> return $ toUrl r+        Nothing -> fail $ "No URL for page: " ++ show n
+ lib/Hakyll/Web/Pandoc.hs view
@@ -0,0 +1,164 @@+--------------------------------------------------------------------------------+-- | Module exporting convenient pandoc bindings+module Hakyll.Web.Pandoc+    ( -- * The basic building blocks+      readPandoc+    , readPandocWith+    , writePandoc+    , writePandocWith+    , renderPandoc+    , renderPandocWith++      -- * Derived compilers+    , pandocCompiler+    , pandocCompilerWith+    , pandocCompilerWithTransform+    , pandocCompilerWithTransformM++      -- * Default options+    , defaultHakyllReaderOptions+    , defaultHakyllWriterOptions+    ) where+++--------------------------------------------------------------------------------+import qualified Data.Set                   as S+import           Text.Pandoc+import           Text.Pandoc.Error          (PandocError (..))+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Item+import           Hakyll.Web.Pandoc.FileType+++--------------------------------------------------------------------------------+-- | Read a string using pandoc, with the default options+readPandoc+    :: Item String             -- ^ String to read+    -> Compiler (Item Pandoc)  -- ^ Resulting document+readPandoc = readPandocWith defaultHakyllReaderOptions+++--------------------------------------------------------------------------------+-- | Read a string using pandoc, with the supplied options+readPandocWith+    :: ReaderOptions           -- ^ Parser options+    -> Item String             -- ^ String to read+    -> Compiler (Item Pandoc)  -- ^ Resulting document+readPandocWith ropt item =+    case traverse (reader ropt (itemFileType item)) item of+        Left (ParseFailure err)  -> fail $+            "Hakyll.Web.Pandoc.readPandocWith: parse failed: " ++ err+        Left (ParsecError _ err) -> fail $+            "Hakyll.Web.Pandoc.readPandocWith: parse failed: " ++ show err+        Right item'              -> return item'+  where+    reader ro t = case t of+        DocBook            -> readDocBook ro+        Html               -> readHtml ro+        LaTeX              -> readLaTeX ro+        LiterateHaskell t' -> reader (addExt ro Ext_literate_haskell) t'+        Markdown           -> readMarkdown ro+        MediaWiki          -> readMediaWiki ro+        OrgMode            -> readOrg ro+        Rst                -> readRST ro+        Textile            -> readTextile ro+        _                  -> error $+            "Hakyll.Web.readPandocWith: I don't know how to read a file of " +++            "the type " ++ show t ++ " for: " ++ show (itemIdentifier item)++    addExt ro e = ro {readerExtensions = S.insert e $ readerExtensions ro}+++--------------------------------------------------------------------------------+-- | Write a document (as HTML) using pandoc, with the default options+writePandoc :: Item Pandoc  -- ^ Document to write+            -> Item String  -- ^ Resulting HTML+writePandoc = writePandocWith defaultHakyllWriterOptions+++--------------------------------------------------------------------------------+-- | Write a document (as HTML) using pandoc, with the supplied options+writePandocWith :: WriterOptions  -- ^ Writer options for pandoc+                -> Item Pandoc    -- ^ Document to write+                -> Item String    -- ^ Resulting HTML+writePandocWith wopt = fmap $ writeHtmlString wopt+++--------------------------------------------------------------------------------+-- | Render the resource using pandoc+renderPandoc :: Item String -> Compiler (Item String)+renderPandoc =+    renderPandocWith defaultHakyllReaderOptions defaultHakyllWriterOptions+++--------------------------------------------------------------------------------+-- | Render the resource using pandoc+renderPandocWith+    :: ReaderOptions -> WriterOptions -> Item String -> Compiler (Item String)+renderPandocWith ropt wopt item =+    writePandocWith wopt <$> readPandocWith ropt item+++--------------------------------------------------------------------------------+-- | Read a page render using pandoc+pandocCompiler :: Compiler (Item String)+pandocCompiler =+    pandocCompilerWith defaultHakyllReaderOptions defaultHakyllWriterOptions+++--------------------------------------------------------------------------------+-- | A version of 'pandocCompiler' which allows you to specify your own pandoc+-- options+pandocCompilerWith :: ReaderOptions -> WriterOptions -> Compiler (Item String)+pandocCompilerWith ropt wopt =+    cached "Hakyll.Web.Pandoc.pandocCompilerWith" $+        pandocCompilerWithTransform ropt wopt id+++--------------------------------------------------------------------------------+-- | An extension of 'pandocCompilerWith' which allows you to specify a custom+-- pandoc transformation for the content+pandocCompilerWithTransform :: ReaderOptions -> WriterOptions+                            -> (Pandoc -> Pandoc)+                            -> Compiler (Item String)+pandocCompilerWithTransform ropt wopt f =+    pandocCompilerWithTransformM ropt wopt (return . f)+++--------------------------------------------------------------------------------+-- | Similar to 'pandocCompilerWithTransform', but the transformation+-- function is monadic. This is useful when you want the pandoc+-- transformation to use the 'Compiler' information such as routes,+-- metadata, etc+pandocCompilerWithTransformM :: ReaderOptions -> WriterOptions+                    -> (Pandoc -> Compiler Pandoc)+                    -> Compiler (Item String)+pandocCompilerWithTransformM ropt wopt f =+    writePandocWith wopt <$>+        (traverse f =<< readPandocWith ropt =<< getResourceBody)+++--------------------------------------------------------------------------------+-- | The default reader options for pandoc parsing in hakyll+defaultHakyllReaderOptions :: ReaderOptions+defaultHakyllReaderOptions = def+    { -- The following option causes pandoc to read smart typography, a nice+      -- and free bonus.+      readerSmart = True+    }+++--------------------------------------------------------------------------------+-- | The default writer options for pandoc rendering in hakyll+defaultHakyllWriterOptions :: WriterOptions+defaultHakyllWriterOptions = def+    { -- This option causes literate haskell to be written using '>' marks in+      -- html, which I think is a good default.+      writerExtensions = S.insert Ext_literate_haskell (writerExtensions def)+    , -- We want to have hightlighting by default, to be compatible with earlier+      -- Hakyll releases+      writerHighlight  = True+    }
+ lib/Hakyll/Web/Pandoc/Biblio.hs view
@@ -0,0 +1,115 @@+--------------------------------------------------------------------------------+-- | Wraps pandocs bibiliography handling+--+-- In order to add a bibliography, you will need a bibliography file (e.g.+-- @.bib@) and a CSL file (@.csl@). Both need to be compiled with their+-- respective compilers ('biblioCompiler' and 'cslCompiler'). Then, you can+-- refer to these files when you use 'readPandocBiblio'. This function also+-- takes the reader options for completeness -- you can use+-- 'defaultHakyllReaderOptions' if you're unsure.+-- 'pandocBiblioCompiler' is a convenience wrapper which works like 'pandocCompiler',+-- but also takes paths to compiled bibliography and csl files.+{-# LANGUAGE Arrows                     #-}+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Hakyll.Web.Pandoc.Biblio+    ( CSL+    , cslCompiler+    , Biblio (..)+    , biblioCompiler+    , readPandocBiblio+    , pandocBiblioCompiler+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad            (liftM, replicateM)+import           Data.Binary              (Binary (..))+import           Data.Default             (def)+import           Data.Typeable            (Typeable)+import           Hakyll.Core.Compiler+import           Hakyll.Core.Identifier+import           Hakyll.Core.Item+import           Hakyll.Core.Writable+import           Hakyll.Web.Pandoc+import           Hakyll.Web.Pandoc.Binary ()+import qualified Text.CSL                 as CSL+import           Text.CSL.Pandoc          (processCites)+import           Text.Pandoc              (Pandoc, ReaderOptions (..))+++--------------------------------------------------------------------------------+data CSL = CSL+    deriving (Show, Typeable)+++--------------------------------------------------------------------------------+instance Binary CSL where+    put CSL = return ()+    get     = return CSL+++--------------------------------------------------------------------------------+instance Writable CSL where+    -- Shouldn't be written.+    write _ _ = return ()+++--------------------------------------------------------------------------------+cslCompiler :: Compiler (Item CSL)+cslCompiler = makeItem CSL+++--------------------------------------------------------------------------------+newtype Biblio = Biblio [CSL.Reference]+    deriving (Show, Typeable)+++--------------------------------------------------------------------------------+instance Binary Biblio where+    -- Ugly.+    get             = do+        len <- get+        Biblio <$> replicateM len get+    put (Biblio rs) = put (length rs) >> mapM_ put rs+++--------------------------------------------------------------------------------+instance Writable Biblio where+    -- Shouldn't be written.+    write _ _ = return ()+++--------------------------------------------------------------------------------+biblioCompiler :: Compiler (Item Biblio)+biblioCompiler = do+    filePath <- toFilePath <$> getUnderlying+    makeItem =<< unsafeCompiler (Biblio <$> CSL.readBiblioFile filePath)+++--------------------------------------------------------------------------------+readPandocBiblio :: ReaderOptions+                 -> Item CSL+                 -> Item Biblio+                 -> (Item String)+                 -> Compiler (Item Pandoc)+readPandocBiblio ropt csl biblio item = do+    -- Parse CSL file, if given+    style <- unsafeCompiler $ CSL.readCSLFile Nothing . toFilePath . itemIdentifier $ csl++    -- We need to know the citation keys, add then *before* actually parsing the+    -- actual page. If we don't do this, pandoc won't even consider them+    -- citations!+    let Biblio refs = itemBody biblio+    pandoc <- itemBody <$> readPandocWith ropt item+    let pandoc' = processCites style refs pandoc++    return $ fmap (const pandoc') item++--------------------------------------------------------------------------------+pandocBiblioCompiler :: String -> String -> Compiler (Item String)+pandocBiblioCompiler cslFileName bibFileName = do+    csl <- load $ fromFilePath cslFileName+    bib <- load $ fromFilePath bibFileName+    liftM writePandoc+        (getResourceBody >>= readPandocBiblio def csl bib)
+ lib/Hakyll/Web/Pandoc/Binary.hs view
@@ -0,0 +1,32 @@+{-# OPTIONS_GHC -fno-warn-orphans #-}+{-# LANGUAGE DeriveGeneric #-}+module Hakyll.Web.Pandoc.Binary where++import           Data.Binary        (Binary (..))++import qualified Text.CSL           as CSL+import qualified Text.CSL.Reference as REF+import qualified Text.CSL.Style     as STY+import           Text.Pandoc++--------------------------------------------------------------------------------+-- orphans++instance Binary Alignment+instance Binary Block+instance Binary CSL.Reference+instance Binary Citation+instance Binary CitationMode+instance Binary Format+instance Binary Inline+instance Binary ListNumberDelim+instance Binary ListNumberStyle+instance Binary MathType+instance Binary QuoteType+instance Binary REF.CLabel+instance Binary REF.CNum+instance Binary REF.Literal+instance Binary REF.RefDate+instance Binary REF.RefType+instance Binary STY.Agent+instance Binary STY.Formatted
+ lib/Hakyll/Web/Pandoc/FileType.hs view
@@ -0,0 +1,74 @@+--------------------------------------------------------------------------------+-- | A module dealing with pandoc file extensions and associated file types+module Hakyll.Web.Pandoc.FileType+    ( FileType (..)+    , fileType+    , itemFileType+    ) where+++--------------------------------------------------------------------------------+import           System.FilePath        (splitExtension)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Identifier+import           Hakyll.Core.Item+++--------------------------------------------------------------------------------+-- | Datatype to represent the different file types Hakyll can deal with by+-- default+data FileType+    = Binary+    | Css+    | DocBook+    | Html+    | LaTeX+    | LiterateHaskell FileType+    | Markdown+    | MediaWiki+    | OrgMode+    | PlainText+    | Rst+    | Textile+    deriving (Eq, Ord, Show, Read)+++--------------------------------------------------------------------------------+-- | Get the file type for a certain file. The type is determined by extension.+fileType :: FilePath -> FileType+fileType = uncurry fileType' . splitExtension+  where+    fileType' _ ".css"       = Css+    fileType' _ ".dbk"       = DocBook+    fileType' _ ".htm"       = Html+    fileType' _ ".html"      = Html+    fileType' f ".lhs"       = LiterateHaskell $ case fileType f of+        -- If no extension is given, default to Markdown + LiterateHaskell+        Binary -> Markdown+        -- Otherwise, LaTeX + LiterateHaskell or whatever the user specified+        x      -> x+    fileType' _ ".markdown"  = Markdown+    fileType' _ ".mediawiki" = MediaWiki+    fileType' _ ".md"        = Markdown+    fileType' _ ".mdn"       = Markdown+    fileType' _ ".mdown"     = Markdown+    fileType' _ ".mdwn"      = Markdown+    fileType' _ ".mkd"       = Markdown+    fileType' _ ".mkdwn"     = Markdown+    fileType' _ ".org"       = OrgMode+    fileType' _ ".page"      = Markdown+    fileType' _ ".rst"       = Rst+    fileType' _ ".tex"       = LaTeX+    fileType' _ ".text"      = PlainText+    fileType' _ ".textile"   = Textile+    fileType' _ ".txt"       = PlainText+    fileType' _ ".wiki"      = MediaWiki+    fileType' _ _            = Binary  -- Treat unknown files as binary+++--------------------------------------------------------------------------------+-- | Get the file type for the current file+itemFileType :: Item a -> FileType+itemFileType = fileType . toFilePath . itemIdentifier
+ lib/Hakyll/Web/Redirect.hs view
@@ -0,0 +1,87 @@+-- | Module used for generating HTML redirect pages. This allows renaming pages+-- to avoid breaking existing links without requiring server-side support for+-- formal 301 Redirect error codes+module Hakyll.Web.Redirect+    ( Redirect (..)+    , createRedirects+    ) where++import           Control.Applicative    ((<$>))+import           Control.Monad          (forM_)+import           Data.Binary            (Binary (..))+import           Hakyll.Core.Compiler+import           Hakyll.Core.Identifier+import           Hakyll.Core.Routes+import           Hakyll.Core.Rules+import           Hakyll.Core.Writable   (Writable (..))++-- | This function exposes a higher-level interface compared to using the+-- 'Redirect' type manually.+--+-- This creates, using a database mapping broken URLs to working ones, HTML+-- files which will do HTML META tag redirect pages (since, as a static site, we+-- can't use web-server-level 301 redirects, and using JS is gross).+--+-- This is useful for sending people using old URLs to renamed versions, dealing+-- with common typos etc, and will increase site traffic.  Such broken URLs can+-- be found by looking at server logs or by using Google Webmaster Tools.+-- Broken URLs must be valid Haskell strings, non-URL-escaped valid POSIX+-- filenames, and relative links, since they will be defined in a @hakyll.hs@+-- and during generation, written to disk with the filename corresponding to the+-- broken URLs.  (Target URLs can be absolute or relative, but should be+-- URL-escaped.) So broken incoming links like <http://www.gwern.net/foo/> which+-- should be <http://www.gwern.net/foobar> cannot be fixed (since you cannot+-- create a HTML file named @"foo/"@ on disk, as that would be a directory).+--+-- An example of a valid association list would be:+--+-- > brokenLinks =+-- >     [ ("projects.html", "http://github.com/gwern")+-- >     , ("/Black-market archive", "Black-market%20archives")+-- >     ]+--+-- In which case the functionality can then be used in `main` with a line like:+--+-- > version "redirects" $ createRedirects brokenLinks+--+-- The 'version' is recommended to separate these items from your other pages.+--+-- The on-disk files can then be uploaded with HTML mimetypes+-- (either explicitly by generating and uploading them separately, by+-- auto-detection of the filetype, or an upload tool defaulting to HTML+-- mimetype, such as calling @s3cmd@ with @--default-mime-type=text/html@) and+-- will redirect browsers and search engines going to the old/broken URLs.+--+-- See also <https://groups.google.com/d/msg/hakyll/sWc6zxfh-uM/fUpZPsFNDgAJ>.+createRedirects :: [(Identifier, String)] -> Rules ()+createRedirects redirects =+    forM_ redirects $ \(ident, to) ->+        create [ident] $ do+            route idRoute+            compile $ makeItem $! Redirect to++-- | This datatype can be used directly if you want a lower-level interface to+-- generate redirects.  For example, if you want to redirect @foo.html@ to+-- @bar.jpg@, you can use:+--+-- > create ["foo.html"] $ do+-- >     route idRoute+-- >     compile $ makeItem $ Redirect "bar.jpg"+data Redirect = Redirect+    { redirectTo :: String+    } deriving (Eq, Ord, Show)++instance Binary Redirect where+    put (Redirect to) = put to+    get = Redirect <$> get++instance Writable Redirect where+    write path = write path . fmap redirectToHtml++redirectToHtml :: Redirect -> String+redirectToHtml (Redirect working) =+    "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/><meta name=\"generator\" content=\"hakyll\"/>" +++    "<meta http-equiv=\"refresh\" content=\"0; url=" ++ working +++    "\"><link rel=\"canonical\" href=\"" ++ working +++    "\"><title>Permanent Redirect</title></head><body><p>The page has moved to: <a href=\"" ++ working +++    "\">this page</a></p></body></html>"
+ lib/Hakyll/Web/Tags.hs view
@@ -0,0 +1,344 @@+--------------------------------------------------------------------------------+-- | This module containing some specialized functions to deal with tags. It+-- assumes you follow some conventions.+--+-- We support two types of tags: tags and categories.+--+-- To use default tags, use 'buildTags'. Tags are placed in a comma-separated+-- metadata field like this:+--+-- > ---+-- > author: Philip K. Dick+-- > title: Do androids dream of electric sheep?+-- > tags: future, science fiction, humanoid+-- > ---+-- > The novel is set in a post-apocalyptic near future, where the Earth and+-- > its populations have been damaged greatly by Nuclear...+--+-- To use categories, use the 'buildCategories' function. Categories are+-- determined by the directory a page is in, for example, the post+--+-- > posts/coding/2010-01-28-hakyll-categories.markdown+--+-- will receive the @coding@ category.+--+-- Advanced users may implement custom systems using 'buildTagsWith' if desired.+--+-- In the above example, we would want to create a page which lists all pages in+-- the @coding@ category, for example, with the 'Identifier':+--+-- > tags/coding.html+--+-- This is where the first parameter of 'buildTags' and 'buildCategories' comes+-- in. In the above case, we used the function:+--+-- > fromCapture "tags/*.html" :: String -> Identifier+--+-- The 'tagsRules' function lets you generate such a page for each tag in the+-- 'Rules' monad.+{-# LANGUAGE Arrows                     #-}+{-# LANGUAGE DeriveDataTypeable         #-}+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE OverloadedStrings          #-}+module Hakyll.Web.Tags+    ( Tags (..)+    , getTags+    , buildTagsWith+    , buildTags+    , buildCategories+    , tagsRules+    , renderTags+    , renderTagCloud+    , renderTagCloudWith+    , tagCloudField+    , tagCloudFieldWith+    , renderTagList+    , tagsField+    , tagsFieldWith+    , categoryField+    , sortTagsBy+    , caseInsensitiveTags+    ) where+++--------------------------------------------------------------------------------+import           Control.Arrow                   ((&&&))+import           Control.Monad                   (foldM, forM, forM_, mplus)+import           Data.Char                       (toLower)+import           Data.List                       (intercalate, intersperse,+                                                  sortBy)+import qualified Data.Map                        as M+import           Data.Maybe                      (catMaybes, fromMaybe)+import           Data.Ord                        (comparing)+import qualified Data.Set                        as S+import           System.FilePath                 (takeBaseName, takeDirectory)+import           Text.Blaze.Html                 (toHtml, toValue, (!))+import           Text.Blaze.Html.Renderer.String (renderHtml)+import qualified Text.Blaze.Html5                as H+import qualified Text.Blaze.Html5.Attributes     as A+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Dependencies+import           Hakyll.Core.Identifier+import           Hakyll.Core.Identifier.Pattern+import           Hakyll.Core.Item+import           Hakyll.Core.Metadata+import           Hakyll.Core.Rules+import           Hakyll.Core.Util.String+import           Hakyll.Web.Html+import           Hakyll.Web.Template.Context+++--------------------------------------------------------------------------------+-- | Data about tags+data Tags = Tags+    { tagsMap        :: [(String, [Identifier])]+    , tagsMakeId     :: String -> Identifier+    , tagsDependency :: Dependency+    }+++--------------------------------------------------------------------------------+-- | Obtain tags from a page in the default way: parse them from the @tags@+-- metadata field. This can either be a list or a comma-separated string.+getTags :: MonadMetadata m => Identifier -> m [String]+getTags identifier = do+    metadata <- getMetadata identifier+    return $ fromMaybe [] $+        (lookupStringList "tags" metadata) `mplus`+        (map trim . splitAll "," <$> lookupString "tags" metadata)+++--------------------------------------------------------------------------------+-- | Obtain categories from a page.+getCategory :: MonadMetadata m => Identifier -> m [String]+getCategory = return . return . takeBaseName . takeDirectory . toFilePath+++--------------------------------------------------------------------------------+-- | Higher-order function to read tags+buildTagsWith :: MonadMetadata m+              => (Identifier -> m [String])+              -> Pattern+              -> (String -> Identifier)+              -> m Tags+buildTagsWith f pattern makeId = do+    ids    <- getMatches pattern+    tagMap <- foldM addTags M.empty ids+    let set' = S.fromList ids+    return $ Tags (M.toList tagMap) makeId (PatternDependency pattern set')+  where+    -- Create a tag map for one page+    addTags tagMap id' = do+        tags <- f id'+        let tagMap' = M.fromList $ zip tags $ repeat [id']+        return $ M.unionWith (++) tagMap tagMap'+++--------------------------------------------------------------------------------+buildTags :: MonadMetadata m => Pattern -> (String -> Identifier) -> m Tags+buildTags = buildTagsWith getTags+++--------------------------------------------------------------------------------+buildCategories :: MonadMetadata m => Pattern -> (String -> Identifier)+                -> m Tags+buildCategories = buildTagsWith getCategory+++--------------------------------------------------------------------------------+tagsRules :: Tags -> (String -> Pattern -> Rules ()) -> Rules ()+tagsRules tags rules =+    forM_ (tagsMap tags) $ \(tag, identifiers) ->+        rulesExtraDependencies [tagsDependency tags] $+            create [tagsMakeId tags tag] $+                rules tag $ fromList identifiers+++--------------------------------------------------------------------------------+-- | Render tags in HTML (the flexible higher-order function)+renderTags :: (String -> String -> Int -> Int -> Int -> String)+           -- ^ Produce a tag item: tag, url, count, min count, max count+           -> ([String] -> String)+           -- ^ Join items+           -> Tags+           -- ^ Tag cloud renderer+           -> Compiler String+renderTags makeHtml concatHtml tags = do+    -- In tags' we create a list: [((tag, route), count)]+    tags' <- forM (tagsMap tags) $ \(tag, ids) -> do+        route' <- getRoute $ tagsMakeId tags tag+        return ((tag, route'), length ids)++    -- TODO: We actually need to tell a dependency here!++    let -- Absolute frequencies of the pages+        freqs = map snd tags'++        -- The minimum and maximum count found+        (min', max')+            | null freqs = (0, 1)+            | otherwise  = (minimum &&& maximum) freqs++        -- Create a link for one item+        makeHtml' ((tag, url), count) =+            makeHtml tag (toUrl $ fromMaybe "/" url) count min' max'++    -- Render and return the HTML+    return $ concatHtml $ map makeHtml' tags'+++--------------------------------------------------------------------------------+-- | Render a tag cloud in HTML+renderTagCloud :: Double+               -- ^ Smallest font size, in percent+               -> Double+               -- ^ Biggest font size, in percent+               -> Tags+               -- ^ Input tags+               -> Compiler String+               -- ^ Rendered cloud+renderTagCloud = renderTagCloudWith makeLink (intercalate " ")+  where+    makeLink minSize maxSize tag url count min' max' =+        -- Show the relative size of one 'count' in percent+        let diff     = 1 + fromIntegral max' - fromIntegral min'+            relative = (fromIntegral count - fromIntegral min') / diff+            size     = floor $ minSize + relative * (maxSize - minSize) :: Int+        in renderHtml $+            H.a ! A.style (toValue $ "font-size: " ++ show size ++ "%")+                ! A.href (toValue url)+                $ toHtml tag+++--------------------------------------------------------------------------------+-- | Render a tag cloud in HTML+renderTagCloudWith :: (Double -> Double ->+                       String -> String -> Int -> Int -> Int -> String)+                   -- ^ Render a single tag link+                   -> ([String] -> String)+                   -- ^ Concatenate links+                   -> Double+                   -- ^ Smallest font size, in percent+                   -> Double+                   -- ^ Biggest font size, in percent+                   -> Tags+                   -- ^ Input tags+                   -> Compiler String+                   -- ^ Rendered cloud+renderTagCloudWith makeLink cat minSize maxSize =+  renderTags (makeLink minSize maxSize) cat+++--------------------------------------------------------------------------------+-- | Render a tag cloud in HTML as a context+tagCloudField :: String+               -- ^ Destination key+               -> Double+               -- ^ Smallest font size, in percent+               -> Double+               -- ^ Biggest font size, in percent+               -> Tags+               -- ^ Input tags+               -> Context a+               -- ^ Context+tagCloudField key minSize maxSize tags =+  field key $ \_ -> renderTagCloud minSize maxSize tags+++--------------------------------------------------------------------------------+-- | Render a tag cloud in HTML as a context+tagCloudFieldWith :: String+                  -- ^ Destination key+                  -> (Double -> Double ->+                      String -> String -> Int -> Int -> Int -> String)+                  -- ^ Render a single tag link+                  -> ([String] -> String)+                  -- ^ Concatenate links+                  -> Double+                  -- ^ Smallest font size, in percent+                  -> Double+                  -- ^ Biggest font size, in percent+                  -> Tags+                  -- ^ Input tags+                  -> Context a+                  -- ^ Context+tagCloudFieldWith key makeLink cat minSize maxSize tags =+  field key $ \_ -> renderTagCloudWith makeLink cat minSize maxSize tags+++--------------------------------------------------------------------------------+-- | Render a simple tag list in HTML, with the tag count next to the item+-- TODO: Maybe produce a Context here+renderTagList :: Tags -> Compiler (String)+renderTagList = renderTags makeLink (intercalate ", ")+  where+    makeLink tag url count _ _ = renderHtml $+        H.a ! A.href (toValue url) $ toHtml (tag ++ " (" ++ show count ++ ")")+++--------------------------------------------------------------------------------+-- | Render tags with links with custom functions to get tags and to+-- render links+tagsFieldWith :: (Identifier -> Compiler [String])+              -- ^ Get the tags+              -> (String -> (Maybe FilePath) -> Maybe H.Html)+              -- ^ Render link for one tag+              -> ([H.Html] -> H.Html)+              -- ^ Concatenate tag links+              -> String+              -- ^ Destination field+              -> Tags+              -- ^ Tags structure+              -> Context a+              -- ^ Resulting context+tagsFieldWith getTags' renderLink cat key tags = field key $ \item -> do+    tags' <- getTags' $ itemIdentifier item+    links <- forM tags' $ \tag -> do+        route' <- getRoute $ tagsMakeId tags tag+        return $ renderLink tag route'++    return $ renderHtml $ cat $ catMaybes $ links+++--------------------------------------------------------------------------------+-- | Render tags with links+tagsField :: String     -- ^ Destination key+          -> Tags       -- ^ Tags+          -> Context a  -- ^ Context+tagsField =+  tagsFieldWith getTags simpleRenderLink (mconcat . intersperse ", ")+++--------------------------------------------------------------------------------+-- | Render the category in a link+categoryField :: String     -- ^ Destination key+              -> Tags       -- ^ Tags+              -> Context a  -- ^ Context+categoryField =+  tagsFieldWith getCategory simpleRenderLink (mconcat . intersperse ", ")+++--------------------------------------------------------------------------------+-- | Render one tag link+simpleRenderLink :: String -> (Maybe FilePath) -> Maybe H.Html+simpleRenderLink _   Nothing         = Nothing+simpleRenderLink tag (Just filePath) =+  Just $ H.a ! A.href (toValue $ toUrl filePath) $ toHtml tag+++--------------------------------------------------------------------------------+-- | Sort tags using supplied function. First element of the tuple passed to+-- the comparing function is the actual tag name.+sortTagsBy :: ((String, [Identifier]) -> (String, [Identifier]) -> Ordering)+           -> Tags -> Tags+sortTagsBy f t = t {tagsMap = sortBy f (tagsMap t)}+++--------------------------------------------------------------------------------+-- | Sample sorting function that compares tags case insensitively.+caseInsensitiveTags :: (String, [Identifier]) -> (String, [Identifier])+                    -> Ordering+caseInsensitiveTags = comparing $ map toLower . fst
+ lib/Hakyll/Web/Template.hs view
@@ -0,0 +1,154 @@+-- | This module provides means for reading and applying 'Template's.+--+-- Templates are tools to convert items into a string. They are perfectly suited+-- for laying out your site.+--+-- Let's look at an example template:+--+-- > <html>+-- >     <head>+-- >         <title>My crazy homepage - $title$</title>+-- >     </head>+-- >     <body>+-- >         <div id="header">+-- >             <h1>My crazy homepage - $title$</h1>+-- >         </div>+-- >         <div id="content">+-- >             $body$+-- >         </div>+-- >         <div id="footer">+-- >             By reading this you agree that I now own your soul+-- >         </div>+-- >     </body>+-- > </html>+--+-- As you can see, the format is very simple -- @$key$@ is used to render the+-- @$key$@ field from the page, everything else is literally copied. If you want+-- to literally insert @\"$key$\"@ into your page (for example, when you're+-- writing a Hakyll tutorial) you can use+--+-- > <p>+-- >     A literal $$key$$.+-- > </p>+--+-- Because of it's simplicity, these templates can be used for more than HTML:+-- you could make, for example, CSS or JS templates as well.+--+-- Apart from interpolating @$key$@s from the 'Context' you can also+-- use the following macros:+--+-- * @$if(key)$@+--+-- > $if(key)$+-- >  <b> Defined </b>+-- > $else$+-- >  <b> Non-defined </b>+-- > $endif$+--+-- This example will print @Defined@ if @key@ is defined in the+-- context and @Non-defined@ otherwise. The @$else$@ clause is+-- optional.+--+-- * @$for(key)$@+--+-- The @for@ macro is used for enumerating 'Context' elements that are+-- lists, i.e. constructed using the 'listField' function. Assume that+-- in a context we have an element @listField \"key\" c itms@. Then+-- the snippet+--+-- > $for(key)$+-- >   $x$+-- > $sep$,+-- > $endfor$+--+-- would, for each item @i@ in 'itms', lookup @$x$@ in the context @c@+-- with item @i@, interpolate it, and join the resulting list with+-- @,@.+--+-- Another concrete example one may consider is the following. Given the+-- context+--+-- > listField "things" (field "thing" (return . itemBody))+-- >    (sequence [makeItem "fruits", makeItem "vegetables"])+--+-- and a template+--+-- >  I like+-- >  $for(things)$+-- >    fresh $thing$$sep$, and+-- >  $endfor$+--+-- the resulting page would look like+--+-- > <p>+-- >  I like+-- >+-- >   fresh fruits, and+-- >+-- >   fresh vegetables+-- > </p>+--+-- The @$sep$@ part can be omitted. Usually, you can get by using the+-- 'applyListTemplate' and 'applyJoinListTemplate' functions.+--+-- * @$partial(path)$@+--+-- Loads a template located in a separate file and interpolates it+-- under the current context.+--+-- Assuming that the file @test.html@ contains+--+-- > <b>$key$</b>+--+-- The result of rendering+--+-- > <p>+-- >   $partial("test.html")$+-- > </p>+--+-- is the same as the result of rendering+--+-- > <p>+-- >   <b>$key$</b>+-- > </p>+--+-- That is, calling @$partial$@ is equivalent to just copying and pasting+-- template code.+--+-- In the examples above you can see that the outputs contain a lot of leftover+-- whitespace that you may wish to remove. Using @'$-'@ or @'-$'@ instead of+-- @'$'@ in a macro strips all whitespace to the left or right of that clause+-- respectively. Given the context+--+-- > listField "counts" (field "count" (return . itemBody))+-- >    (sequence [makeItem "3", makeItem "2", makeItem "1"])+--+-- and a template+--+-- > <p>+-- >     $for(counts)-$+-- >       $count$+-- >       $-sep$...+-- >     $-endfor$+-- > </p>+--+-- the resulting page would look like+--+-- > <p>+-- >     3...2...1+-- > </p>+--+module Hakyll.Web.Template+    ( Template+    , templateBodyCompiler+    , templateCompiler+    , applyTemplate+    , loadAndApplyTemplate+    , applyAsTemplate+    , readTemplate+    , unsafeReadTemplateFile+    ) where+++--------------------------------------------------------------------------------+import           Hakyll.Web.Template.Internal
+ lib/Hakyll/Web/Template/Context.hs view
@@ -0,0 +1,379 @@+--------------------------------------------------------------------------------+{-# LANGUAGE CPP                       #-}+{-# LANGUAGE ExistentialQuantification #-}+module Hakyll.Web.Template.Context+    ( ContextField (..)+    , Context (..)+    , field+    , boolField+    , constField+    , listField+    , listFieldWith+    , functionField+    , mapContext++    , defaultContext+    , bodyField+    , metadataField+    , urlField+    , pathField+    , titleField+    , snippetField+    , dateField+    , dateFieldWith+    , getItemUTC+    , getItemModificationTime+    , modificationTimeField+    , modificationTimeFieldWith+    , teaserField+    , teaserFieldWithSeparator+    , missingField+    ) where+++--------------------------------------------------------------------------------+import           Control.Applicative           (Alternative (..))+import           Control.Monad                 (msum)+import           Data.List                     (intercalate)+import           Data.Time.Clock               (UTCTime (..))+import           Data.Time.Format              (formatTime)+import qualified Data.Time.Format              as TF+import           Data.Time.Locale.Compat       (TimeLocale, defaultTimeLocale)+import           Hakyll.Core.Compiler+import           Hakyll.Core.Compiler.Internal+import           Hakyll.Core.Identifier+import           Hakyll.Core.Item+import           Hakyll.Core.Metadata+import           Hakyll.Core.Provider+import           Hakyll.Core.Util.String       (needlePrefix, splitAll)+import           Hakyll.Web.Html+import           System.FilePath               (splitDirectories, takeBaseName)+++--------------------------------------------------------------------------------+-- | Mostly for internal usage+data ContextField+    = StringField String+    | forall a. ListField (Context a) [Item a]+++--------------------------------------------------------------------------------+-- | The 'Context' monoid. Please note that the order in which you+-- compose the items is important. For example in+--+-- > field "A" f1 <> field "A" f2+--+-- the first context will overwrite the second. This is especially+-- important when something is being composed with+-- 'metadataField' (or 'defaultContext'). If you want your context to be+-- overwritten by the metadata fields, compose it from the right:+--+-- @+-- 'metadataField' \<\> field \"date\" fDate+-- @+--+newtype Context a = Context+    { unContext :: String -> [String] -> Item a -> Compiler ContextField+    }+++--------------------------------------------------------------------------------+instance Monoid (Context a) where+    mempty                          = missingField+    mappend (Context f) (Context g) = Context $ \k a i -> f k a i <|> g k a i+++--------------------------------------------------------------------------------+field' :: String -> (Item a -> Compiler ContextField) -> Context a+field' key value = Context $ \k _ i -> if k == key then value i else empty+++--------------------------------------------------------------------------------+-- | Constructs a new field in the 'Context.'+field+    :: String                      -- ^ Key+    -> (Item a -> Compiler String) -- ^ Function that constructs a value based+                                   -- on the item+    -> Context a+field key value = field' key (fmap StringField . value)+++--------------------------------------------------------------------------------+-- | Creates a 'field' to use with the @$if()$@ template macro.+boolField+    :: String+    -> (Item a -> Bool)+    -> Context a+boolField name f = field name (\i -> if f i+    then pure (error $ unwords ["no string value for bool field:",name])+    else empty)+++--------------------------------------------------------------------------------+-- | Creates a 'field' that does not depend on the 'Item'+constField :: String -> String -> Context a+constField key = field key . const . return+++--------------------------------------------------------------------------------+listField :: String -> Context a -> Compiler [Item a] -> Context b+listField key c xs = listFieldWith key c (const xs)+++--------------------------------------------------------------------------------+listFieldWith+    :: String -> Context a -> (Item b -> Compiler [Item a]) -> Context b+listFieldWith key c f = field' key $ fmap (ListField c) . f+++--------------------------------------------------------------------------------+functionField :: String -> ([String] -> Item a -> Compiler String) -> Context a+functionField name value = Context $ \k args i ->+    if k == name+        then StringField <$> value args i+        else empty+++--------------------------------------------------------------------------------+mapContext :: (String -> String) -> Context a -> Context a+mapContext f (Context c) = Context $ \k a i -> do+    fld <- c k a i+    case fld of+        StringField str -> return $ StringField (f str)+        ListField _ _   -> fail $+            "Hakyll.Web.Template.Context.mapContext: " +++            "can't map over a ListField!"++--------------------------------------------------------------------------------+-- | A context that allows snippet inclusion. In processed file, use as:+--+-- > ...+-- > $snippet("path/to/snippet/")$+-- > ...+--+-- The contents of the included file will not be interpolated.+--+snippetField :: Context String+snippetField = functionField "snippet" f+  where+    f [contentsPath] _ = loadBody (fromFilePath contentsPath)+    f _              i = error $+        "Too many arguments to function 'snippet()' in item " +++            show (itemIdentifier i)++--------------------------------------------------------------------------------+-- | A context that contains (in that order)+--+--     1. A @$body$@ field+--+--     2. Metadata fields+--+--     3. A @$url$@ 'urlField'+--+--     4. A @$path$@ 'pathField'+--+--     5. A @$title$@ 'titleField'+defaultContext :: Context String+defaultContext =+    bodyField     "body"     `mappend`+    metadataField            `mappend`+    urlField      "url"      `mappend`+    pathField     "path"     `mappend`+    titleField    "title"    `mappend`+    missingField+++--------------------------------------------------------------------------------+teaserSeparator :: String+teaserSeparator = "<!--more-->"+++--------------------------------------------------------------------------------+-- | Constructs a 'field' that contains the body of the item.+bodyField :: String -> Context String+bodyField key = field key $ return . itemBody+++--------------------------------------------------------------------------------+-- | Map any field to its metadata value, if present+metadataField :: Context a+metadataField = Context $ \k _ i -> do+    value <- getMetadataField (itemIdentifier i) k+    maybe empty (return . StringField) value+++--------------------------------------------------------------------------------+-- | Absolute url to the resulting item+urlField :: String -> Context a+urlField key = field key $+    fmap (maybe empty toUrl) . getRoute . itemIdentifier+++--------------------------------------------------------------------------------+-- | Filepath of the underlying file of the item+pathField :: String -> Context a+pathField key = field key $ return . toFilePath . itemIdentifier+++--------------------------------------------------------------------------------+-- | This title 'field' takes the basename of the underlying file by default+titleField :: String -> Context a+titleField = mapContext takeBaseName . pathField+++--------------------------------------------------------------------------------+-- | When the metadata has a field called @published@ in one of the+-- following formats then this function can render the date.+--+--   * @Mon, 06 Sep 2010 00:01:00 +0000@+--+--   * @Mon, 06 Sep 2010 00:01:00 UTC@+--+--   * @Mon, 06 Sep 2010 00:01:00@+--+--   * @2010-09-06T00:01:00+0000@+--+--   * @2010-09-06T00:01:00Z@+--+--   * @2010-09-06T00:01:00@+--+--   * @2010-09-06 00:01:00+0000@+--+--   * @2010-09-06 00:01:00@+--+--   * @September 06, 2010 00:01 AM@+--+-- Following date-only formats are supported too (@00:00:00@ for time is+-- assumed)+--+--   * @2010-09-06@+--+--   * @September 06, 2010@+--+-- Alternatively, when the metadata has a field called @path@ in a+-- @folder/yyyy-mm-dd-title.extension@ format (the convention for pages)+-- and no @published@ metadata field set, this function can render+-- the date. This pattern matches the file name or directory names+-- that begins with @yyyy-mm-dd@ . For example:+-- @folder//yyyy-mm-dd-title//dist//main.extension@ .+-- In case of multiple matches, the rightmost one is used.++dateField :: String     -- ^ Key in which the rendered date should be placed+          -> String     -- ^ Format to use on the date+          -> Context a  -- ^ Resulting context+dateField = dateFieldWith defaultTimeLocale+++--------------------------------------------------------------------------------+-- | This is an extended version of 'dateField' that allows you to+-- specify a time locale that is used for outputting the date. For more+-- details, see 'dateField'.+dateFieldWith :: TimeLocale  -- ^ Output time locale+              -> String      -- ^ Destination key+              -> String      -- ^ Format to use on the date+              -> Context a   -- ^ Resulting context+dateFieldWith locale key format = field key $ \i -> do+    time <- getItemUTC locale $ itemIdentifier i+    return $ formatTime locale format time+++--------------------------------------------------------------------------------+-- | Parser to try to extract and parse the time from the @published@+-- field or from the filename. See 'dateField' for more information.+-- Exported for user convenience.+getItemUTC :: MonadMetadata m+           => TimeLocale        -- ^ Output time locale+           -> Identifier        -- ^ Input page+           -> m UTCTime         -- ^ Parsed UTCTime+getItemUTC locale id' = do+    metadata <- getMetadata id'+    let tryField k fmt = lookupString k metadata >>= parseTime' fmt+        paths          = splitDirectories $ toFilePath id'++    maybe empty' return $ msum $+        [tryField "published" fmt | fmt <- formats] +++        [tryField "date"      fmt | fmt <- formats] +++        [parseTime' "%Y-%m-%d" $ intercalate "-" $ take 3 $ splitAll "-" fnCand | fnCand <- reverse paths]+  where+    empty'     = fail $ "Hakyll.Web.Template.Context.getItemUTC: " +++        "could not parse time for " ++ show id'+    parseTime' = parseTimeM True locale+    formats    =+        [ "%a, %d %b %Y %H:%M:%S %Z"+        , "%Y-%m-%dT%H:%M:%S%Z"+        , "%Y-%m-%d %H:%M:%S%Z"+        , "%Y-%m-%d"+        , "%B %e, %Y %l:%M %p"+        , "%B %e, %Y"+        , "%b %d, %Y"+        ]+++--------------------------------------------------------------------------------+-- | Get the time on which the actual file was last modified. This only works if+-- there actually is an underlying file, of couse.+getItemModificationTime+    :: Identifier+    -> Compiler UTCTime+getItemModificationTime identifier = do+    provider <- compilerProvider <$> compilerAsk+    return $ resourceModificationTime provider identifier+++--------------------------------------------------------------------------------+modificationTimeField :: String     -- ^ Key+                      -> String     -- ^ Format+                      -> Context  a -- ^ Resuting context+modificationTimeField = modificationTimeFieldWith defaultTimeLocale+++--------------------------------------------------------------------------------+modificationTimeFieldWith :: TimeLocale  -- ^ Time output locale+                          -> String      -- ^ Key+                          -> String      -- ^ Format+                          -> Context a   -- ^ Resulting context+modificationTimeFieldWith locale key fmt = field key $ \i -> do+    mtime <- getItemModificationTime $ itemIdentifier i+    return $ formatTime locale fmt mtime+++--------------------------------------------------------------------------------+-- | A context with "teaser" key which contain a teaser of the item.+-- The item is loaded from the given snapshot (which should be saved+-- in the user code before any templates are applied).+teaserField :: String           -- ^ Key to use+            -> Snapshot         -- ^ Snapshot to load+            -> Context String   -- ^ Resulting context+teaserField = teaserFieldWithSeparator teaserSeparator+++--------------------------------------------------------------------------------+-- | A context with "teaser" key which contain a teaser of the item, defined as+-- the snapshot content before the teaser separator. The item is loaded from the+-- given snapshot (which should be saved in the user code before any templates+-- are applied).+teaserFieldWithSeparator :: String           -- ^ Separator to use+                         -> String           -- ^ Key to use+                         -> Snapshot         -- ^ Snapshot to load+                         -> Context String   -- ^ Resulting context+teaserFieldWithSeparator separator key snapshot = field key $ \item -> do+    body <- itemBody <$> loadSnapshot (itemIdentifier item) snapshot+    case needlePrefix separator body of+        Nothing -> fail $+            "Hakyll.Web.Template.Context: no teaser defined for " +++            show (itemIdentifier item)+        Just t -> return t+++--------------------------------------------------------------------------------+missingField :: Context a+missingField = Context $ \k _ i -> fail $+    "Missing field $" ++ k ++ "$ in context for item " +++    show (itemIdentifier i)++parseTimeM :: Bool -> TimeLocale -> String -> String -> Maybe UTCTime+#if MIN_VERSION_time(1,5,0)+parseTimeM = TF.parseTimeM+#else+parseTimeM _ = TF.parseTime+#endif
+ lib/Hakyll/Web/Template/Internal.hs view
@@ -0,0 +1,203 @@+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+{-# LANGUAGE ScopedTypeVariables        #-}+module Hakyll.Web.Template.Internal+    ( Template (..)+    , template+    , templateBodyCompiler+    , templateCompiler+    , applyTemplate+    , applyTemplate'+    , loadAndApplyTemplate+    , applyAsTemplate+    , readTemplate+    , unsafeReadTemplateFile++    , module Hakyll.Web.Template.Internal.Element+    , module Hakyll.Web.Template.Internal.Trim+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad.Except                 (MonadError (..))+import           Data.Binary                          (Binary)+import           Data.List                            (intercalate)+import           Data.Typeable                        (Typeable)+import           GHC.Exts                             (IsString (..))+import           Prelude                              hiding (id)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Identifier+import           Hakyll.Core.Item+import           Hakyll.Core.Writable+import           Hakyll.Web.Template.Context+import           Hakyll.Web.Template.Internal.Element+import           Hakyll.Web.Template.Internal.Trim+++--------------------------------------------------------------------------------+-- | Datatype used for template substitutions.+newtype Template = Template+    { unTemplate :: [TemplateElement]+    } deriving (Show, Eq, Binary, Typeable)+++--------------------------------------------------------------------------------+instance Writable Template where+    -- Writing a template is impossible+    write _ _ = return ()+++--------------------------------------------------------------------------------+instance IsString Template where+    fromString = readTemplate+++--------------------------------------------------------------------------------+-- | Wrap the constructor to ensure trim is called.+template :: [TemplateElement] -> Template+template = Template . trim+++--------------------------------------------------------------------------------+readTemplate :: String -> Template+readTemplate = Template . trim . readTemplateElems++--------------------------------------------------------------------------------+-- | Read a template, without metadata header+templateBodyCompiler :: Compiler (Item Template)+templateBodyCompiler = cached "Hakyll.Web.Template.templateBodyCompiler" $ do+    item <- getResourceBody+    file <- getResourceFilePath+    return $ fmap (template . readTemplateElemsFile file) item++--------------------------------------------------------------------------------+-- | Read complete file contents as a template+templateCompiler :: Compiler (Item Template)+templateCompiler = cached "Hakyll.Web.Template.templateCompiler" $ do+    item <- getResourceString+    file <- getResourceFilePath+    return $ fmap (template . readTemplateElemsFile file) item+++--------------------------------------------------------------------------------+applyTemplate :: Template                -- ^ Template+              -> Context a               -- ^ Context+              -> Item a                  -- ^ Page+              -> Compiler (Item String)  -- ^ Resulting item+applyTemplate tpl context item = do+    body <- applyTemplate' (unTemplate tpl) context item+    return $ itemSetBody body item+++--------------------------------------------------------------------------------+applyTemplate'+    :: forall a.+       [TemplateElement] -- ^ Unwrapped Template+    -> Context a         -- ^ Context+    -> Item a            -- ^ Page+    -> Compiler String   -- ^ Resulting item+applyTemplate' tes context x = go tes+  where+    context' :: String -> [String] -> Item a -> Compiler ContextField+    context' = unContext (context `mappend` missingField)++    go = fmap concat . mapM applyElem++    trimError = error $ "Hakyll.Web.Template.applyTemplate: template not " +++        "fully trimmed."++    ---------------------------------------------------------------------------++    applyElem :: TemplateElement -> Compiler String++    applyElem TrimL = trimError++    applyElem TrimR = trimError++    applyElem (Chunk c) = return c++    applyElem (Expr e) = applyExpr e >>= getString e++    applyElem Escaped = return "$"++    applyElem (If e t mf) = (applyExpr e >> go t) `catchError` handler+      where+        handler _ = case mf of+            Nothing -> return ""+            Just f  -> go f++    applyElem (For e b s) = applyExpr e >>= \cf -> case cf of+        StringField _  -> fail $+            "Hakyll.Web.Template.applyTemplateWith: expected ListField but " +++            "got StringField for expr " ++ show e+        ListField c xs -> do+            sep <- maybe (return "") go s+            bs  <- mapM (applyTemplate' b c) xs+            return $ intercalate sep bs++    applyElem (Partial e) = do+        p             <- applyExpr e >>= getString e+        Template tpl' <- loadBody (fromFilePath p)+        applyTemplate' tpl' context x++    ---------------------------------------------------------------------------++    applyExpr :: TemplateExpr -> Compiler ContextField++    applyExpr (Ident (TemplateKey k)) = context' k [] x++    applyExpr (Call (TemplateKey k) args) = do+        args' <- mapM (\e -> applyExpr e >>= getString e) args+        context' k args' x++    applyExpr (StringLiteral s) = return (StringField s)++    ----------------------------------------------------------------------------++    getString _ (StringField s) = return s+    getString e (ListField _ _) = fail $+        "Hakyll.Web.Template.applyTemplateWith: expected StringField but " +++        "got ListField for expr " ++ show e+++--------------------------------------------------------------------------------+-- | The following pattern is so common:+--+-- > tpl <- loadBody "templates/foo.html"+-- > someCompiler+-- >     >>= applyTemplate tpl context+--+-- That we have a single function which does this:+--+-- > someCompiler+-- >     >>= loadAndApplyTemplate "templates/foo.html" context+loadAndApplyTemplate :: Identifier              -- ^ Template identifier+                     -> Context a               -- ^ Context+                     -> Item a                  -- ^ Page+                     -> Compiler (Item String)  -- ^ Resulting item+loadAndApplyTemplate identifier context item = do+    tpl <- loadBody identifier+    applyTemplate tpl context item+++--------------------------------------------------------------------------------+-- | It is also possible that you want to substitute @$key$@s within the body of+-- an item. This function does that by interpreting the item body as a template,+-- and then applying it to itself.+applyAsTemplate :: Context String          -- ^ Context+                -> Item String             -- ^ Item and template+                -> Compiler (Item String)  -- ^ Resulting item+applyAsTemplate context item =+    let tpl = template $ readTemplateElemsFile file (itemBody item)+        file = toFilePath $ itemIdentifier item+    in applyTemplate tpl context item+++--------------------------------------------------------------------------------+unsafeReadTemplateFile :: FilePath -> Compiler Template+unsafeReadTemplateFile file = do+    tpl <- unsafeCompiler $ readFile file+    pure $ template $ readTemplateElemsFile file tpl+
+ lib/Hakyll/Web/Template/Internal/Element.hs view
@@ -0,0 +1,298 @@+--------------------------------------------------------------------------------+-- | Module containing the elements used in a template.  A template is generally+-- just a list of these elements.+{-# LANGUAGE GeneralizedNewtypeDeriving #-}+module Hakyll.Web.Template.Internal.Element+    ( TemplateKey (..)+    , TemplateExpr (..)+    , TemplateElement (..)+    , templateElems+    , readTemplateElems+    , readTemplateElemsFile+    ) where+++--------------------------------------------------------------------------------+import           Control.Applicative     ((<|>))+import           Control.Monad           (void)+import           Data.Binary             (Binary, get, getWord8, put, putWord8)+import           Data.List               (intercalate)+import           Data.Maybe              (isJust)+import           Data.Typeable           (Typeable)+import           GHC.Exts                (IsString (..))+import qualified Text.Parsec             as P+import qualified Text.Parsec.String      as P+++--------------------------------------------------------------------------------+import           Hakyll.Core.Util.Parser+++--------------------------------------------------------------------------------+newtype TemplateKey = TemplateKey String+    deriving (Binary, Show, Eq, Typeable)+++--------------------------------------------------------------------------------+instance IsString TemplateKey where+    fromString = TemplateKey+++--------------------------------------------------------------------------------+-- | Elements of a template.+data TemplateElement+    = Chunk String+    | Expr TemplateExpr+    | Escaped+      -- expr, then, else+    | If TemplateExpr [TemplateElement] (Maybe [TemplateElement])+      -- expr, body, separator+    | For TemplateExpr [TemplateElement] (Maybe [TemplateElement])+      -- filename+    | Partial TemplateExpr+    | TrimL+    | TrimR+    deriving (Show, Eq, Typeable)+++--------------------------------------------------------------------------------+instance Binary TemplateElement where+    put (Chunk string) = putWord8 0 >> put string+    put (Expr e)       = putWord8 1 >> put e+    put  Escaped       = putWord8 2+    put (If e t f)     = putWord8 3 >> put e >> put t >> put f+    put (For e b s)    = putWord8 4 >> put e >> put b >> put s+    put (Partial e)    = putWord8 5 >> put e+    put  TrimL         = putWord8 6+    put  TrimR         = putWord8 7++    get = getWord8 >>= \tag -> case tag of+        0 -> Chunk <$> get+        1 -> Expr <$> get+        2 -> pure Escaped+        3 -> If <$> get <*> get <*> get+        4 -> For <$> get <*> get <*> get+        5 -> Partial <$> get+        6 -> pure TrimL+        7 -> pure TrimR+        _ -> error "Hakyll.Web.Template.Internal: Error reading cached template"+++--------------------------------------------------------------------------------+-- | Expression in a template+data TemplateExpr+    = Ident TemplateKey+    | Call TemplateKey [TemplateExpr]+    | StringLiteral String+    deriving (Eq, Typeable)+++--------------------------------------------------------------------------------+instance Show TemplateExpr where+    show (Ident (TemplateKey k))   = k+    show (Call (TemplateKey k) as) =+        k ++ "(" ++ intercalate ", " (map show as) ++ ")"+    show (StringLiteral s)         = show s+++--------------------------------------------------------------------------------+instance Binary TemplateExpr where+    put (Ident k)         = putWord8 0 >> put k+    put (Call k as)       = putWord8 1 >> put k >> put as+    put (StringLiteral s) = putWord8 2 >> put s++    get = getWord8 >>= \tag -> case tag of+        0 -> Ident         <$> get+        1 -> Call          <$> get <*> get+        2 -> StringLiteral <$> get+        _ -> error "Hakyll.Web.Template.Internal: Error reading cached template"+++--------------------------------------------------------------------------------+readTemplateElems :: String -> [TemplateElement]+readTemplateElems = readTemplateElemsFile "{literal}"+++--------------------------------------------------------------------------------+readTemplateElemsFile :: FilePath -> String -> [TemplateElement]+readTemplateElemsFile file input = case P.parse templateElems file input of+    Left err -> error $ "Cannot parse template: " ++ show err+    Right t  -> t+++--------------------------------------------------------------------------------+templateElems :: P.Parser [TemplateElement]+templateElems = mconcat <$> P.many (P.choice [ lift chunk+                                             , lift escaped+                                             , conditional+                                             , for+                                             , partial+                                             , expr+                                             ])+    where lift = fmap (:[])+++--------------------------------------------------------------------------------+chunk :: P.Parser TemplateElement+chunk = Chunk <$> P.many1 (P.noneOf "$")+++--------------------------------------------------------------------------------+expr :: P.Parser [TemplateElement]+expr = P.try $ do+    trimLExpr <- trimOpen+    e <- expr'+    trimRExpr <- trimClose+    return $ [TrimL | trimLExpr] ++ [Expr e] ++ [TrimR | trimRExpr]+++--------------------------------------------------------------------------------+expr' :: P.Parser TemplateExpr+expr' = stringLiteral <|> call <|> ident+++--------------------------------------------------------------------------------+escaped :: P.Parser TemplateElement+escaped = Escaped <$ P.try (P.string "$$")+++--------------------------------------------------------------------------------+trimOpen :: P.Parser Bool+trimOpen = do+    void $ P.char '$'+    trimLIf <- P.optionMaybe $ P.try (P.char '-')+    pure $ isJust trimLIf+++--------------------------------------------------------------------------------+trimClose :: P.Parser Bool+trimClose = do+    trimIfR <- P.optionMaybe $ P.try (P.char '-')+    void $ P.char '$'+    pure $ isJust trimIfR+++--------------------------------------------------------------------------------+conditional :: P.Parser [TemplateElement]+conditional = P.try $ do+    -- if+    trimLIf <- trimOpen+    void $ P.string "if("+    e <- expr'+    void $ P.char ')'+    trimRIf <- trimClose+    -- then+    thenBranch <- templateElems+    -- else+    elseParse <- opt "else"+    -- endif+    trimLEnd <- trimOpen+    void $ P.string "endif"+    trimREnd <- trimClose++    -- As else is optional we need to sort out where any Trim_s need to go.+    let (thenBody, elseBody) = maybe (thenNoElse, Nothing) thenElse elseParse+            where thenNoElse =+                      [TrimR | trimRIf] ++ thenBranch ++ [TrimL | trimLEnd]++                  thenElse (trimLElse, elseBranch, trimRElse) = (thenB, elseB)+                      where thenB = [TrimR | trimRIf]+                                 ++ thenBranch+                                 ++ [TrimL | trimLElse]++                            elseB = Just $ [TrimR | trimRElse]+                                        ++ elseBranch+                                        ++ [TrimL | trimLEnd]++    pure $ [TrimL | trimLIf] ++ [If e thenBody elseBody] ++ [TrimR | trimREnd]+++--------------------------------------------------------------------------------+for :: P.Parser [TemplateElement]+for = P.try $ do+    -- for+    trimLFor <- trimOpen+    void $ P.string "for("+    e <- expr'+    void $ P.char ')'+    trimRFor <- trimClose+    -- body+    bodyBranch <- templateElems+    -- sep+    sepParse <- opt "sep"+    -- endfor+    trimLEnd <- trimOpen+    void $ P.string "endfor"+    trimREnd <- trimClose++    -- As sep is optional we need to sort out where any Trim_s need to go.+    let (forBody, sepBody) = maybe (forNoSep, Nothing) forSep sepParse+            where forNoSep =+                      [TrimR | trimRFor] ++ bodyBranch ++ [TrimL | trimLEnd]++                  forSep (trimLSep, sepBranch, trimRSep) = (forB, sepB)+                      where forB = [TrimR | trimRFor]+                                ++ bodyBranch+                                ++ [TrimL | trimLSep]++                            sepB = Just $ [TrimR | trimRSep]+                                       ++ sepBranch+                                       ++ [TrimL | trimLEnd]++    pure $ [TrimL | trimLFor] ++ [For e forBody sepBody] ++ [TrimR | trimREnd]+++--------------------------------------------------------------------------------+partial :: P.Parser [TemplateElement]+partial = P.try $ do+    trimLPart <- trimOpen+    void $ P.string "partial("+    e <- expr'+    void $ P.char ')'+    trimRPart <- trimClose++    pure $ [TrimL | trimLPart] ++ [Partial e] ++ [TrimR | trimRPart]+++--------------------------------------------------------------------------------+ident :: P.Parser TemplateExpr+ident = P.try $ Ident <$> key+++--------------------------------------------------------------------------------+call :: P.Parser TemplateExpr+call = P.try $ do+    f <- key+    void $ P.char '('+    P.spaces+    as <- P.sepBy expr' (P.spaces >> P.char ',' >> P.spaces)+    P.spaces+    void $ P.char ')'+    return $ Call f as+++--------------------------------------------------------------------------------+stringLiteral :: P.Parser TemplateExpr+stringLiteral = do+    void $ P.char '\"'+    str <- P.many $ do+        x <- P.noneOf "\""+        if x == '\\' then P.anyChar else return x+    void $ P.char '\"'+    return $ StringLiteral str+++--------------------------------------------------------------------------------+key :: P.Parser TemplateKey+key = TemplateKey <$> metadataKey+++--------------------------------------------------------------------------------+opt :: String -> P.Parser (Maybe (Bool, [TemplateElement], Bool))+opt clause = P.optionMaybe $ P.try $ do+    trimL <- trimOpen+    void $ P.string clause+    trimR <- trimClose+    branch <- templateElems+    pure (trimL, branch, trimR)+
+ lib/Hakyll/Web/Template/Internal/Trim.hs view
@@ -0,0 +1,95 @@+--------------------------------------------------------------------------------+-- | Module for trimming whitespace from tempaltes.+module Hakyll.Web.Template.Internal.Trim+    ( trim+    ) where+++--------------------------------------------------------------------------------+import           Data.Char                            (isSpace)+import           Data.List                            (dropWhileEnd)+++--------------------------------------------------------------------------------+import           Hakyll.Web.Template.Internal.Element+++--------------------------------------------------------------------------------+trim :: [TemplateElement] -> [TemplateElement]+trim = cleanse . canonicalize+++--------------------------------------------------------------------------------+-- | Apply the Trim nodes to the Chunks.+cleanse :: [TemplateElement] -> [TemplateElement]+cleanse = recurse cleanse . process+    where process [] = []+          process (TrimR:Chunk str:ts) = let str' = dropWhile isSpace str+                                         in if null str'+                                                then process ts+                                                -- Might need to TrimL.+                                                else process $ Chunk str':ts++          process (Chunk str:TrimL:ts) = let str' = dropWhileEnd isSpace str+                                         in if null str'+                                                then process ts+                                                else Chunk str':process ts++          process (t:ts) = t:process ts++--------------------------------------------------------------------------------+-- | Enforce the invariant that:+--+--     * Every 'TrimL' has a 'Chunk' to its left.+--     * Every 'TrimR' has a 'Chunk' to its right.+--+canonicalize :: [TemplateElement] -> [TemplateElement]+canonicalize = go+    where go t = let t' = redundant . swap $ dedupe t+                 in if t == t' then t else go t'+++--------------------------------------------------------------------------------+-- | Remove the 'TrimR' and 'TrimL's that are no-ops.+redundant :: [TemplateElement] -> [TemplateElement]+redundant = recurse redundant . process+    where -- Remove the leading 'TrimL's.+          process (TrimL:ts) = process ts+          -- Remove trailing 'TrimR's.+          process ts = foldr trailing [] ts+              where trailing TrimR [] = []+                    trailing x xs     = x:xs+++--------------------------------------------------------------------------------+-- >>> swap $ [TrimR, TrimL]+-- [TrimL, TrimR]+swap :: [TemplateElement] -> [TemplateElement]+swap = recurse swap . process+    where process []               = []+          process (TrimR:TrimL:ts) = TrimL:process (TrimR:ts)+          process (t:ts)           = t:process ts+++--------------------------------------------------------------------------------+-- | Remove 'TrimR' and 'TrimL' duplication.+dedupe :: [TemplateElement] -> [TemplateElement]+dedupe = recurse dedupe . process+    where process []               = []+          process (TrimR:TrimR:ts) = process (TrimR:ts)+          process (TrimL:TrimL:ts) = process (TrimL:ts)+          process (t:ts)           = t:process ts+++--------------------------------------------------------------------------------+-- | @'recurse' f t@ applies f to every '[TemplateElement]' in t.+recurse :: ([TemplateElement] -> [TemplateElement])+        -> [TemplateElement]+        -> [TemplateElement]+recurse _ []     = []+recurse f (x:xs) = process x:recurse f xs+    where process y = case y of+                          If e tb eb -> If e (f tb) (f <$> eb)+                          For e t s  -> For e (f t) (f <$> s)+                          _          -> y+
+ lib/Hakyll/Web/Template/List.hs view
@@ -0,0 +1,91 @@+--------------------------------------------------------------------------------+-- | Provides an easy way to combine several items in a list. The applications+-- are obvious:+--+-- * A post list on a blog+--+-- * An image list in a gallery+--+-- * A sitemap+{-# LANGUAGE TupleSections #-}+module Hakyll.Web.Template.List+    ( applyTemplateList+    , applyJoinTemplateList+    , chronological+    , recentFirst+    , sortChronological+    , sortRecentFirst+    ) where+++--------------------------------------------------------------------------------+import           Control.Monad               (liftM)+import           Data.List                   (intersperse, sortBy)+import           Data.Ord                    (comparing)+import           Data.Time.Locale.Compat     (defaultTimeLocale)+++--------------------------------------------------------------------------------+import           Hakyll.Core.Compiler+import           Hakyll.Core.Identifier+import           Hakyll.Core.Item+import           Hakyll.Core.Metadata+import           Hakyll.Web.Template+import           Hakyll.Web.Template.Context+++--------------------------------------------------------------------------------+-- | Generate a string of a listing of pages, after applying a template to each+-- page.+applyTemplateList :: Template+                  -> Context a+                  -> [Item a]+                  -> Compiler String+applyTemplateList = applyJoinTemplateList ""+++--------------------------------------------------------------------------------+-- | Join a listing of pages with a string in between, after applying a template+-- to each page.+applyJoinTemplateList :: String+                      -> Template+                      -> Context a+                      -> [Item a]+                      -> Compiler String+applyJoinTemplateList delimiter tpl context items = do+    items' <- mapM (applyTemplate tpl context) items+    return $ concat $ intersperse delimiter $ map itemBody items'+++--------------------------------------------------------------------------------+-- | Sort pages chronologically. Uses the same method as 'dateField' for+-- extracting the date.+chronological :: MonadMetadata m => [Item a] -> m [Item a]+chronological =+    sortByM $ getItemUTC defaultTimeLocale . itemIdentifier+  where+    sortByM :: (Monad m, Ord k) => (a -> m k) -> [a] -> m [a]+    sortByM f xs = liftM (map fst . sortBy (comparing snd)) $+                   mapM (\x -> liftM (x,) (f x)) xs+++--------------------------------------------------------------------------------+-- | The reverse of 'chronological'+recentFirst :: MonadMetadata m => [Item a] -> m [Item a]+recentFirst = liftM reverse . chronological+++--------------------------------------------------------------------------------+-- | Version of 'chronological' which doesn't need the actual items.+sortChronological+    :: MonadMetadata m => [Identifier] -> m [Identifier]+sortChronological ids =+    liftM (map itemIdentifier) $ chronological [Item i () | i <- ids]+++--------------------------------------------------------------------------------+-- | Version of 'recentFirst' which doesn't need the actual items.+sortRecentFirst+    :: MonadMetadata m => [Identifier] -> m [Identifier]+sortRecentFirst ids =+    liftM (map itemIdentifier) $ recentFirst [Item i () | i <- ids]
− src/Data/List/Extended.hs
@@ -1,15 +0,0 @@-module Data.List.Extended-    ( module Data.List-    , breakWhen-    ) where--import           Data.List---- | Like 'break', but can act on the entire tail of the list.-breakWhen :: ([a] -> Bool) -> [a] -> ([a], [a])-breakWhen predicate = go []-  where-    go buf []                = (reverse buf, [])-    go buf (x : xs)-        | predicate (x : xs) = (reverse buf, x : xs)-        | otherwise          = go (x : buf) xs
− src/Data/Yaml/Extended.hs
@@ -1,24 +0,0 @@-module Data.Yaml.Extended-    ( module Data.Yaml-    , toString-    , toList-    ) where--import qualified Data.Text   as T-import qualified Data.Vector as V-import           Data.Yaml-import           Data.Scientific--toString :: Value -> Maybe String-toString (String t)     = Just (T.unpack t)-toString (Bool   True)  = Just "true"-toString (Bool   False) = Just "false"--- | Make sure that numeric fields containing integer numbers are shown as--- | integers (i.e., "42" instead of "42.0").-toString (Number d) | isInteger d = Just (formatScientific Fixed (Just 0) d)-                    | otherwise   = Just (show d)-toString _              = Nothing--toList :: Value -> Maybe [Value]-toList (Array a) = Just (V.toList a)-toList _         = Nothing
− src/Hakyll.hs
@@ -1,62 +0,0 @@------------------------------------------------------------------------------------ | Top-level module exporting all modules that are interesting for the user-{-# LANGUAGE CPP #-}-module Hakyll-    ( module Hakyll.Core.Compiler-    , module Hakyll.Core.Configuration-    , module Hakyll.Core.File-    , module Hakyll.Core.Identifier-    , module Hakyll.Core.Identifier.Pattern-    , module Hakyll.Core.Item-    , module Hakyll.Core.Metadata-    , module Hakyll.Core.Routes-    , module Hakyll.Core.Rules-    , module Hakyll.Core.UnixFilter-    , module Hakyll.Core.Util.File-    , module Hakyll.Core.Util.String-    , module Hakyll.Core.Writable-    , module Hakyll.Main-    , module Hakyll.Web.CompressCss-    , module Hakyll.Web.Feed-    , module Hakyll.Web.Html-    , module Hakyll.Web.Html.RelativizeUrls-    , module Hakyll.Web.Pandoc-    , module Hakyll.Web.Paginate-    , module Hakyll.Web.Pandoc.Biblio-    , module Hakyll.Web.Pandoc.FileType-    , module Hakyll.Web.Redirect-    , module Hakyll.Web.Tags-    , module Hakyll.Web.Template-    , module Hakyll.Web.Template.Context-    , module Hakyll.Web.Template.List-    ) where------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Configuration-import           Hakyll.Core.File-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Item-import           Hakyll.Core.Metadata-import           Hakyll.Core.Routes-import           Hakyll.Core.Rules-import           Hakyll.Core.UnixFilter-import           Hakyll.Core.Util.File-import           Hakyll.Core.Util.String-import           Hakyll.Core.Writable-import           Hakyll.Main-import           Hakyll.Web.CompressCss-import           Hakyll.Web.Feed-import           Hakyll.Web.Html-import           Hakyll.Web.Html.RelativizeUrls-import           Hakyll.Web.Paginate-import           Hakyll.Web.Pandoc-import           Hakyll.Web.Pandoc.Biblio-import           Hakyll.Web.Pandoc.FileType-import           Hakyll.Web.Redirect-import           Hakyll.Web.Tags-import           Hakyll.Web.Template-import           Hakyll.Web.Template.Context-import           Hakyll.Web.Template.List
− src/Hakyll/Check.hs
@@ -1,290 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE CPP               #-}-{-# LANGUAGE OverloadedStrings #-}-module Hakyll.Check-    ( Check (..)-    , check-    ) where------------------------------------------------------------------------------------import           Control.Concurrent.MVar      (MVar, newEmptyMVar, putMVar,-                                               readMVar)-import           Control.Exception            (SomeAsyncException (..),-                                               SomeException (..), throw, try)-import           Control.Monad                (foldM, forM_)-import           Control.Monad.Reader         (ReaderT, ask, runReaderT)-import           Control.Monad.State          (StateT, get, modify, runStateT)-import           Control.Monad.Trans          (liftIO)-import           Control.Monad.Trans.Resource (runResourceT)-import           Data.ByteString.Char8        (unpack)-import           Data.List                    (isPrefixOf)-import qualified Data.Map.Lazy                as Map-import           Network.URI                  (unEscapeString)-import           System.Directory             (doesDirectoryExist,-                                               doesFileExist)-import           System.Exit                  (ExitCode (..))-import           System.FilePath              (takeDirectory, takeExtension,-                                               (</>))-import qualified Text.HTML.TagSoup            as TS------------------------------------------------------------------------------------#ifdef CHECK_EXTERNAL-import           Data.List                    (intercalate)-import           Data.Typeable                (cast)-import           Data.Version                 (versionBranch)-import           GHC.Exts                     (fromString)-import qualified Network.HTTP.Conduit         as Http-import qualified Network.HTTP.Types           as Http-import qualified Paths_hakyll                 as Paths_hakyll-#endif------------------------------------------------------------------------------------import           Hakyll.Core.Configuration-import           Hakyll.Core.Logger           (Logger)-import qualified Hakyll.Core.Logger           as Logger-import           Hakyll.Core.Util.File-import           Hakyll.Web.Html------------------------------------------------------------------------------------data Check = All | InternalLinks-    deriving (Eq, Ord, Show)------------------------------------------------------------------------------------check :: Configuration -> Logger -> Check -> IO ExitCode-check config logger check' = do-    ((), state) <- runChecker checkDestination config logger check'-    failed <- countFailedLinks state-    return $ if failed > 0 then ExitFailure 1 else ExitSuccess------------------------------------------------------------------------------------countFailedLinks :: CheckerState -> IO Int-countFailedLinks state = foldM addIfFailure 0 (Map.elems state)-    where addIfFailure failures mvar = do-              checkerWrite <- readMVar mvar-              return $ failures + checkerFaulty checkerWrite------------------------------------------------------------------------------------data CheckerRead = CheckerRead-    { checkerConfig :: Configuration-    , checkerLogger :: Logger-    , checkerCheck  :: Check-    }------------------------------------------------------------------------------------data CheckerWrite = CheckerWrite-    { checkerFaulty :: Int-    , checkerOk     :: Int-    } deriving (Show)------------------------------------------------------------------------------------instance Monoid CheckerWrite where-    mempty                                            = CheckerWrite 0 0-    mappend (CheckerWrite f1 o1) (CheckerWrite f2 o2) =-        CheckerWrite (f1 + f2) (o1 + o2)------------------------------------------------------------------------------------type CheckerState = Map.Map URL (MVar CheckerWrite)------------------------------------------------------------------------------------type Checker a = ReaderT CheckerRead (StateT CheckerState IO) a------------------------------------------------------------------------------------type URL = String------------------------------------------------------------------------------------runChecker :: Checker a -> Configuration -> Logger -> Check-           -> IO (a, CheckerState)-runChecker checker config logger check' = do-    let read' = CheckerRead-                    { checkerConfig = config-                    , checkerLogger = logger-                    , checkerCheck  = check'-                    }-    Logger.flush logger-    runStateT (runReaderT checker read') Map.empty------------------------------------------------------------------------------------checkDestination :: Checker ()-checkDestination = do-    config <- checkerConfig <$> ask-    files  <- liftIO $ getRecursiveContents-        (const $ return False) (destinationDirectory config)--    let htmls =-            [ destinationDirectory config </> file-            | file <- files-            , takeExtension file == ".html"-            ]--    forM_ htmls checkFile------------------------------------------------------------------------------------checkFile :: FilePath -> Checker ()-checkFile filePath = do-    logger   <- checkerLogger <$> ask-    contents <- liftIO $ readFile filePath-    Logger.header logger $ "Checking file " ++ filePath--    let urls = getUrls $ TS.parseTags contents-    forM_ urls $ \url -> do-        Logger.debug logger $ "Checking link " ++ url-        m <- liftIO newEmptyMVar-        checkUrlIfNeeded filePath (canonicalizeUrl url) m-    where-        -- Check scheme-relative links-        canonicalizeUrl url = if schemeRelative url then "http:" ++ url else url-        schemeRelative = isPrefixOf "//"------------------------------------------------------------------------------------checkUrlIfNeeded :: FilePath -> URL -> MVar CheckerWrite -> Checker ()-checkUrlIfNeeded filepath url m = do-    logger     <- checkerLogger           <$> ask-    needsCheck <- (== All) . checkerCheck <$> ask-    checked    <- (url `Map.member`)      <$> get-    if not needsCheck || checked-        then Logger.debug logger "Already checked, skipping"-        else do modify $ Map.insert url m-                checkUrl filepath url------------------------------------------------------------------------------------checkUrl :: FilePath -> URL -> Checker ()-checkUrl filePath url-    | isExternal url  = checkExternalUrl url-    | hasProtocol url = skip url $ Just "Unknown protocol, skipping"-    | otherwise       = checkInternalUrl filePath url-  where-    validProtoChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "+-."-    hasProtocol str = case break (== ':') str of-        (proto, ':' : _) -> all (`elem` validProtoChars) proto-        _                -> False------------------------------------------------------------------------------------ok :: URL -> Checker ()-ok url = putCheckResult url mempty {checkerOk = 1}------------------------------------------------------------------------------------skip :: URL -> Maybe String -> Checker ()-skip url maybeReason = do-    logger <- checkerLogger <$> ask-    case maybeReason of-        Nothing     -> return ()-        Just reason -> Logger.debug logger reason-    putCheckResult url mempty {checkerOk = 1}------------------------------------------------------------------------------------faulty :: URL -> Maybe String -> Checker ()-faulty url reason = do-    logger <- checkerLogger <$> ask-    Logger.error logger $ "Broken link to " ++ show url ++ explanation-    putCheckResult url mempty {checkerFaulty = 1}-  where-    formatExplanation = (" (" ++) . (++ ")")-    explanation = maybe "" formatExplanation reason------------------------------------------------------------------------------------putCheckResult :: URL -> CheckerWrite -> Checker ()-putCheckResult url result = do-    state <- get-    let maybeMVar = Map.lookup url state-    case maybeMVar of-        Just m -> liftIO $ putMVar m result-        Nothing -> do-            logger <- checkerLogger <$> ask-            Logger.debug logger "Failed to find existing entry for checked URL"------------------------------------------------------------------------------------checkInternalUrl :: FilePath -> URL -> Checker ()-checkInternalUrl base url = case url' of-    "" -> ok url-    _  -> do-        config <- checkerConfig <$> ask-        let dest = destinationDirectory config-            dir  = takeDirectory base-            filePath-                | "/" `isPrefixOf` url' = dest ++ url'-                | otherwise             = dir </> url'--        exists <- checkFileExists filePath-        if exists then ok url else faulty url Nothing-  where-    url' = stripFragments $ unEscapeString url------------------------------------------------------------------------------------checkExternalUrl :: URL -> Checker ()-#ifdef CHECK_EXTERNAL-checkExternalUrl url = do-    result <- requestExternalUrl url-    case result of-        Left (SomeException e) ->-            case (cast e :: Maybe SomeAsyncException) of-                Just ae -> throw ae-                _       -> faulty url (Just $ showException e)-        Right _ -> ok url-    where-        -- Convert exception to a concise form-        showException e = case cast e of-            Just (Http.HttpExceptionRequest _ e') -> show e'-            _                                     -> head $ words $ show e--requestExternalUrl :: URL -> Checker (Either SomeException Bool)-requestExternalUrl url = liftIO $ try $ do-    mgr <- Http.newManager Http.tlsManagerSettings-    runResourceT $ do-        request  <- Http.parseRequest url-        response <- Http.http (settings request) mgr-        let code = Http.statusCode (Http.responseStatus response)-        return $ code >= 200 && code < 300-    where-        -- Add additional request info-        settings r = r-            { Http.method         = "HEAD"-            , Http.redirectCount  = 10-            , Http.requestHeaders = ("User-Agent", ua) : Http.requestHeaders r-            }--        -- Nice user agent info-        ua = fromString $ "hakyll-check/" ++-             (intercalate "." $ map show $ versionBranch Paths_hakyll.version)-#else-checkExternalUrl url = skip url Nothing-#endif-------------------------------------------------------------------------------------- | Wraps doesFileExist, also checks for index.html-checkFileExists :: FilePath -> Checker Bool-checkFileExists filePath = liftIO $ do-    file <- doesFileExist filePath-    dir  <- doesDirectoryExist filePath-    case (file, dir) of-        (True, _) -> return True-        (_, True) -> doesFileExist $ filePath </> "index.html"-        _         -> return False------------------------------------------------------------------------------------stripFragments :: String -> String-stripFragments = takeWhile (not . flip elem ['?', '#'])
− src/Hakyll/Commands.hs
@@ -1,160 +0,0 @@- ----------------------------------------------------------------------------------- | Implementation of Hakyll commands: build, preview...-{-# LANGUAGE CPP #-}-module Hakyll.Commands-    ( build-    , check-    , clean-    , preview-    , rebuild-    , server-    , deploy-    , watch-    ) where------------------------------------------------------------------------------------import           Control.Concurrent-import           System.Exit                (ExitCode, exitWith)-----------------------------------------------------------------------------------import qualified Hakyll.Check               as Check-import           Hakyll.Core.Configuration-import           Hakyll.Core.Logger         (Logger)-import qualified Hakyll.Core.Logger         as Logger-import           Hakyll.Core.Rules-import           Hakyll.Core.Rules.Internal-import           Hakyll.Core.Runtime-import           Hakyll.Core.Util.File-----------------------------------------------------------------------------------#ifdef WATCH_SERVER-import           Hakyll.Preview.Poll        (watchUpdates)-#endif--#ifdef PREVIEW_SERVER-import           Hakyll.Preview.Server-#endif--#ifdef mingw32_HOST_OS-import           Control.Monad              (void)-import           System.IO.Error            (catchIOError)-#endif-------------------------------------------------------------------------------------- | Build the site-build :: Configuration -> Logger -> Rules a -> IO ExitCode-build conf logger rules = fst <$> run conf logger rules-------------------------------------------------------------------------------------- | Run the checker and exit-check :: Configuration -> Logger -> Check.Check -> IO ExitCode-check = Check.check-------------------------------------------------------------------------------------- | Remove the output directories-clean :: Configuration -> Logger -> IO ()-clean conf logger = do-    remove $ destinationDirectory conf-    remove $ storeDirectory conf-    remove $ tmpDirectory conf-  where-    remove dir = do-        Logger.header logger $ "Removing " ++ dir ++ "..."-        removeDirectory dir-------------------------------------------------------------------------------------- | Preview the site-preview :: Configuration -> Logger -> Rules a -> Int -> IO ()-#ifdef PREVIEW_SERVER-preview conf logger rules port  = do-    deprecatedMessage-    watch conf logger "0.0.0.0" port True rules-  where-    deprecatedMessage = mapM_ putStrLn [ "The preview command has been deprecated."-                                       , "Use the watch command for recompilation and serving."-                                       ]-#else-preview _ _ _ _ = previewServerDisabled-#endif-------------------------------------------------------------------------------------- | Watch and recompile for changes--watch :: Configuration -> Logger -> String -> Int -> Bool -> Rules a -> IO ()-#ifdef WATCH_SERVER-watch conf logger host port runServer rules = do-#ifndef mingw32_HOST_OS-    _ <- forkIO $ watchUpdates conf update-#else-    -- Force windows users to compile with -threaded flag, as otherwise-    -- thread is blocked indefinitely.-    catchIOError (void $ forkOS $ watchUpdates conf update) $ do-        fail $ "Hakyll.Commands.watch: Could not start update watching " ++-               "thread. Did you compile with -threaded flag?"-#endif-    server'-  where-    update = do-        (_, ruleSet) <- run conf logger rules-        return $ rulesPattern ruleSet-    loop = threadDelay 100000 >> loop-    server' = if runServer then server conf logger host port else loop-#else-watch _ _ _ _ _ _ = watchServerDisabled-#endif------------------------------------------------------------------------------------- | Rebuild the site-rebuild :: Configuration -> Logger -> Rules a -> IO ExitCode-rebuild conf logger rules =-    clean conf logger >> build conf logger rules------------------------------------------------------------------------------------- | Start a server-server :: Configuration -> Logger -> String -> Int -> IO ()-#ifdef PREVIEW_SERVER-server conf logger host port = do-    let destination = destinationDirectory conf-    staticServer logger destination host port-#else-server _ _ _ _ = previewServerDisabled-#endif-------------------------------------------------------------------------------------- | Upload the site-deploy :: Configuration -> IO ExitCode-deploy conf = deploySite conf conf-------------------------------------------------------------------------------------- | Print a warning message about the preview serving not being enabled-#ifndef PREVIEW_SERVER-previewServerDisabled :: IO ()-previewServerDisabled =-    mapM_ putStrLn-        [ "PREVIEW SERVER"-        , ""-        , "The preview server is not enabled in the version of Hakyll. To"-        , "enable it, set the flag to True and recompile Hakyll."-        , "Alternatively, use an external tool to serve your site directory."-        ]-#endif--#ifndef WATCH_SERVER-watchServerDisabled :: IO ()-watchServerDisabled =-    mapM_ putStrLn-      [ "WATCH SERVER"-      , ""-      , "The watch server is not enabled in the version of Hakyll. To"-      , "enable it, set the flag to True and recompile Hakyll."-      , "Alternatively, use an external tool to serve your site directory."-      ]-#endif
− src/Hakyll/Core/Compiler.hs
@@ -1,189 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE ScopedTypeVariables        #-}-module Hakyll.Core.Compiler-    ( Compiler-    , getUnderlying-    , getUnderlyingExtension-    , makeItem-    , getRoute-    , getResourceBody-    , getResourceString-    , getResourceLBS-    , getResourceFilePath--    , Internal.Snapshot-    , saveSnapshot-    , Internal.load-    , Internal.loadSnapshot-    , Internal.loadBody-    , Internal.loadSnapshotBody-    , Internal.loadAll-    , Internal.loadAllSnapshots--    , cached-    , unsafeCompiler-    , debugCompiler-    ) where------------------------------------------------------------------------------------import           Control.Monad                 (when, unless)-import           Data.Binary                   (Binary)-import           Data.ByteString.Lazy          (ByteString)-import           Data.Typeable                 (Typeable)-import           System.Environment            (getProgName)-import           System.FilePath               (takeExtension)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler.Internal-import qualified Hakyll.Core.Compiler.Require  as Internal-import           Hakyll.Core.Dependencies-import           Hakyll.Core.Identifier-import           Hakyll.Core.Item-import           Hakyll.Core.Logger            as Logger-import           Hakyll.Core.Provider-import           Hakyll.Core.Routes-import qualified Hakyll.Core.Store             as Store-------------------------------------------------------------------------------------- | Get the underlying identifier.-getUnderlying :: Compiler Identifier-getUnderlying = compilerUnderlying <$> compilerAsk-------------------------------------------------------------------------------------- | Get the extension of the underlying identifier. Returns something like--- @".html"@-getUnderlyingExtension :: Compiler String-getUnderlyingExtension = takeExtension . toFilePath <$> getUnderlying------------------------------------------------------------------------------------makeItem :: a -> Compiler (Item a)-makeItem x = do-    identifier <- getUnderlying-    return $ Item identifier x-------------------------------------------------------------------------------------- | Get the route for a specified item-getRoute :: Identifier -> Compiler (Maybe FilePath)-getRoute identifier = do-    provider <- compilerProvider <$> compilerAsk-    routes   <- compilerRoutes <$> compilerAsk-    -- Note that this makes us dependend on that identifier: when the metadata-    -- of that item changes, the route may change, hence we have to recompile-    (mfp, um) <- compilerUnsafeIO $ runRoutes routes provider identifier-    when um $ compilerTellDependencies [IdentifierDependency identifier]-    return mfp-------------------------------------------------------------------------------------- | Get the full contents of the matched source file as a string,--- but without metadata preamble, if there was one.-getResourceBody :: Compiler (Item String)-getResourceBody = getResourceWith resourceBody-------------------------------------------------------------------------------------- | Get the full contents of the matched source file as a string.-getResourceString :: Compiler (Item String)-getResourceString = getResourceWith resourceString-------------------------------------------------------------------------------------- | Get the full contents of the matched source file as a lazy bytestring.-getResourceLBS :: Compiler (Item ByteString)-getResourceLBS = getResourceWith resourceLBS-------------------------------------------------------------------------------------- | Get the file path of the resource we are compiling-getResourceFilePath :: Compiler FilePath-getResourceFilePath = do-    provider <- compilerProvider   <$> compilerAsk-    id'      <- compilerUnderlying <$> compilerAsk-    return $ resourceFilePath provider id'-------------------------------------------------------------------------------------- | Overloadable function for 'getResourceString' and 'getResourceLBS'-getResourceWith :: (Provider -> Identifier -> IO a) -> Compiler (Item a)-getResourceWith reader = do-    provider <- compilerProvider   <$> compilerAsk-    id'      <- compilerUnderlying <$> compilerAsk-    let filePath = toFilePath id'-    if resourceExists provider id'-        then compilerUnsafeIO $ Item id' <$> reader provider id'-        else fail $ error' filePath-  where-    error' fp = "Hakyll.Core.Compiler.getResourceWith: resource " ++-        show fp ++ " not found"-------------------------------------------------------------------------------------- | Save a snapshot of the item. This function returns the same item, which--- convenient for building '>>=' chains.-saveSnapshot :: (Binary a, Typeable a)-             => Internal.Snapshot -> Item a -> Compiler (Item a)-saveSnapshot snapshot item = do-    store  <- compilerStore <$> compilerAsk-    logger <- compilerLogger <$> compilerAsk-    compilerUnsafeIO $ do-        Logger.debug logger $ "Storing snapshot: " ++ snapshot-        Internal.saveSnapshot store snapshot item--    -- Signal that we saved the snapshot.-    Compiler $ \_ -> return $ CompilerSnapshot snapshot (return item)------------------------------------------------------------------------------------cached :: (Binary a, Typeable a)-       => String-       -> Compiler a-       -> Compiler a-cached name compiler = do-    id'      <- compilerUnderlying <$> compilerAsk-    store    <- compilerStore      <$> compilerAsk-    provider <- compilerProvider   <$> compilerAsk--    -- Give a better error message when the resource is not there at all.-    unless (resourceExists provider id') $ fail $ itDoesntEvenExist id'--    let modified = resourceModified provider id'-    if modified-        then do-            x <- compiler-            compilerUnsafeIO $ Store.set store [name, show id'] x-            return x-        else do-            compilerTellCacheHits 1-            x        <- compilerUnsafeIO $ Store.get store [name, show id']-            progName <- compilerUnsafeIO getProgName-            case x of Store.Found x' -> return x'-                      _              -> fail $ error' progName-  where-    error' progName =-        "Hakyll.Core.Compiler.cached: Cache corrupt! " ++-         "Try running: " ++ progName ++ " clean"--    itDoesntEvenExist id' =-        "Hakyll.Core.Compiler.cached: You are trying to (perhaps "    ++-        "indirectly) use `cached` on a non-existing resource: there " ++-        "is no file backing " ++ show id'------------------------------------------------------------------------------------unsafeCompiler :: IO a -> Compiler a-unsafeCompiler = compilerUnsafeIO-------------------------------------------------------------------------------------- | Compiler for debugging purposes-debugCompiler :: String -> Compiler ()-debugCompiler msg = do-    logger <- compilerLogger <$> compilerAsk-    compilerUnsafeIO $ Logger.debug logger msg
− src/Hakyll/Core/Compiler/Internal.hs
@@ -1,265 +0,0 @@------------------------------------------------------------------------------------ | Internally used compiler module-{-# LANGUAGE FlexibleInstances          #-}-{-# LANGUAGE GADTs                      #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE MultiParamTypeClasses      #-}-module Hakyll.Core.Compiler.Internal-    ( -- * Types-      Snapshot-    , CompilerRead (..)-    , CompilerWrite (..)-    , CompilerResult (..)-    , Compiler (..)-    , runCompiler--      -- * Core operations-    , compilerTell-    , compilerAsk-    , compilerThrow-    , compilerCatch-    , compilerResult-    , compilerUnsafeIO--      -- * Utilities-    , compilerTellDependencies-    , compilerTellCacheHits-    ) where------------------------------------------------------------------------------------import           Control.Applicative            (Alternative (..))-import           Control.Exception              (SomeException, handle)-import           Control.Monad                  (forM_)-import           Control.Monad.Except            (MonadError (..))-import           Data.Set                       (Set)-import qualified Data.Set                       as S------------------------------------------------------------------------------------import           Hakyll.Core.Configuration-import           Hakyll.Core.Dependencies-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Logger             (Logger)-import qualified Hakyll.Core.Logger             as Logger-import           Hakyll.Core.Metadata-import           Hakyll.Core.Provider-import           Hakyll.Core.Routes-import           Hakyll.Core.Store-------------------------------------------------------------------------------------- | Whilst compiling an item, it possible to save multiple snapshots of it, and--- not just the final result.-type Snapshot = String-------------------------------------------------------------------------------------- | Environment in which a compiler runs-data CompilerRead = CompilerRead-    { -- | Main configuration-      compilerConfig     :: Configuration-    , -- | Underlying identifier-      compilerUnderlying :: Identifier-    , -- | Resource provider-      compilerProvider   :: Provider-    , -- | List of all known identifiers-      compilerUniverse   :: Set Identifier-    , -- | Site routes-      compilerRoutes     :: Routes-    , -- | Compiler store-      compilerStore      :: Store-    , -- | Logger-      compilerLogger     :: Logger-    }------------------------------------------------------------------------------------data CompilerWrite = CompilerWrite-    { compilerDependencies :: [Dependency]-    , compilerCacheHits    :: Int-    } deriving (Show)------------------------------------------------------------------------------------instance Monoid CompilerWrite where-    mempty = CompilerWrite [] 0-    mappend (CompilerWrite d1 h1) (CompilerWrite d2 h2) =-        CompilerWrite (d1 ++ d2) (h1 + h2)------------------------------------------------------------------------------------data CompilerResult a where-    CompilerDone     :: a -> CompilerWrite -> CompilerResult a-    CompilerSnapshot :: Snapshot -> Compiler a -> CompilerResult a-    CompilerError    :: [String] -> CompilerResult a-    CompilerRequire  :: (Identifier, Snapshot) -> Compiler a -> CompilerResult a-------------------------------------------------------------------------------------- | A monad which lets you compile items and takes care of dependency tracking--- for you.-newtype Compiler a = Compiler-    { unCompiler :: CompilerRead -> IO (CompilerResult a)-    }------------------------------------------------------------------------------------instance Functor Compiler where-    fmap f (Compiler c) = Compiler $ \r -> do-        res <- c r-        return $ case res of-            CompilerDone x w      -> CompilerDone (f x) w-            CompilerSnapshot s c' -> CompilerSnapshot s (fmap f c')-            CompilerError e       -> CompilerError e-            CompilerRequire i c'  -> CompilerRequire i (fmap f c')-    {-# INLINE fmap #-}------------------------------------------------------------------------------------instance Monad Compiler where-    return x = Compiler $ \_ -> return $ CompilerDone x mempty-    {-# INLINE return #-}--    Compiler c >>= f = Compiler $ \r -> do-        res <- c r-        case res of-            CompilerDone x w    -> do-                res' <- unCompiler (f x) r-                return $ case res' of-                    CompilerDone y w'     -> CompilerDone y (w `mappend` w')-                    CompilerSnapshot s c' -> CompilerSnapshot s $ do-                        compilerTell w  -- Save dependencies!-                        c'-                    CompilerError e       -> CompilerError e-                    CompilerRequire i c'  -> CompilerRequire i $ do-                        compilerTell w  -- Save dependencies!-                        c'--            CompilerSnapshot s c' -> return $ CompilerSnapshot s (c' >>= f)-            CompilerError e       -> return $ CompilerError e-            CompilerRequire i c'  -> return $ CompilerRequire i (c' >>= f)-    {-# INLINE (>>=) #-}--    fail = compilerThrow . return-    {-# INLINE fail #-}------------------------------------------------------------------------------------instance Applicative Compiler where-    pure x = return x-    {-# INLINE pure #-}--    f <*> x = f >>= \f' -> fmap f' x-    {-# INLINE (<*>) #-}------------------------------------------------------------------------------------instance MonadMetadata Compiler where-    getMetadata = compilerGetMetadata-    getMatches  = compilerGetMatches------------------------------------------------------------------------------------instance MonadError [String] Compiler where-  throwError = compilerThrow-  catchError = compilerCatch------------------------------------------------------------------------------------runCompiler :: Compiler a -> CompilerRead -> IO (CompilerResult a)-runCompiler compiler read' = handle handler $ unCompiler compiler read'-  where-    handler :: SomeException -> IO (CompilerResult a)-    handler e = return $ CompilerError [show e]------------------------------------------------------------------------------------instance Alternative Compiler where-    empty   = compilerThrow []-    x <|> y = compilerCatch x $ \es -> do-        logger <- compilerLogger <$> compilerAsk-        forM_ es $ \e -> compilerUnsafeIO $ Logger.debug logger $-            "Hakyll.Core.Compiler.Internal: Alternative failed: " ++ e-        y-    {-# INLINE (<|>) #-}------------------------------------------------------------------------------------compilerAsk :: Compiler CompilerRead-compilerAsk = Compiler $ \r -> return $ CompilerDone r mempty-{-# INLINE compilerAsk #-}------------------------------------------------------------------------------------compilerTell :: CompilerWrite -> Compiler ()-compilerTell deps = Compiler $ \_ -> return $ CompilerDone () deps-{-# INLINE compilerTell #-}------------------------------------------------------------------------------------compilerThrow :: [String] -> Compiler a-compilerThrow es = Compiler $ \_ -> return $ CompilerError es-{-# INLINE compilerThrow #-}------------------------------------------------------------------------------------compilerCatch :: Compiler a -> ([String] -> Compiler a) -> Compiler a-compilerCatch (Compiler x) f = Compiler $ \r -> do-    res <- x r-    case res of-        CompilerDone res' w  -> return (CompilerDone res' w)-        CompilerSnapshot s c -> return (CompilerSnapshot s (compilerCatch c f))-        CompilerError e      -> unCompiler (f e) r-        CompilerRequire i c  -> return (CompilerRequire i (compilerCatch c f))-{-# INLINE compilerCatch #-}-------------------------------------------------------------------------------------- | Put the result back in a compiler-compilerResult :: CompilerResult a -> Compiler a-compilerResult x = Compiler $ \_ -> return x-{-# INLINE compilerResult #-}------------------------------------------------------------------------------------compilerUnsafeIO :: IO a -> Compiler a-compilerUnsafeIO io = Compiler $ \_ -> do-    x <- io-    return $ CompilerDone x mempty-{-# INLINE compilerUnsafeIO #-}------------------------------------------------------------------------------------compilerTellDependencies :: [Dependency] -> Compiler ()-compilerTellDependencies ds = do-  logger <- compilerLogger <$> compilerAsk-  forM_ ds $ \d -> compilerUnsafeIO $ Logger.debug logger $-      "Hakyll.Core.Compiler.Internal: Adding dependency: " ++ show d-  compilerTell mempty {compilerDependencies = ds}-{-# INLINE compilerTellDependencies #-}------------------------------------------------------------------------------------compilerTellCacheHits :: Int -> Compiler ()-compilerTellCacheHits ch = compilerTell mempty {compilerCacheHits = ch}-{-# INLINE compilerTellCacheHits #-}------------------------------------------------------------------------------------compilerGetMetadata :: Identifier -> Compiler Metadata-compilerGetMetadata identifier = do-    provider <- compilerProvider <$> compilerAsk-    compilerTellDependencies [IdentifierDependency identifier]-    compilerUnsafeIO $ resourceMetadata provider identifier------------------------------------------------------------------------------------compilerGetMatches :: Pattern -> Compiler [Identifier]-compilerGetMatches pattern = do-    universe <- compilerUniverse <$> compilerAsk-    let matching = filterMatches pattern $ S.toList universe-        set'     = S.fromList matching-    compilerTellDependencies [PatternDependency pattern set']-    return matching
− src/Hakyll/Core/Compiler/Require.hs
@@ -1,121 +0,0 @@----------------------------------------------------------------------------------module Hakyll.Core.Compiler.Require-    ( Snapshot-    , save-    , saveSnapshot-    , load-    , loadSnapshot-    , loadBody-    , loadSnapshotBody-    , loadAll-    , loadAllSnapshots-    ) where------------------------------------------------------------------------------------import           Control.Monad                  (when)-import           Data.Binary                    (Binary)-import qualified Data.Set                       as S-import           Data.Typeable------------------------------------------------------------------------------------import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Dependencies-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Item-import           Hakyll.Core.Metadata-import           Hakyll.Core.Store              (Store)-import qualified Hakyll.Core.Store              as Store------------------------------------------------------------------------------------save :: (Binary a, Typeable a) => Store -> Item a -> IO ()-save store item = saveSnapshot store final item-------------------------------------------------------------------------------------- | Save a specific snapshot of an item, so you can load it later using--- 'loadSnapshot'.-saveSnapshot :: (Binary a, Typeable a)-             => Store -> Snapshot -> Item a -> IO ()-saveSnapshot store snapshot item =-    Store.set store (key (itemIdentifier item) snapshot) (itemBody item)-------------------------------------------------------------------------------------- | Load an item compiled elsewhere. If the required item is not yet compiled,--- the build system will take care of that automatically.-load :: (Binary a, Typeable a) => Identifier -> Compiler (Item a)-load id' = loadSnapshot id' final-------------------------------------------------------------------------------------- | Require a specific snapshot of an item.-loadSnapshot :: (Binary a, Typeable a)-             => Identifier -> Snapshot -> Compiler (Item a)-loadSnapshot id' snapshot = do-    store    <- compilerStore <$> compilerAsk-    universe <- compilerUniverse <$> compilerAsk--    -- Quick check for better error messages-    when (id' `S.notMember` universe) $ fail notFound--    compilerTellDependencies [IdentifierDependency id']-    compilerResult $ CompilerRequire (id', snapshot) $ do-        result <- compilerUnsafeIO $ Store.get store (key id' snapshot)-        case result of-            Store.NotFound      -> fail notFound-            Store.WrongType e r -> fail $ wrongType e r-            Store.Found x       -> return $ Item id' x-  where-    notFound =-        "Hakyll.Core.Compiler.Require.load: " ++ show id' ++-        " (snapshot " ++ snapshot ++ ") was not found in the cache, " ++-        "the cache might be corrupted or " ++-        "the item you are referring to might not exist"-    wrongType e r =-        "Hakyll.Core.Compiler.Require.load: " ++ show id' ++-        " (snapshot " ++ snapshot ++ ") was found in the cache, " ++-        "but does not have the right type: expected " ++ show e ++-        " but got " ++ show r-------------------------------------------------------------------------------------- | A shortcut for only requiring the body of an item.------ > loadBody = fmap itemBody . load-loadBody :: (Binary a, Typeable a) => Identifier -> Compiler a-loadBody id' = loadSnapshotBody id' final------------------------------------------------------------------------------------loadSnapshotBody :: (Binary a, Typeable a)-                 => Identifier -> Snapshot -> Compiler a-loadSnapshotBody id' snapshot = fmap itemBody $ loadSnapshot id' snapshot-------------------------------------------------------------------------------------- | This function allows you to 'load' a dynamic list of items-loadAll :: (Binary a, Typeable a) => Pattern -> Compiler [Item a]-loadAll pattern = loadAllSnapshots pattern final------------------------------------------------------------------------------------loadAllSnapshots :: (Binary a, Typeable a)-                 => Pattern -> Snapshot -> Compiler [Item a]-loadAllSnapshots pattern snapshot = do-    matching <- getMatches pattern-    mapM (\i -> loadSnapshot i snapshot) matching------------------------------------------------------------------------------------key :: Identifier -> String -> [String]-key identifier snapshot =-    ["Hakyll.Core.Compiler.Require", show identifier, snapshot]------------------------------------------------------------------------------------final :: Snapshot-final = "_final"
− src/Hakyll/Core/Configuration.hs
@@ -1,134 +0,0 @@------------------------------------------------------------------------------------ | Exports a datastructure for the top-level hakyll configuration-module Hakyll.Core.Configuration-    ( Configuration (..)-    , shouldIgnoreFile-    , defaultConfiguration-    ) where------------------------------------------------------------------------------------import           Data.Default     (Default (..))-import           Data.List        (isPrefixOf, isSuffixOf)-import           System.Directory (canonicalizePath)-import           System.Exit      (ExitCode)-import           System.FilePath  (isAbsolute, normalise, takeFileName)-import           System.IO.Error  (catchIOError)-import           System.Process   (system)------------------------------------------------------------------------------------data Configuration = Configuration-    { -- | Directory in which the output written-      destinationDirectory :: FilePath-    , -- | Directory where hakyll's internal store is kept-      storeDirectory       :: FilePath-    , -- | Directory in which some temporary files will be kept-      tmpDirectory         :: FilePath-    , -- | Directory where hakyll finds the files to compile. This is @.@ by-      -- default.-      providerDirectory    :: FilePath-    , -- | Function to determine ignored files-      ---      -- In 'defaultConfiguration', the following files are ignored:-      ---      -- * files starting with a @.@-      ---      -- * files starting with a @#@-      ---      -- * files ending with a @~@-      ---      -- * files ending with @.swp@-      ---      -- Note that the files in 'destinationDirectory' and 'storeDirectory' will-      -- also be ignored. Note that this is the configuration parameter, if you-      -- want to use the test, you should use 'shouldIgnoreFile'.-      ---      ignoreFile           :: FilePath -> Bool-    , -- | Here, you can plug in a system command to upload/deploy your site.-      ---      -- Example:-      ---      -- > rsync -ave 'ssh -p 2217' _site jaspervdj@jaspervdj.be:hakyll-      ---      -- You can execute this by using-      ---      -- > ./site deploy-      ---      deployCommand        :: String-    , -- | Function to deploy the site from Haskell.-      ---      -- By default, this command executes the shell command stored in-      -- 'deployCommand'. If you override it, 'deployCommand' will not-      -- be used implicitely.-      ---      -- The 'Configuration' object is passed as a parameter to this-      -- function.-      ---      deploySite           :: Configuration -> IO ExitCode-    , -- | Use an in-memory cache for items. This is faster but uses more-      -- memory.-      inMemoryCache        :: Bool-    , -- | Override default host for preview server. Default is "127.0.0.1",-      -- which binds only on the loopback address.-      -- One can also override the host as a command line argument:-      -- ./site preview -h "0.0.0.0"-      previewHost          :: String-    , -- | Override default port for preview server. Default is 8000.-      -- One can also override the port as a command line argument:-      -- ./site preview -p 1234-      previewPort          :: Int-    }-----------------------------------------------------------------------------------instance Default Configuration where-    def = defaultConfiguration------------------------------------------------------------------------------------- | Default configuration for a hakyll application-defaultConfiguration :: Configuration-defaultConfiguration = Configuration-    { destinationDirectory = "_site"-    , storeDirectory       = "_cache"-    , tmpDirectory         = "_cache/tmp"-    , providerDirectory    = "."-    , ignoreFile           = ignoreFile'-    , deployCommand        = "echo 'No deploy command specified' && exit 1"-    , deploySite           = system . deployCommand-    , inMemoryCache        = True-    , previewHost          = "127.0.0.1"-    , previewPort          = 8000-    }-  where-    ignoreFile' path-        | "."    `isPrefixOf` fileName = True-        | "#"    `isPrefixOf` fileName = True-        | "~"    `isSuffixOf` fileName = True-        | ".swp" `isSuffixOf` fileName = True-        | otherwise                    = False-      where-        fileName = takeFileName path-------------------------------------------------------------------------------------- | Check if a file should be ignored-shouldIgnoreFile :: Configuration -> FilePath -> IO Bool-shouldIgnoreFile conf path = orM-    [ inDir (destinationDirectory conf)-    , inDir (storeDirectory conf)-    , inDir (tmpDirectory conf)-    , return (ignoreFile conf path')-    ]-  where-    path'    = normalise path-    absolute = isAbsolute path--    inDir dir-        | absolute  = do-            dir' <- catchIOError (canonicalizePath dir) (const $ return dir)-            return $ dir' `isPrefixOf` path'-        | otherwise = return $ dir `isPrefixOf` path'--    orM :: [IO Bool] -> IO Bool-    orM []       = return False-    orM (x : xs) = x >>= \b -> if b then return True else orM xs
− src/Hakyll/Core/Dependencies.hs
@@ -1,146 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE DeriveDataTypeable #-}-module Hakyll.Core.Dependencies-    ( Dependency (..)-    , DependencyFacts-    , outOfDate-    ) where------------------------------------------------------------------------------------import           Control.Monad                  (foldM, forM_, unless, when)-import           Control.Monad.Reader           (ask)-import           Control.Monad.RWS              (RWS, runRWS)-import qualified Control.Monad.State            as State-import           Control.Monad.Writer           (tell)-import           Data.Binary                    (Binary (..), getWord8,-                                                 putWord8)-import           Data.List                      (find)-import           Data.Map                       (Map)-import qualified Data.Map                       as M-import           Data.Maybe                     (fromMaybe)-import           Data.Set                       (Set)-import qualified Data.Set                       as S-import           Data.Typeable                  (Typeable)------------------------------------------------------------------------------------import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern------------------------------------------------------------------------------------data Dependency-    = PatternDependency Pattern (Set Identifier)-    | IdentifierDependency Identifier-    deriving (Show, Typeable)------------------------------------------------------------------------------------instance Binary Dependency where-    put (PatternDependency p is) = putWord8 0 >> put p >> put is-    put (IdentifierDependency i) = putWord8 1 >> put i-    get = getWord8 >>= \t -> case t of-        0 -> PatternDependency <$> get <*> get-        1 -> IdentifierDependency <$> get-        _ -> error "Data.Binary.get: Invalid Dependency"------------------------------------------------------------------------------------type DependencyFacts = Map Identifier [Dependency]------------------------------------------------------------------------------------outOfDate-    :: [Identifier]     -- ^ All known identifiers-    -> Set Identifier   -- ^ Initially out-of-date resources-    -> DependencyFacts  -- ^ Old dependency facts-    -> (Set Identifier, DependencyFacts, [String])-outOfDate universe ood oldFacts =-    let (_, state, logs) = runRWS rws universe (DependencyState oldFacts ood)-    in (dependencyOod state, dependencyFacts state, logs)-  where-    rws = do-        checkNew-        checkChangedPatterns-        bruteForce------------------------------------------------------------------------------------data DependencyState = DependencyState-    { dependencyFacts :: DependencyFacts-    , dependencyOod   :: Set Identifier-    } deriving (Show)------------------------------------------------------------------------------------type DependencyM a = RWS [Identifier] [String] DependencyState a------------------------------------------------------------------------------------markOod :: Identifier -> DependencyM ()-markOod id' = State.modify $ \s ->-    s {dependencyOod = S.insert id' $ dependencyOod s}------------------------------------------------------------------------------------dependenciesFor :: Identifier -> DependencyM [Identifier]-dependenciesFor id' = do-    facts <- dependencyFacts <$> State.get-    return $ concatMap dependenciesFor' $ fromMaybe [] $ M.lookup id' facts-  where-    dependenciesFor' (IdentifierDependency i) = [i]-    dependenciesFor' (PatternDependency _ is) = S.toList is------------------------------------------------------------------------------------checkNew :: DependencyM ()-checkNew = do-    universe <- ask-    facts    <- dependencyFacts <$> State.get-    forM_ universe $ \id' -> unless (id' `M.member` facts) $ do-        tell [show id' ++ " is out-of-date because it is new"]-        markOod id'------------------------------------------------------------------------------------checkChangedPatterns :: DependencyM ()-checkChangedPatterns = do-    facts <- M.toList . dependencyFacts <$> State.get-    forM_ facts $ \(id', deps) -> do-        deps' <- foldM (go id') [] deps-        State.modify $ \s -> s-            {dependencyFacts = M.insert id' deps' $ dependencyFacts s}-  where-    go _   ds (IdentifierDependency i) = return $ IdentifierDependency i : ds-    go id' ds (PatternDependency p ls) = do-        universe <- ask-        let ls' = S.fromList $ filterMatches p universe-        if ls == ls'-            then return $ PatternDependency p ls : ds-            else do-                tell [show id' ++ " is out-of-date because a pattern changed"]-                markOod id'-                return $ PatternDependency p ls' : ds------------------------------------------------------------------------------------bruteForce :: DependencyM ()-bruteForce = do-    todo <- ask-    go todo-  where-    go todo = do-        (todo', changed) <- foldM check ([], False) todo-        when changed (go todo')--    check (todo, changed) id' = do-        deps <- dependenciesFor id'-        ood  <- dependencyOod <$> State.get-        case find (`S.member` ood) deps of-            Nothing -> return (id' : todo, changed)-            Just d  -> do-                tell [show id' ++ " is out-of-date because " ++-                    show d ++ " is out-of-date"]-                markOod id'-                return (todo, True)
− src/Hakyll/Core/File.hs
@@ -1,93 +0,0 @@------------------------------------------------------------------------------------ | Exports simple compilers to just copy files-{-# LANGUAGE CPP                        #-}-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-module Hakyll.Core.File-    ( CopyFile (..)-    , copyFileCompiler-    , TmpFile (..)-    , newTmpFile-    ) where------------------------------------------------------------------------------------import           Data.Binary                   (Binary (..))-import           Data.Typeable                 (Typeable)-#if MIN_VERSION_directory(1,2,6)-import           System.Directory              (copyFileWithMetadata)-#else-import           System.Directory              (copyFile)-#endif-import           System.Directory              (doesFileExist,-                                                renameFile)-import           System.FilePath               ((</>))-import           System.Random                 (randomIO)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Configuration-import           Hakyll.Core.Item-import           Hakyll.Core.Provider-import qualified Hakyll.Core.Store             as Store-import           Hakyll.Core.Util.File-import           Hakyll.Core.Writable-------------------------------------------------------------------------------------- | This will copy any file directly by using a system call-newtype CopyFile = CopyFile FilePath-    deriving (Binary, Eq, Ord, Show, Typeable)------------------------------------------------------------------------------------instance Writable CopyFile where-#if MIN_VERSION_directory(1,2,6)-    write dst (Item _ (CopyFile src)) = copyFileWithMetadata src dst-#else-    write dst (Item _ (CopyFile src)) = copyFile src dst-#endif----------------------------------------------------------------------------------copyFileCompiler :: Compiler (Item CopyFile)-copyFileCompiler = do-    identifier <- getUnderlying-    provider   <- compilerProvider <$> compilerAsk-    makeItem $ CopyFile $ resourceFilePath provider identifier------------------------------------------------------------------------------------newtype TmpFile = TmpFile FilePath-    deriving (Typeable)------------------------------------------------------------------------------------instance Binary TmpFile where-    put _ = return ()-    get   = error $-        "Hakyll.Core.File.TmpFile: You tried to load a TmpFile, however, " ++-        "this is not possible since these are deleted as soon as possible."------------------------------------------------------------------------------------instance Writable TmpFile where-    write dst (Item _ (TmpFile fp)) = renameFile fp dst-------------------------------------------------------------------------------------- | Create a tmp file-newTmpFile :: String            -- ^ Suffix and extension-           -> Compiler TmpFile  -- ^ Resulting tmp path-newTmpFile suffix = do-    path <- mkPath-    compilerUnsafeIO $ makeDirectories path-    debugCompiler $ "newTmpFile " ++ path-    return $ TmpFile path-  where-    mkPath = do-        rand <- compilerUnsafeIO $ randomIO :: Compiler Int-        tmp  <- tmpDirectory . compilerConfig <$> compilerAsk-        let path = tmp </> Store.hash [show rand] ++ "-" ++ suffix-        exists <- compilerUnsafeIO $ doesFileExist path-        if exists then mkPath else return path
− src/Hakyll/Core/Identifier.hs
@@ -1,80 +0,0 @@------------------------------------------------------------------------------------ | An identifier is a type used to uniquely identify an item. An identifier is--- conceptually similar to a file path. Examples of identifiers are:------ * @posts/foo.markdown@------ * @index@------ * @error/404@-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-module Hakyll.Core.Identifier-    ( Identifier-    , fromFilePath-    , toFilePath-    , identifierVersion-    , setVersion-    ) where------------------------------------------------------------------------------------import           Control.DeepSeq     (NFData (..))-import           Data.List           (intercalate)-import           System.FilePath     (dropTrailingPathSeparator, splitPath)------------------------------------------------------------------------------------import           Data.Binary         (Binary (..))-import           Data.Typeable       (Typeable)-import           GHC.Exts            (IsString, fromString)------------------------------------------------------------------------------------data Identifier = Identifier-    { identifierVersion :: Maybe String-    , identifierPath    :: String-    } deriving (Eq, Ord, Typeable)------------------------------------------------------------------------------------instance Binary Identifier where-    put (Identifier v p) = put v >> put p-    get = Identifier <$> get <*> get------------------------------------------------------------------------------------instance IsString Identifier where-    fromString = fromFilePath------------------------------------------------------------------------------------instance NFData Identifier where-    rnf (Identifier v p) = rnf v `seq` rnf p `seq` ()------------------------------------------------------------------------------------instance Show Identifier where-    show i = case identifierVersion i of-        Nothing -> toFilePath i-        Just v  -> toFilePath i ++ " (" ++ v ++ ")"-------------------------------------------------------------------------------------- | Parse an identifier from a string-fromFilePath :: String -> Identifier-fromFilePath = Identifier Nothing .-    intercalate "/" . filter (not . null) . split'-  where-    split' = map dropTrailingPathSeparator . splitPath-------------------------------------------------------------------------------------- | Convert an identifier to a relative 'FilePath'-toFilePath :: Identifier -> FilePath-toFilePath = identifierPath------------------------------------------------------------------------------------setVersion :: Maybe String -> Identifier -> Identifier-setVersion v i = i {identifierVersion = v}
− src/Hakyll/Core/Identifier/Pattern.hs
@@ -1,322 +0,0 @@------------------------------------------------------------------------------------ | As 'Identifier' is used to specify a single item, a 'Pattern' is used to--- specify a list of items.------ In most cases, globs are used for patterns.------ A very simple pattern of such a pattern is @\"foo\/bar\"@. This pattern will--- only match the exact @foo\/bar@ identifier.------ To match more than one identifier, there are different captures that one can--- use:------ * @\"*\"@: matches at most one element of an identifier;------ * @\"**\"@: matches one or more elements of an identifier.------ Some examples:------ * @\"foo\/*\"@ will match @\"foo\/bar\"@ and @\"foo\/foo\"@, but not---   @\"foo\/bar\/qux\"@;------ * @\"**\"@ will match any identifier;------ * @\"foo\/**\"@ will match @\"foo\/bar\"@ and @\"foo\/bar\/qux\"@, but not---   @\"bar\/foo\"@;------ * @\"foo\/*.html\"@ will match all HTML files in the @\"foo\/\"@ directory.------ The 'capture' function allows the user to get access to the elements captured--- by the capture elements in the pattern.-module Hakyll.Core.Identifier.Pattern-    ( -- * The pattern type-      Pattern--      -- * Creating patterns-    , fromGlob-    , fromList-    , fromRegex-    , fromVersion-    , hasVersion-    , hasNoVersion--      -- * Composing patterns-    , (.&&.)-    , (.||.)-    , complement--      -- * Applying patterns-    , matches-    , filterMatches--      -- * Capturing strings-    , capture-    , fromCapture-    , fromCaptures-    ) where------------------------------------------------------------------------------------import           Control.Arrow          ((&&&), (>>>))-import           Control.Monad          (msum)-import           Data.Binary            (Binary (..), getWord8, putWord8)-import           Data.List              (inits, isPrefixOf, tails)-import           Data.Maybe             (isJust)-import           Data.Set               (Set)-import qualified Data.Set               as S------------------------------------------------------------------------------------import           GHC.Exts               (IsString, fromString)-import           Text.Regex.TDFA        ((=~))------------------------------------------------------------------------------------import           Hakyll.Core.Identifier-------------------------------------------------------------------------------------- | Elements of a glob pattern-data GlobComponent-    = Capture-    | CaptureMany-    | Literal String-    deriving (Eq, Show)------------------------------------------------------------------------------------instance Binary GlobComponent where-    put Capture     = putWord8 0-    put CaptureMany = putWord8 1-    put (Literal s) = putWord8 2 >> put s--    get = getWord8 >>= \t -> case t of-        0 -> pure Capture-        1 -> pure CaptureMany-        2 -> Literal <$> get-        _ -> error "Data.Binary.get: Invalid GlobComponent"-------------------------------------------------------------------------------------- | Type that allows matching on identifiers-data Pattern-    = Everything-    | Complement Pattern-    | And Pattern Pattern-    | Glob [GlobComponent]-    | List (Set Identifier)-    | Regex String-    | Version (Maybe String)-    deriving (Show)------------------------------------------------------------------------------------instance Binary Pattern where-    put Everything     = putWord8 0-    put (Complement p) = putWord8 1 >> put p-    put (And x y)      = putWord8 2 >> put x >> put y-    put (Glob g)       = putWord8 3 >> put g-    put (List is)      = putWord8 4 >> put is-    put (Regex r)      = putWord8 5 >> put r-    put (Version v)    = putWord8 6 >> put v--    get = getWord8 >>= \t -> case t of-        0 -> pure Everything-        1 -> Complement <$> get-        2 -> And <$> get <*> get-        3 -> Glob <$> get-        4 -> List <$> get-        5 -> Regex <$> get-        _ -> Version <$> get------------------------------------------------------------------------------------instance IsString Pattern where-    fromString = fromGlob------------------------------------------------------------------------------------instance Monoid Pattern where-    mempty  = Everything-    mappend = (.&&.)-------------------------------------------------------------------------------------- | Parse a pattern from a string-fromGlob :: String -> Pattern-fromGlob = Glob . parse'-  where-    parse' str =-        let (chunk, rest) = break (`elem` "\\*") str-        in case rest of-            ('\\' : x   : xs) -> Literal (chunk ++ [x]) : parse' xs-            ('*'  : '*' : xs) -> Literal chunk : CaptureMany : parse' xs-            ('*'  : xs)       -> Literal chunk : Capture : parse' xs-            xs                -> Literal chunk : Literal xs : []-------------------------------------------------------------------------------------- | Create a 'Pattern' from a list of 'Identifier's it should match.------ /Warning/: use this carefully with 'hasNoVersion' and 'hasVersion'. The--- 'Identifier's in the list /already/ have versions assigned, and the pattern--- will then only match the intersection of both versions.------ A more concrete example,------ > fromList ["foo.markdown"] .&&. hasVersion "pdf"------ will not match anything! The @"foo.markdown"@ 'Identifier' has no version--- assigned, so the LHS of '.&&.' will only match this 'Identifier' with no--- version. The RHS only matches 'Identifier's with version set to @"pdf"@ ----- hence, this pattern matches nothing.------ The correct way to use this is:------ > fromList $ map (setVersion $ Just "pdf") ["foo.markdown"]-fromList :: [Identifier] -> Pattern-fromList = List . S.fromList-------------------------------------------------------------------------------------- | Create a 'Pattern' from a regex------ Example:------ > regex "^foo/[^x]*$-fromRegex :: String -> Pattern-fromRegex = Regex-------------------------------------------------------------------------------------- | Create a pattern which matches all items with the given version.-fromVersion :: Maybe String -> Pattern-fromVersion = Version-------------------------------------------------------------------------------------- | Specify a version, e.g.------ > "foo/*.markdown" .&&. hasVersion "pdf"-hasVersion :: String -> Pattern-hasVersion = fromVersion . Just-------------------------------------------------------------------------------------- | Match only if the identifier has no version set, e.g.------ > "foo/*.markdown" .&&. hasNoVersion-hasNoVersion :: Pattern-hasNoVersion = fromVersion Nothing-------------------------------------------------------------------------------------- | '&&' for patterns: the given identifier must match both subterms-(.&&.) :: Pattern -> Pattern -> Pattern-x .&&. y = And x y-infixr 3 .&&.-------------------------------------------------------------------------------------- | '||' for patterns: the given identifier must match any subterm-(.||.) :: Pattern -> Pattern -> Pattern-x .||. y = complement (complement x `And` complement y)  -- De Morgan's law-infixr 2 .||.-------------------------------------------------------------------------------------- | Inverts a pattern, e.g.------ > complement "foo/bar.html"------ will match /anything/ except @\"foo\/bar.html\"@-complement :: Pattern -> Pattern-complement = Complement-------------------------------------------------------------------------------------- | Check if an identifier matches a pattern-matches :: Pattern -> Identifier -> Bool-matches Everything     _ = True-matches (Complement p) i = not $ matches p i-matches (And x y)      i = matches x i && matches y i-matches (Glob p)       i = isJust $ capture (Glob p) i-matches (List l)       i = i `S.member` l-matches (Regex r)      i = toFilePath i =~ r-matches (Version v)    i = identifierVersion i == v-------------------------------------------------------------------------------------- | Given a list of identifiers, retain only those who match the given pattern-filterMatches :: Pattern -> [Identifier] -> [Identifier]-filterMatches = filter . matches-------------------------------------------------------------------------------------- | Split a list at every possible point, generate a list of (init, tail)--- cases. The result is sorted with inits decreasing in length.-splits :: [a] -> [([a], [a])]-splits = inits &&& tails >>> uncurry zip >>> reverse-------------------------------------------------------------------------------------- | Match a glob against a pattern, generating a list of captures-capture :: Pattern -> Identifier -> Maybe [String]-capture (Glob p) i = capture' p (toFilePath i)-capture _        _ = Nothing-------------------------------------------------------------------------------------- | Internal verion of 'capture'-capture' :: [GlobComponent] -> String -> Maybe [String]-capture' [] [] = Just []  -- An empty match-capture' [] _  = Nothing  -- No match-capture' (Literal l : ms) str-    -- Match the literal against the string-    | l `isPrefixOf` str = capture' ms $ drop (length l) str-    | otherwise          = Nothing-capture' (Capture : ms) str =-    -- Match until the next /-    let (chunk, rest) = break (== '/') str-    in msum $ [ fmap (i :) (capture' ms (t ++ rest)) | (i, t) <- splits chunk ]-capture' (CaptureMany : ms) str =-    -- Match everything-    msum $ [ fmap (i :) (capture' ms t) | (i, t) <- splits str ]-------------------------------------------------------------------------------------- | Create an identifier from a pattern by filling in the captures with a given--- string------ Example:------ > fromCapture (fromGlob "tags/*") "foo"------ Result:------ > "tags/foo"-fromCapture :: Pattern -> String -> Identifier-fromCapture pattern = fromCaptures pattern . repeat-------------------------------------------------------------------------------------- | Create an identifier from a pattern by filling in the captures with the--- given list of strings-fromCaptures :: Pattern -> [String] -> Identifier-fromCaptures (Glob p) = fromFilePath . fromCaptures' p-fromCaptures _        = error $-    "Hakyll.Core.Identifier.Pattern.fromCaptures: fromCaptures only works " ++-    "on simple globs!"-------------------------------------------------------------------------------------- | Internally used version of 'fromCaptures'-fromCaptures' :: [GlobComponent] -> [String] -> String-fromCaptures' []        _ = mempty-fromCaptures' (m : ms) [] = case m of-    Literal l -> l `mappend` fromCaptures' ms []-    _         -> error $  "Hakyll.Core.Identifier.Pattern.fromCaptures': "-                       ++ "identifier list exhausted"-fromCaptures' (m : ms) ids@(i : is) = case m of-    Literal l -> l `mappend` fromCaptures' ms ids-    _         -> i `mappend` fromCaptures' ms is
− src/Hakyll/Core/Item.hs
@@ -1,63 +0,0 @@------------------------------------------------------------------------------------ | An item is a combination of some content and its 'Identifier'. This way, we--- can still use the 'Identifier' to access metadata.-{-# LANGUAGE DeriveDataTypeable #-}-module Hakyll.Core.Item-    ( Item (..)-    , itemSetBody-    , withItemBody-    ) where------------------------------------------------------------------------------------import           Data.Binary                   (Binary (..))-import           Data.Foldable                 (Foldable (..))-import           Data.Typeable                 (Typeable)-import           Prelude                       hiding (foldr)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Identifier------------------------------------------------------------------------------------data Item a = Item-    { itemIdentifier :: Identifier-    , itemBody       :: a-    } deriving (Show, Typeable)------------------------------------------------------------------------------------instance Functor Item where-    fmap f (Item i x) = Item i (f x)------------------------------------------------------------------------------------instance Foldable Item where-    foldr f z (Item _ x) = f x z------------------------------------------------------------------------------------instance Traversable Item where-    traverse f (Item i x) = Item i <$> f x------------------------------------------------------------------------------------instance Binary a => Binary (Item a) where-    put (Item i x) = put i >> put x-    get            = Item <$> get <*> get------------------------------------------------------------------------------------itemSetBody :: a -> Item b -> Item a-itemSetBody x (Item i _) = Item i x-------------------------------------------------------------------------------------- | Perform a compiler action on the item body. This is the same as 'traverse',--- but looks less intimidating.------ > withItemBody = traverse-withItemBody :: (a -> Compiler b) -> Item a -> Compiler (Item b)-withItemBody = traverse
− src/Hakyll/Core/Item/SomeItem.hs
@@ -1,23 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE DeriveDataTypeable        #-}-{-# LANGUAGE ExistentialQuantification #-}-module Hakyll.Core.Item.SomeItem-    ( SomeItem (..)-    ) where------------------------------------------------------------------------------------import           Data.Binary          (Binary)-import           Data.Typeable        (Typeable)------------------------------------------------------------------------------------import           Hakyll.Core.Item-import           Hakyll.Core.Writable-------------------------------------------------------------------------------------- | An existential type, mostly for internal usage.-data SomeItem = forall a.-    (Binary a, Typeable a, Writable a) => SomeItem (Item a)-    deriving (Typeable)
− src/Hakyll/Core/Logger.hs
@@ -1,97 +0,0 @@------------------------------------------------------------------------------------ | Produce pretty, thread-safe logs-module Hakyll.Core.Logger-    ( Verbosity (..)-    , Logger-    , new-    , flush-    , error-    , header-    , message-    , debug-    ) where------------------------------------------------------------------------------------import           Control.Concurrent      (forkIO)-import           Control.Concurrent.Chan (Chan, newChan, readChan, writeChan)-import           Control.Concurrent.MVar (MVar, newEmptyMVar, putMVar, takeMVar)-import           Control.Monad           (forever)-import           Control.Monad.Trans     (MonadIO, liftIO)-import           Prelude                 hiding (error)------------------------------------------------------------------------------------data Verbosity-    = Error-    | Message-    | Debug-    deriving (Eq, Ord, Show)-------------------------------------------------------------------------------------- | Logger structure. Very complicated.-data Logger = Logger-    { loggerChan      :: Chan (Maybe String)  -- ^ Nothing marks the end-    , loggerSync      :: MVar ()              -- ^ Used for sync on quit-    , loggerSink      :: String -> IO ()      -- ^ Out sink-    , loggerVerbosity :: Verbosity            -- ^ Verbosity-    }-------------------------------------------------------------------------------------- | Create a new logger-new :: Verbosity -> IO Logger-new vbty = do-    logger <- Logger <$>-        newChan <*> newEmptyMVar <*> pure putStrLn <*> pure vbty-    _      <- forkIO $ loggerThread logger-    return logger-  where-    loggerThread logger = forever $ do-        msg <- readChan $ loggerChan logger-        case msg of-            -- Stop: sync-            Nothing -> putMVar (loggerSync logger) ()-            -- Print and continue-            Just m  -> loggerSink logger m-------------------------------------------------------------------------------------- | Flush the logger (blocks until flushed)-flush :: Logger -> IO ()-flush logger = do-    writeChan (loggerChan logger) Nothing-    () <- takeMVar $ loggerSync logger-    return ()------------------------------------------------------------------------------------string :: MonadIO m-       => Logger     -- ^ Logger-       -> Verbosity  -- ^ Verbosity of the string-       -> String     -- ^ Section name-       -> m ()       -- ^ No result-string l v m-    | loggerVerbosity l >= v = liftIO $ writeChan (loggerChan l) (Just m)-    | otherwise              = return ()------------------------------------------------------------------------------------error :: MonadIO m => Logger -> String -> m ()-error l m = string l Error $ "  [ERROR] " ++ m------------------------------------------------------------------------------------header :: MonadIO m => Logger -> String -> m ()-header l = string l Message------------------------------------------------------------------------------------message :: MonadIO m => Logger -> String -> m ()-message l m = string l Message $ "  " ++ m------------------------------------------------------------------------------------debug :: MonadIO m => Logger -> String -> m ()-debug l m = string l Debug $ "  [DEBUG] " ++ m
− src/Hakyll/Core/Metadata.hs
@@ -1,138 +0,0 @@----------------------------------------------------------------------------------module Hakyll.Core.Metadata-    ( Metadata-    , lookupString-    , lookupStringList--    , MonadMetadata (..)-    , getMetadataField-    , getMetadataField'-    , makePatternDependency--    , BinaryMetadata (..)-    ) where------------------------------------------------------------------------------------import           Control.Arrow                  (second)-import           Control.Monad                  (forM)-import           Data.Binary                    (Binary (..), getWord8,-                                                 putWord8, Get)-import qualified Data.HashMap.Strict            as HMS-import qualified Data.Set                       as S-import qualified Data.Text                      as T-import qualified Data.Vector                    as V-import qualified Data.Yaml.Extended                      as Yaml-import           Hakyll.Core.Dependencies-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern------------------------------------------------------------------------------------type Metadata = Yaml.Object------------------------------------------------------------------------------------lookupString :: String -> Metadata -> Maybe String-lookupString key meta = HMS.lookup (T.pack key) meta >>= Yaml.toString------------------------------------------------------------------------------------lookupStringList :: String -> Metadata -> Maybe [String]-lookupStringList key meta =-    HMS.lookup (T.pack key) meta >>= Yaml.toList >>= mapM Yaml.toString------------------------------------------------------------------------------------class Monad m => MonadMetadata m where-    getMetadata    :: Identifier -> m Metadata-    getMatches     :: Pattern -> m [Identifier]--    getAllMetadata :: Pattern -> m [(Identifier, Metadata)]-    getAllMetadata pattern = do-        matches' <- getMatches pattern-        forM matches' $ \id' -> do-            metadata <- getMetadata id'-            return (id', metadata)------------------------------------------------------------------------------------getMetadataField :: MonadMetadata m => Identifier -> String -> m (Maybe String)-getMetadataField identifier key = do-    metadata <- getMetadata identifier-    return $ lookupString key metadata-------------------------------------------------------------------------------------- | Version of 'getMetadataField' which throws an error if the field does not--- exist.-getMetadataField' :: MonadMetadata m => Identifier -> String -> m String-getMetadataField' identifier key = do-    field <- getMetadataField identifier key-    case field of-        Just v  -> return v-        Nothing -> fail $ "Hakyll.Core.Metadata.getMetadataField': " ++-            "Item " ++ show identifier ++ " has no metadata field " ++ show key------------------------------------------------------------------------------------makePatternDependency :: MonadMetadata m => Pattern -> m Dependency-makePatternDependency pattern = do-    matches' <- getMatches pattern-    return $ PatternDependency pattern (S.fromList matches')-------------------------------------------------------------------------------------- | Newtype wrapper for serialization.-newtype BinaryMetadata = BinaryMetadata-    {unBinaryMetadata :: Metadata}---instance Binary BinaryMetadata where-    put (BinaryMetadata obj) = put (BinaryYaml $ Yaml.Object obj)-    get = do-        BinaryYaml (Yaml.Object obj) <- get-        return $ BinaryMetadata obj------------------------------------------------------------------------------------newtype BinaryYaml = BinaryYaml {unBinaryYaml :: Yaml.Value}------------------------------------------------------------------------------------instance Binary BinaryYaml where-    put (BinaryYaml yaml) = case yaml of-        Yaml.Object obj -> do-            putWord8 0-            let list :: [(T.Text, BinaryYaml)]-                list = map (second BinaryYaml) $ HMS.toList obj-            put list--        Yaml.Array arr -> do-            putWord8 1-            let list = map BinaryYaml (V.toList arr) :: [BinaryYaml]-            put list--        Yaml.String s -> putWord8 2 >> put s-        Yaml.Number n -> putWord8 3 >> put n-        Yaml.Bool   b -> putWord8 4 >> put b-        Yaml.Null     -> putWord8 5--    get = do-        tag <- getWord8-        case tag of-            0 -> do-                list <- get :: Get [(T.Text, BinaryYaml)]-                return $ BinaryYaml $ Yaml.Object $-                    HMS.fromList $ map (second unBinaryYaml) list--            1 -> do-                list <- get :: Get [BinaryYaml]-                return $ BinaryYaml $-                    Yaml.Array $ V.fromList $ map unBinaryYaml list--            2 -> BinaryYaml . Yaml.String <$> get-            3 -> BinaryYaml . Yaml.Number <$> get-            4 -> BinaryYaml . Yaml.Bool   <$> get-            5 -> return $ BinaryYaml Yaml.Null-            _ -> fail "Data.Binary.get: Invalid Binary Metadata"
− src/Hakyll/Core/Provider.hs
@@ -1,43 +0,0 @@------------------------------------------------------------------------------------ | This module provides an wrapper API around the file system which does some--- caching.-module Hakyll.Core.Provider-    ( -- * Constructing resource providers-      Internal.Provider-    , newProvider--      -- * Querying resource properties-    , Internal.resourceList-    , Internal.resourceExists-    , Internal.resourceFilePath-    , Internal.resourceModified-    , Internal.resourceModificationTime--      -- * Access to raw resource content-    , Internal.resourceString-    , Internal.resourceLBS--      -- * Access to metadata and body content-    , Internal.resourceMetadata-    , Internal.resourceBody-    ) where------------------------------------------------------------------------------------import qualified Hakyll.Core.Provider.Internal      as Internal-import qualified Hakyll.Core.Provider.MetadataCache as Internal-import           Hakyll.Core.Store                  (Store)-------------------------------------------------------------------------------------- | Create a resource provider-newProvider :: Store                   -- ^ Store to use-            -> (FilePath -> IO Bool)   -- ^ Should we ignore this file?-            -> FilePath                -- ^ Search directory-            -> IO Internal.Provider    -- ^ Resulting provider-newProvider store ignore directory = do-    -- Delete metadata cache where necessary-    p <- Internal.newProvider store ignore directory-    mapM_ (Internal.resourceInvalidateMetadataCache p) $-        filter (Internal.resourceModified p) $ Internal.resourceList p-    return p
− src/Hakyll/Core/Provider/Internal.hs
@@ -1,202 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE CPP                        #-}-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-module Hakyll.Core.Provider.Internal-    ( ResourceInfo (..)-    , Provider (..)-    , newProvider--    , resourceList-    , resourceExists--    , resourceFilePath-    , resourceString-    , resourceLBS--    , resourceModified-    , resourceModificationTime-    ) where------------------------------------------------------------------------------------import           Control.DeepSeq        (NFData (..), deepseq)-import           Control.Monad          (forM)-import           Data.Binary            (Binary (..))-import qualified Data.ByteString.Lazy   as BL-import           Data.Map               (Map)-import qualified Data.Map               as M-import           Data.Maybe             (fromMaybe)-import           Data.Set               (Set)-import qualified Data.Set               as S-import           Data.Time              (Day (..), UTCTime (..))-import           Data.Typeable          (Typeable)-import           System.Directory       (getModificationTime)-import           System.FilePath        (addExtension, (</>))------------------------------------------------------------------------------------#if !MIN_VERSION_directory(1,2,0)-import           Data.Time              (readTime)-import           System.Locale          (defaultTimeLocale)-import           System.Time            (formatCalendarTime, toCalendarTime)-#endif------------------------------------------------------------------------------------import           Hakyll.Core.Identifier-import           Hakyll.Core.Store      (Store)-import qualified Hakyll.Core.Store      as Store-import           Hakyll.Core.Util.File-------------------------------------------------------------------------------------- | Because UTCTime doesn't have a Binary instance...-newtype BinaryTime = BinaryTime {unBinaryTime :: UTCTime}-    deriving (Eq, NFData, Ord, Show, Typeable)------------------------------------------------------------------------------------instance Binary BinaryTime where-    put (BinaryTime (UTCTime (ModifiedJulianDay d) dt)) =-        put d >> put (toRational dt)--    get = fmap BinaryTime $ UTCTime-        <$> (ModifiedJulianDay <$> get)-        <*> (fromRational <$> get)------------------------------------------------------------------------------------data ResourceInfo = ResourceInfo-    { resourceInfoModified :: BinaryTime-    , resourceInfoMetadata :: Maybe Identifier-    } deriving (Show, Typeable)------------------------------------------------------------------------------------instance Binary ResourceInfo where-    put (ResourceInfo mtime meta) = put mtime >> put meta-    get = ResourceInfo <$> get <*> get------------------------------------------------------------------------------------instance NFData ResourceInfo where-    rnf (ResourceInfo mtime meta) = rnf mtime `seq` rnf meta `seq` ()-------------------------------------------------------------------------------------- | Responsible for retrieving and listing resources-data Provider = Provider-    { -- Top of the provided directory-      providerDirectory :: FilePath-    , -- | A list of all files found-      providerFiles     :: Map Identifier ResourceInfo-    , -- | A list of the files from the previous run-      providerOldFiles  :: Map Identifier ResourceInfo-    , -- | Underlying persistent store for caching-      providerStore     :: Store-    } deriving (Show)-------------------------------------------------------------------------------------- | Create a resource provider-newProvider :: Store                  -- ^ Store to use-            -> (FilePath -> IO Bool)  -- ^ Should we ignore this file?-            -> FilePath               -- ^ Search directory-            -> IO Provider            -- ^ Resulting provider-newProvider store ignore directory = do-    list <- map fromFilePath <$> getRecursiveContents ignore directory-    let universe = S.fromList list-    files <- fmap (maxmtime . M.fromList) $ forM list $ \identifier -> do-        rInfo <- getResourceInfo directory universe identifier-        return (identifier, rInfo)--    -- Get the old files from the store, and then immediately replace them by-    -- the new files.-    oldFiles <- fromMaybe mempty . Store.toMaybe <$> Store.get store oldKey-    oldFiles `deepseq` Store.set store oldKey files--    return $ Provider directory files oldFiles store-  where-    oldKey = ["Hakyll.Core.Provider.Internal.newProvider", "oldFiles"]--    -- Update modified if metadata is modified-    maxmtime files = flip M.map files $ \rInfo@(ResourceInfo mtime meta) ->-        let metaMod = fmap resourceInfoModified $ meta >>= flip M.lookup files-        in rInfo {resourceInfoModified = maybe mtime (max mtime) metaMod}------------------------------------------------------------------------------------getResourceInfo :: FilePath -> Set Identifier -> Identifier -> IO ResourceInfo-getResourceInfo directory universe identifier = do-    mtime <- fileModificationTime $ directory </> toFilePath identifier-    return $ ResourceInfo (BinaryTime mtime) $-        if mdRsc `S.member` universe then Just mdRsc else Nothing-  where-    mdRsc = fromFilePath $ flip addExtension "metadata" $ toFilePath identifier------------------------------------------------------------------------------------resourceList :: Provider -> [Identifier]-resourceList = M.keys . providerFiles-------------------------------------------------------------------------------------- | Check if a given resource exists-resourceExists :: Provider -> Identifier -> Bool-resourceExists provider =-    (`M.member` providerFiles provider) . setVersion Nothing------------------------------------------------------------------------------------resourceFilePath :: Provider -> Identifier -> FilePath-resourceFilePath p i = providerDirectory p </> toFilePath i-------------------------------------------------------------------------------------- | Get the raw body of a resource as string-resourceString :: Provider -> Identifier -> IO String-resourceString p i = readFile $ resourceFilePath p i-------------------------------------------------------------------------------------- | Get the raw body of a resource of a lazy bytestring-resourceLBS :: Provider -> Identifier -> IO BL.ByteString-resourceLBS p i = BL.readFile $ resourceFilePath p i-------------------------------------------------------------------------------------- | A resource is modified if it or its metadata has changed-resourceModified :: Provider -> Identifier -> Bool-resourceModified p r = case (ri, oldRi) of-    (Nothing, _)      -> False-    (Just _, Nothing) -> True-    (Just n, Just o)  ->-        resourceInfoModified n >  resourceInfoModified o ||-        resourceInfoMetadata n /= resourceInfoMetadata o-  where-    normal = setVersion Nothing r-    ri     = M.lookup normal (providerFiles p)-    oldRi  = M.lookup normal (providerOldFiles p)------------------------------------------------------------------------------------resourceModificationTime :: Provider -> Identifier -> UTCTime-resourceModificationTime p i =-    case M.lookup (setVersion Nothing i) (providerFiles p) of-        Just ri -> unBinaryTime $ resourceInfoModified ri-        Nothing -> error $-            "Hakyll.Core.Provider.Internal.resourceModificationTime: " ++-            "resource " ++ show i ++ " does not exist"------------------------------------------------------------------------------------fileModificationTime :: FilePath -> IO UTCTime-fileModificationTime fp = do-#if MIN_VERSION_directory(1,2,0)-    getModificationTime fp-#else-    ct <- toCalendarTime =<< getModificationTime fp-    let str = formatCalendarTime defaultTimeLocale "%s" ct-    return $ readTime defaultTimeLocale "%s" str-#endif
− src/Hakyll/Core/Provider/Metadata.hs
@@ -1,151 +0,0 @@------------------------------------------------------------------------------------ | Internal module to parse metadata-{-# LANGUAGE BangPatterns    #-}-{-# LANGUAGE RecordWildCards #-}-module Hakyll.Core.Provider.Metadata-    ( loadMetadata-    , parsePage--    , MetadataException (..)-    ) where------------------------------------------------------------------------------------import           Control.Arrow                 (second)-import           Control.Exception             (Exception, throwIO)-import           Control.Monad                 (guard)-import qualified Data.ByteString               as B-import qualified Data.ByteString.Char8         as BC-import           Data.List.Extended            (breakWhen)-import qualified Data.Map                      as M-import           Data.Maybe                    (fromMaybe)-import           Data.Monoid                   ((<>))-import qualified Data.Text                     as T-import qualified Data.Text.Encoding            as T-import qualified Data.Yaml                     as Yaml-import           Hakyll.Core.Identifier-import           Hakyll.Core.Metadata-import           Hakyll.Core.Provider.Internal-import           System.IO                     as IO------------------------------------------------------------------------------------loadMetadata :: Provider -> Identifier -> IO (Metadata, Maybe String)-loadMetadata p identifier = do-    hasHeader  <- probablyHasMetadataHeader fp-    (md, body) <- if hasHeader-        then second Just <$> loadMetadataHeader fp-        else return (mempty, Nothing)--    emd <- case mi of-        Nothing  -> return mempty-        Just mi' -> loadMetadataFile $ resourceFilePath p mi'--    return (md <> emd, body)-  where-    normal = setVersion Nothing identifier-    fp     = resourceFilePath p identifier-    mi     = M.lookup normal (providerFiles p) >>= resourceInfoMetadata------------------------------------------------------------------------------------loadMetadataHeader :: FilePath -> IO (Metadata, String)-loadMetadataHeader fp = do-    fileContent <- readFile fp-    case parsePage fileContent of-        Right x   -> return x-        Left  err -> throwIO $ MetadataException fp err------------------------------------------------------------------------------------loadMetadataFile :: FilePath -> IO Metadata-loadMetadataFile fp = do-    fileContent <- B.readFile fp-    let errOrMeta = Yaml.decodeEither' fileContent-    either (fail . show) return errOrMeta-------------------------------------------------------------------------------------- | Check if a file "probably" has a metadata header. The main goal of this is--- to exclude binary files (which are unlikely to start with "---").-probablyHasMetadataHeader :: FilePath -> IO Bool-probablyHasMetadataHeader fp = do-    handle <- IO.openFile fp IO.ReadMode-    bs     <- BC.hGet handle 1024-    IO.hClose handle-    return $ isMetadataHeader bs-  where-    isMetadataHeader bs =-        let pre = BC.takeWhile (\x -> x /= '\n' && x /= '\r') bs-        in  BC.length pre >= 3 && BC.all (== '-') pre-------------------------------------------------------------------------------------- | Parse the page metadata and body.-splitMetadata :: String -> (Maybe String, String)-splitMetadata str0 = fromMaybe (Nothing, str0) $ do-    guard $ leading >= 3-    let !str1 = drop leading str0-    guard $ all isNewline (take 1 str1)-    let !(!meta, !content0) = breakWhen isTrailing str1-    guard $ not $ null content0-    let !content1 = drop (leading + 1) content0-        !content2 = dropWhile isNewline $ dropWhile isInlineSpace content1-    -- Adding this newline fixes the line numbers reported by the YAML parser.-    -- It's a bit ugly but it works.-    return (Just ('\n' : meta), content2)-  where-    -- Parse the leading "---"-    !leading = length $ takeWhile (== '-') str0--    -- Predicate to recognize the trailing "---" or "..."-    isTrailing []       = False-    isTrailing (x : xs) =-        isNewline x && length (takeWhile isDash xs) == leading--    -- Characters-    isNewline     c = c == '\n' || c == '\r'-    isDash        c = c == '-'  || c == '.'-    isInlineSpace c = c == '\t' || c == ' '------------------------------------------------------------------------------------parseMetadata :: String -> Either Yaml.ParseException Metadata-parseMetadata = Yaml.decodeEither' . T.encodeUtf8 . T.pack------------------------------------------------------------------------------------parsePage :: String -> Either Yaml.ParseException (Metadata, String)-parsePage fileContent = case mbMetaBlock of-    Nothing        -> return (mempty, content)-    Just metaBlock -> case parseMetadata metaBlock of-        Left  err  -> Left   err-        Right meta -> return (meta, content)-  where-    !(!mbMetaBlock, !content) = splitMetadata fileContent-------------------------------------------------------------------------------------- | Thrown in the IO monad if things go wrong. Provides a nice-ish error--- message.-data MetadataException = MetadataException FilePath Yaml.ParseException------------------------------------------------------------------------------------instance Exception MetadataException------------------------------------------------------------------------------------instance Show MetadataException where-    show (MetadataException fp err) =-        fp ++ ": " ++ Yaml.prettyPrintParseException err ++ hint--      where-        hint = case err of-            Yaml.InvalidYaml (Just (Yaml.YamlParseException {..}))-                | yamlProblem == problem -> "\n" ++-                    "Hint: if the metadata value contains characters such\n" ++-                    "as ':' or '-', try enclosing it in quotes."-            _ -> ""--        problem = "mapping values are not allowed in this context"
− src/Hakyll/Core/Provider/MetadataCache.hs
@@ -1,62 +0,0 @@----------------------------------------------------------------------------------module Hakyll.Core.Provider.MetadataCache-    ( resourceMetadata-    , resourceBody-    , resourceInvalidateMetadataCache-    ) where------------------------------------------------------------------------------------import           Control.Monad                 (unless)-import           Hakyll.Core.Identifier-import           Hakyll.Core.Metadata-import           Hakyll.Core.Provider.Internal-import           Hakyll.Core.Provider.Metadata-import qualified Hakyll.Core.Store             as Store------------------------------------------------------------------------------------resourceMetadata :: Provider -> Identifier -> IO Metadata-resourceMetadata p r-    | not (resourceExists p r) = return mempty-    | otherwise                = do-        -- TODO keep time in md cache-        load p r-        Store.Found (BinaryMetadata md) <- Store.get (providerStore p)-            [name, toFilePath r, "metadata"]-        return md------------------------------------------------------------------------------------resourceBody :: Provider -> Identifier -> IO String-resourceBody p r = do-    load p r-    Store.Found bd <- Store.get (providerStore p)-        [name, toFilePath r, "body"]-    maybe (resourceString p r) return bd------------------------------------------------------------------------------------resourceInvalidateMetadataCache :: Provider -> Identifier -> IO ()-resourceInvalidateMetadataCache p r = do-    Store.delete (providerStore p) [name, toFilePath r, "metadata"]-    Store.delete (providerStore p) [name, toFilePath r, "body"]------------------------------------------------------------------------------------load :: Provider -> Identifier -> IO ()-load p r = do-    mmof <- Store.isMember store mdk-    unless mmof $ do-        (md, body) <- loadMetadata p r-        Store.set store mdk (BinaryMetadata md)-        Store.set store bk  body-  where-    store = providerStore p-    mdk   = [name, toFilePath r, "metadata"]-    bk    = [name, toFilePath r, "body"]------------------------------------------------------------------------------------name :: String-name = "Hakyll.Core.Resource.Provider.MetadataCache"
− src/Hakyll/Core/Routes.hs
@@ -1,194 +0,0 @@------------------------------------------------------------------------------------ | Once a target is compiled, the user usually wants to save it to the disk.--- This is where the 'Routes' type comes in; it determines where a certain--- target should be written.------ Suppose we have an item @foo\/bar.markdown@. We can render this to--- @foo\/bar.html@ using:------ > route "foo/bar.markdown" (setExtension ".html")------ If we do not want to change the extension, we can use 'idRoute', the simplest--- route available:------ > route "foo/bar.markdown" idRoute------ That will route @foo\/bar.markdown@ to @foo\/bar.markdown@.------ Note that the extension says nothing about the content! If you set the--- extension to @.html@, it is your own responsibility to ensure that the--- content is indeed HTML.------ Finally, some special cases:------ * If there is no route for an item, this item will not be routed, so it will---   not appear in your site directory.------ * If an item matches multiple routes, the first rule will be chosen.-{-# LANGUAGE Rank2Types #-}-module Hakyll.Core.Routes-    ( UsedMetadata-    , Routes-    , runRoutes-    , idRoute-    , setExtension-    , matchRoute-    , customRoute-    , constRoute-    , gsubRoute-    , metadataRoute-    , composeRoutes-    ) where------------------------------------------------------------------------------------import           System.FilePath                (replaceExtension)------------------------------------------------------------------------------------import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Metadata-import           Hakyll.Core.Provider-import           Hakyll.Core.Util.String-------------------------------------------------------------------------------------- | When you ran a route, it's useful to know whether or not this used--- metadata. This allows us to do more granular dependency analysis.-type UsedMetadata = Bool------------------------------------------------------------------------------------data RoutesRead = RoutesRead-    { routesProvider   :: Provider-    , routesUnderlying :: Identifier-    }-------------------------------------------------------------------------------------- | Type used for a route-newtype Routes = Routes-    { unRoutes :: RoutesRead -> Identifier -> IO (Maybe FilePath, UsedMetadata)-    }------------------------------------------------------------------------------------instance Monoid Routes where-    mempty = Routes $ \_ _ -> return (Nothing, False)-    mappend (Routes f) (Routes g) = Routes $ \p id' -> do-        (mfp, um) <- f p id'-        case mfp of-            Nothing -> g p id'-            Just _  -> return (mfp, um)-------------------------------------------------------------------------------------- | Apply a route to an identifier-runRoutes :: Routes -> Provider -> Identifier-          -> IO (Maybe FilePath, UsedMetadata)-runRoutes routes provider identifier =-    unRoutes routes (RoutesRead provider identifier) identifier-------------------------------------------------------------------------------------- | A route that uses the identifier as filepath. For example, the target with--- ID @foo\/bar@ will be written to the file @foo\/bar@.-idRoute :: Routes-idRoute = customRoute toFilePath-------------------------------------------------------------------------------------- | Set (or replace) the extension of a route.------ Example:------ > runRoutes (setExtension "html") "foo/bar"------ Result:------ > Just "foo/bar.html"------ Example:------ > runRoutes (setExtension "html") "posts/the-art-of-trolling.markdown"------ Result:------ > Just "posts/the-art-of-trolling.html"-setExtension :: String -> Routes-setExtension extension = customRoute $-    (`replaceExtension` extension) . toFilePath-------------------------------------------------------------------------------------- | Apply the route if the identifier matches the given pattern, fail--- otherwise-matchRoute :: Pattern -> Routes -> Routes-matchRoute pattern (Routes route) = Routes $ \p id' ->-    if matches pattern id' then route p id' else return (Nothing, False)-------------------------------------------------------------------------------------- | Create a custom route. This should almost always be used with--- 'matchRoute'-customRoute :: (Identifier -> FilePath) -> Routes-customRoute f = Routes $ const $ \id' -> return (Just (f id'), False)-------------------------------------------------------------------------------------- | A route that always gives the same result. Obviously, you should only use--- this for a single compilation rule.-constRoute :: FilePath -> Routes-constRoute = customRoute . const-------------------------------------------------------------------------------------- | Create a gsub route------ Example:------ > runRoutes (gsubRoute "rss/" (const "")) "tags/rss/bar.xml"------ Result:------ > Just "tags/bar.xml"-gsubRoute :: String              -- ^ Pattern-          -> (String -> String)  -- ^ Replacement-          -> Routes              -- ^ Resulting route-gsubRoute pattern replacement = customRoute $-    replaceAll pattern replacement . toFilePath-------------------------------------------------------------------------------------- | Get access to the metadata in order to determine the route-metadataRoute :: (Metadata -> Routes) -> Routes-metadataRoute f = Routes $ \r i -> do-    metadata <- resourceMetadata (routesProvider r) (routesUnderlying r)-    unRoutes (f metadata) r i-------------------------------------------------------------------------------------- | Compose routes so that @f \`composeRoutes\` g@ is more or less equivalent--- with @g . f@.------ Example:------ > let routes = gsubRoute "rss/" (const "") `composeRoutes` setExtension "xml"--- > in runRoutes routes "tags/rss/bar"------ Result:------ > Just "tags/bar.xml"------ If the first route given fails, Hakyll will not apply the second route.-composeRoutes :: Routes  -- ^ First route to apply-              -> Routes  -- ^ Second route to apply-              -> Routes  -- ^ Resulting route-composeRoutes (Routes f) (Routes g) = Routes $ \p i -> do-    (mfp, um) <- f p i-    case mfp of-        Nothing -> return (Nothing, um)-        Just fp -> do-            (mfp', um') <- g p (fromFilePath fp)-            return (mfp', um || um')
− src/Hakyll/Core/Rules.hs
@@ -1,223 +0,0 @@------------------------------------------------------------------------------------ | This module provides a declarative DSL in which the user can specify the--- different rules used to run the compilers.------ The convention is to just list all items in the 'Rules' monad, routes and--- compilation rules.------ A typical usage example would be:------ > main = hakyll $ do--- >     match "posts/*" $ do--- >         route   (setExtension "html")--- >         compile someCompiler--- >     match "css/*" $ do--- >         route   idRoute--- >         compile compressCssCompiler-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings          #-}-module Hakyll.Core.Rules-    ( Rules-    , match-    , matchMetadata-    , create-    , version-    , compile-    , route--      -- * Advanced usage-    , preprocess-    , Dependency (..)-    , rulesExtraDependencies-    ) where------------------------------------------------------------------------------------import           Control.Monad.Reader           (ask, local)-import           Control.Monad.State            (get, modify, put)-import           Control.Monad.Trans            (liftIO)-import           Control.Monad.Writer           (censor, tell)-import           Data.Maybe                     (fromMaybe)-import qualified Data.Set                       as S------------------------------------------------------------------------------------import           Data.Binary                    (Binary)-import           Data.Typeable                  (Typeable)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Dependencies-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Item-import           Hakyll.Core.Item.SomeItem-import           Hakyll.Core.Metadata-import           Hakyll.Core.Routes-import           Hakyll.Core.Rules.Internal-import           Hakyll.Core.Writable-------------------------------------------------------------------------------------- | Add a route-tellRoute :: Routes -> Rules ()-tellRoute route' = Rules $ tell $ RuleSet route' mempty mempty mempty-------------------------------------------------------------------------------------- | Add a number of compilers-tellCompilers :: [(Identifier, Compiler SomeItem)] -> Rules ()-tellCompilers compilers = Rules $ tell $ RuleSet mempty compilers mempty mempty-------------------------------------------------------------------------------------- | Add resources-tellResources :: [Identifier] -> Rules ()-tellResources resources' = Rules $ tell $-    RuleSet mempty mempty (S.fromList resources') mempty-------------------------------------------------------------------------------------- | Add a pattern-tellPattern :: Pattern -> Rules ()-tellPattern pattern = Rules $ tell $ RuleSet mempty mempty mempty pattern------------------------------------------------------------------------------------flush :: Rules ()-flush = Rules $ do-    mcompiler <- rulesCompiler <$> get-    case mcompiler of-        Nothing       -> return ()-        Just compiler -> do-            matches' <- rulesMatches                  <$> ask-            version' <- rulesVersion                  <$> ask-            route'   <- fromMaybe mempty . rulesRoute <$> get--            -- The version is possibly not set correctly at this point (yet)-            let ids = map (setVersion version') matches'--            {--            ids      <- case fromLiteral pattern of-                Just id' -> return [setVersion version' id']-                Nothing  -> do-                    ids <- unRules $ getMatches pattern-                    unRules $ tellResources ids-                    return $ map (setVersion version') ids-            -}--            -- Create a fast pattern for routing that matches exactly the-            -- compilers created in the block given to match-            let fastPattern = fromList ids--            -- Write out the compilers and routes-            unRules $ tellRoute $ matchRoute fastPattern route'-            unRules $ tellCompilers $ [(id', compiler) | id' <- ids]--    put $ emptyRulesState------------------------------------------------------------------------------------matchInternal :: Pattern -> Rules [Identifier] -> Rules () -> Rules ()-matchInternal pattern getIDs rules = do-    tellPattern pattern-    flush-    ids <- getIDs-    tellResources ids-    Rules $ local (setMatches ids) $ unRules $ rules >> flush-  where-    setMatches ids env = env {rulesMatches = ids}-----------------------------------------------------------------------------------match :: Pattern -> Rules () -> Rules ()-match pattern = matchInternal pattern $ getMatches pattern------------------------------------------------------------------------------------matchMetadata :: Pattern -> (Metadata -> Bool) -> Rules () -> Rules ()-matchMetadata pattern metadataPred = matchInternal pattern $-    map fst . filter (metadataPred . snd) <$> getAllMetadata pattern------------------------------------------------------------------------------------create :: [Identifier] -> Rules () -> Rules ()-create ids rules = do-    flush-    -- TODO Maybe check if the resources exist and call tellResources on that-    Rules $ local setMatches $ unRules $ rules >> flush-  where-    setMatches env = env {rulesMatches = ids}------------------------------------------------------------------------------------version :: String -> Rules () -> Rules ()-version v rules = do-    flush-    Rules $ local setVersion' $ unRules $ rules >> flush-  where-    setVersion' env = env {rulesVersion = Just v}-------------------------------------------------------------------------------------- | Add a compilation rule to the rules.------ This instructs all resources to be compiled using the given compiler.-compile :: (Binary a, Typeable a, Writable a) => Compiler (Item a) -> Rules ()-compile compiler = Rules $ modify $ \s ->-    s {rulesCompiler = Just (fmap SomeItem compiler)}-------------------------------------------------------------------------------------- | Add a route.------ This adds a route for all items matching the current pattern.-route :: Routes -> Rules ()-route route' = Rules $ modify $ \s -> s {rulesRoute = Just route'}-------------------------------------------------------------------------------------- | Execute an 'IO' action immediately while the rules are being evaluated.--- This should be avoided if possible, but occasionally comes in useful.-preprocess :: IO a -> Rules a-preprocess = Rules . liftIO-------------------------------------------------------------------------------------- | Advanced usage: add extra dependencies to compilers. Basically this is--- needed when you're doing unsafe tricky stuff in the rules monad, but you--- still want correct builds.------ A useful utility for this purpose is 'makePatternDependency'.-rulesExtraDependencies :: [Dependency] -> Rules a -> Rules a-rulesExtraDependencies deps rules =-    -- Note that we add the dependencies seemingly twice here. However, this is-    -- done so that 'rulesExtraDependencies' works both if we have something-    -- like:-    ---    -- > match "*.css" $ rulesExtraDependencies [foo] $ ...-    ---    -- and something like:-    ---    -- > rulesExtraDependencies [foo] $ match "*.css" $ ...-    ---    -- (1) takes care of the latter and (2) of the former.-    Rules $ censor fixRuleSet $ do-        x <- unRules rules-        fixCompiler-        return x-  where-    -- (1) Adds the dependencies to the compilers we are yet to create-    fixCompiler = modify $ \s -> case rulesCompiler s of-        Nothing -> s-        Just c  -> s-            { rulesCompiler = Just $ compilerTellDependencies deps >> c-            }--    -- (2) Adds the dependencies to the compilers that are already in the ruleset-    fixRuleSet ruleSet = ruleSet-        { rulesCompilers =-            [ (i, compilerTellDependencies deps >> c)-            | (i, c) <- rulesCompilers ruleSet-            ]-        }
− src/Hakyll/Core/Rules/Internal.hs
@@ -1,109 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE Rank2Types                 #-}-module Hakyll.Core.Rules.Internal-    ( RulesRead (..)-    , RuleSet (..)-    , RulesState (..)-    , emptyRulesState-    , Rules (..)-    , runRules-    ) where------------------------------------------------------------------------------------import           Control.Monad.Reader           (ask)-import           Control.Monad.RWS              (RWST, runRWST)-import           Control.Monad.Trans            (liftIO)-import qualified Data.Map                       as M-import           Data.Set                       (Set)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Item.SomeItem-import           Hakyll.Core.Metadata-import           Hakyll.Core.Provider-import           Hakyll.Core.Routes------------------------------------------------------------------------------------data RulesRead = RulesRead-    { rulesProvider :: Provider-    , rulesMatches  :: [Identifier]-    , rulesVersion  :: Maybe String-    }------------------------------------------------------------------------------------data RuleSet = RuleSet-    { -- | Accumulated routes-      rulesRoutes    :: Routes-    , -- | Accumulated compilers-      rulesCompilers :: [(Identifier, Compiler SomeItem)]-    , -- | A set of the actually used files-      rulesResources :: Set Identifier-    , -- | A pattern we can use to check if a file *would* be used. This is-      -- needed for the preview server.-      rulesPattern   :: Pattern-    }------------------------------------------------------------------------------------instance Monoid RuleSet where-    mempty = RuleSet mempty mempty mempty mempty-    mappend (RuleSet r1 c1 s1 p1) (RuleSet r2 c2 s2 p2) =-        RuleSet (mappend r1 r2) (mappend c1 c2) (mappend s1 s2) (p1 .||. p2)------------------------------------------------------------------------------------data RulesState = RulesState-    { rulesRoute    :: Maybe Routes-    , rulesCompiler :: Maybe (Compiler SomeItem)-    }------------------------------------------------------------------------------------emptyRulesState :: RulesState-emptyRulesState = RulesState Nothing Nothing-------------------------------------------------------------------------------------- | The monad used to compose rules-newtype Rules a = Rules-    { unRules :: RWST RulesRead RuleSet RulesState IO a-    } deriving (Monad, Functor, Applicative)------------------------------------------------------------------------------------instance MonadMetadata Rules where-    getMetadata identifier = Rules $ do-        provider <- rulesProvider <$> ask-        liftIO $ resourceMetadata provider identifier--    getMatches pattern = Rules $ do-        provider <- rulesProvider <$> ask-        return $ filterMatches pattern $ resourceList provider-------------------------------------------------------------------------------------- | Run a Rules monad, resulting in a 'RuleSet'-runRules :: Rules a -> Provider -> IO RuleSet-runRules rules provider = do-    (_, _, ruleSet) <- runRWST (unRules rules) env emptyRulesState--    -- Ensure compiler uniqueness-    let ruleSet' = ruleSet-            { rulesCompilers = M.toList $-                M.fromListWith (flip const) (rulesCompilers ruleSet)-            }--    return ruleSet'-  where-    env = RulesRead-        { rulesProvider = provider-        , rulesMatches  = []-        , rulesVersion  = Nothing-        }
− src/Hakyll/Core/Runtime.hs
@@ -1,276 +0,0 @@----------------------------------------------------------------------------------module Hakyll.Core.Runtime-    ( run-    ) where------------------------------------------------------------------------------------import           Control.Monad                 (unless)-import           Control.Monad.Except          (ExceptT, runExceptT, throwError)-import           Control.Monad.Reader          (ask)-import           Control.Monad.RWS             (RWST, runRWST)-import           Control.Monad.State           (get, modify)-import           Control.Monad.Trans           (liftIO)-import           Data.List                     (intercalate)-import           Data.Map                      (Map)-import qualified Data.Map                      as M-import           Data.Set                      (Set)-import qualified Data.Set                      as S-import           System.Exit                   (ExitCode (..))-import           System.FilePath               ((</>))------------------------------------------------------------------------------------import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Compiler.Require-import           Hakyll.Core.Configuration-import           Hakyll.Core.Dependencies-import           Hakyll.Core.Identifier-import           Hakyll.Core.Item-import           Hakyll.Core.Item.SomeItem-import           Hakyll.Core.Logger            (Logger)-import qualified Hakyll.Core.Logger            as Logger-import           Hakyll.Core.Provider-import           Hakyll.Core.Routes-import           Hakyll.Core.Rules.Internal-import           Hakyll.Core.Store             (Store)-import qualified Hakyll.Core.Store             as Store-import           Hakyll.Core.Util.File-import           Hakyll.Core.Writable------------------------------------------------------------------------------------run :: Configuration -> Logger -> Rules a -> IO (ExitCode, RuleSet)-run config logger rules = do-    -- Initialization-    Logger.header logger "Initialising..."-    Logger.message logger "Creating store..."-    store <- Store.new (inMemoryCache config) $ storeDirectory config-    Logger.message logger "Creating provider..."-    provider <- newProvider store (shouldIgnoreFile config) $-        providerDirectory config-    Logger.message logger "Running rules..."-    ruleSet  <- runRules rules provider--    -- Get old facts-    mOldFacts <- Store.get store factsKey-    let (oldFacts) = case mOldFacts of Store.Found f -> f-                                       _             -> mempty--    -- Build runtime read/state-    let compilers = rulesCompilers ruleSet-        read'     = RuntimeRead-            { runtimeConfiguration = config-            , runtimeLogger        = logger-            , runtimeProvider      = provider-            , runtimeStore         = store-            , runtimeRoutes        = rulesRoutes ruleSet-            , runtimeUniverse      = M.fromList compilers-            }-        state     = RuntimeState-            { runtimeDone      = S.empty-            , runtimeSnapshots = S.empty-            , runtimeTodo      = M.empty-            , runtimeFacts     = oldFacts-            }--    -- Run the program and fetch the resulting state-    result <- runExceptT $ runRWST build read' state-    case result of-        Left e          -> do-            Logger.error logger e-            Logger.flush logger-            return (ExitFailure 1, ruleSet)--        Right (_, s, _) -> do-            Store.set store factsKey $ runtimeFacts s--            Logger.debug logger "Removing tmp directory..."-            removeDirectory $ tmpDirectory config--            Logger.flush logger-            return (ExitSuccess, ruleSet)-  where-    factsKey = ["Hakyll.Core.Runtime.run", "facts"]------------------------------------------------------------------------------------data RuntimeRead = RuntimeRead-    { runtimeConfiguration :: Configuration-    , runtimeLogger        :: Logger-    , runtimeProvider      :: Provider-    , runtimeStore         :: Store-    , runtimeRoutes        :: Routes-    , runtimeUniverse      :: Map Identifier (Compiler SomeItem)-    }------------------------------------------------------------------------------------data RuntimeState = RuntimeState-    { runtimeDone      :: Set Identifier-    , runtimeSnapshots :: Set (Identifier, Snapshot)-    , runtimeTodo      :: Map Identifier (Compiler SomeItem)-    , runtimeFacts     :: DependencyFacts-    }------------------------------------------------------------------------------------type Runtime a = RWST RuntimeRead () RuntimeState (ExceptT String IO) a------------------------------------------------------------------------------------build :: Runtime ()-build = do-    logger <- runtimeLogger <$> ask-    Logger.header logger "Checking for out-of-date items"-    scheduleOutOfDate-    Logger.header logger "Compiling"-    pickAndChase-    Logger.header logger "Success"------------------------------------------------------------------------------------scheduleOutOfDate :: Runtime ()-scheduleOutOfDate = do-    logger   <- runtimeLogger   <$> ask-    provider <- runtimeProvider <$> ask-    universe <- runtimeUniverse <$> ask-    facts    <- runtimeFacts    <$> get-    todo     <- runtimeTodo     <$> get--    let identifiers = M.keys universe-        modified    = S.fromList $ flip filter identifiers $-            resourceModified provider--    let (ood, facts', msgs) = outOfDate identifiers modified facts-        todo'               = M.filterWithKey-            (\id' _ -> id' `S.member` ood) universe--    -- Print messages-    mapM_ (Logger.debug logger) msgs--    -- Update facts and todo items-    modify $ \s -> s-        { runtimeDone  = runtimeDone s `S.union`-            (S.fromList identifiers `S.difference` ood)-        , runtimeTodo  = todo `M.union` todo'-        , runtimeFacts = facts'-        }------------------------------------------------------------------------------------pickAndChase :: Runtime ()-pickAndChase = do-    todo <- runtimeTodo <$> get-    case M.minViewWithKey todo of-        Nothing            -> return ()-        Just ((id', _), _) -> do-            chase [] id'-            pickAndChase------------------------------------------------------------------------------------chase :: [Identifier] -> Identifier -> Runtime ()-chase trail id'-    | id' `elem` trail = throwError $ "Hakyll.Core.Runtime.chase: " ++-        "Dependency cycle detected: " ++ intercalate " depends on "-            (map show $ dropWhile (/= id') (reverse trail) ++ [id'])-    | otherwise        = do-        logger   <- runtimeLogger        <$> ask-        todo     <- runtimeTodo          <$> get-        provider <- runtimeProvider      <$> ask-        universe <- runtimeUniverse      <$> ask-        routes   <- runtimeRoutes        <$> ask-        store    <- runtimeStore         <$> ask-        config   <- runtimeConfiguration <$> ask-        Logger.debug logger $ "Processing " ++ show id'--        let compiler = todo M.! id'-            read' = CompilerRead-                { compilerConfig     = config-                , compilerUnderlying = id'-                , compilerProvider   = provider-                , compilerUniverse   = M.keysSet universe-                , compilerRoutes     = routes-                , compilerStore      = store-                , compilerLogger     = logger-                }--        result <- liftIO $ runCompiler compiler read'-        case result of-            -- Rethrow error-            CompilerError [] -> throwError-                "Compiler failed but no info given, try running with -v?"-            CompilerError es -> throwError $ intercalate "; " es--            -- Signal that a snapshot was saved ->-            CompilerSnapshot snapshot c -> do-                -- Update info. The next 'chase' will pick us again at some-                -- point so we can continue then.-                modify $ \s -> s-                    { runtimeSnapshots =-                        S.insert (id', snapshot) (runtimeSnapshots s)-                    , runtimeTodo      = M.insert id' c (runtimeTodo s)-                    }--            -- Huge success-            CompilerDone (SomeItem item) cwrite -> do-                -- Print some info-                let facts = compilerDependencies cwrite-                    cacheHits-                        | compilerCacheHits cwrite <= 0 = "updated"-                        | otherwise                     = "cached "-                Logger.message logger $ cacheHits ++ " " ++ show id'--                -- Sanity check-                unless (itemIdentifier item == id') $ throwError $-                    "The compiler yielded an Item with Identifier " ++-                    show (itemIdentifier item) ++ ", but we were expecting " ++-                    "an Item with Identifier " ++ show id' ++ " " ++-                    "(you probably want to call makeItem to solve this problem)"--                -- Write if necessary-                (mroute, _) <- liftIO $ runRoutes routes provider id'-                case mroute of-                    Nothing    -> return ()-                    Just route -> do-                        let path = destinationDirectory config </> route-                        liftIO $ makeDirectories path-                        liftIO $ write path item-                        Logger.debug logger $ "Routed to " ++ path--                -- Save! (For load)-                liftIO $ save store item--                -- Update state-                modify $ \s -> s-                    { runtimeDone  = S.insert id' (runtimeDone s)-                    , runtimeTodo  = M.delete id' (runtimeTodo s)-                    , runtimeFacts = M.insert id' facts (runtimeFacts s)-                    }--            -- Try something else first-            CompilerRequire dep c -> do-                -- Update the compiler so we don't execute it twice-                let (depId, depSnapshot) = dep-                done      <- runtimeDone <$> get-                snapshots <- runtimeSnapshots <$> get--                -- Done if we either completed the entire item (runtimeDone) or-                -- if we previously saved the snapshot (runtimeSnapshots).-                let depDone =-                        depId `S.member` done ||-                        (depId, depSnapshot) `S.member` snapshots--                modify $ \s -> s-                    { runtimeTodo = M.insert id'-                        (if depDone then c else compilerResult result)-                        (runtimeTodo s)-                    }--                -- If the required item is already compiled, continue, or, start-                -- chasing that-                Logger.debug logger $ "Require " ++ show depId ++-                    " (snapshot " ++ depSnapshot ++ "): " ++-                    (if depDone then "OK" else "chasing")-                if depDone then chase trail id' else chase (id' : trail) depId
− src/Hakyll/Core/Store.hs
@@ -1,197 +0,0 @@------------------------------------------------------------------------------------ | A store for storing and retreiving items-{-# LANGUAGE ExistentialQuantification #-}-{-# LANGUAGE ScopedTypeVariables       #-}-module Hakyll.Core.Store-    ( Store-    , Result (..)-    , toMaybe-    , new-    , set-    , get-    , isMember-    , delete-    , hash-    ) where------------------------------------------------------------------------------------import           Control.Exception    (IOException, handle)-import qualified Crypto.Hash.MD5      as MD5-import           Data.Binary          (Binary, decode, encodeFile)-import qualified Data.ByteString      as B-import qualified Data.ByteString.Lazy as BL-import qualified Data.Cache.LRU.IO    as Lru-import           Data.List            (intercalate)-import           Data.Maybe           (isJust)-import qualified Data.Text            as T-import qualified Data.Text.Encoding   as T-import           Data.Typeable        (TypeRep, Typeable, cast, typeOf)-import           System.Directory     (createDirectoryIfMissing)-import           System.Directory     (doesFileExist, removeFile)-import           System.FilePath      ((</>))-import           System.IO            (IOMode (..), hClose, openFile)-import           Text.Printf          (printf)-------------------------------------------------------------------------------------- | Simple wrapper type-data Box = forall a. Typeable a => Box a------------------------------------------------------------------------------------data Store = Store-    { -- | All items are stored on the filesystem-      storeDirectory :: FilePath-    , -- | Optionally, items are also kept in-memory-      storeMap       :: Maybe (Lru.AtomicLRU FilePath Box)-    }------------------------------------------------------------------------------------instance Show Store where-    show _ = "<Store>"-------------------------------------------------------------------------------------- | Result of a store query-data Result a-    = Found a                    -- ^ Found, result-    | NotFound                   -- ^ Not found-    | WrongType TypeRep TypeRep  -- ^ Expected, true type-    deriving (Show, Eq)-------------------------------------------------------------------------------------- | Convert result to 'Maybe'-toMaybe :: Result a -> Maybe a-toMaybe (Found x) = Just x-toMaybe _         = Nothing-------------------------------------------------------------------------------------- | Initialize the store-new :: Bool      -- ^ Use in-memory caching-    -> FilePath  -- ^ Directory to use for hard disk storage-    -> IO Store  -- ^ Store-new inMemory directory = do-    createDirectoryIfMissing True directory-    ref <- if inMemory then Just <$> Lru.newAtomicLRU csize else return Nothing-    return Store-        { storeDirectory = directory-        , storeMap       = ref-        }-  where-    csize = Just 500-------------------------------------------------------------------------------------- | Auxiliary: add an item to the in-memory cache-cacheInsert :: Typeable a => Store -> String -> a -> IO ()-cacheInsert (Store _ Nothing)    _   _     = return ()-cacheInsert (Store _ (Just lru)) key x =-    Lru.insert key (Box x) lru-------------------------------------------------------------------------------------- | Auxiliary: get an item from the in-memory cache-cacheLookup :: forall a. Typeable a => Store -> String -> IO (Result a)-cacheLookup (Store _ Nothing)    _   = return NotFound-cacheLookup (Store _ (Just lru)) key = do-    res <- Lru.lookup key lru-    return $ case res of-        Nothing      -> NotFound-        Just (Box x) -> case cast x of-            Just x' -> Found x'-            Nothing -> WrongType (typeOf (undefined :: a)) (typeOf x)------------------------------------------------------------------------------------cacheIsMember :: Store -> String -> IO Bool-cacheIsMember (Store _ Nothing)    _   = return False-cacheIsMember (Store _ (Just lru)) key = isJust <$> Lru.lookup key lru-------------------------------------------------------------------------------------- | Auxiliary: delete an item from the in-memory cache-cacheDelete :: Store -> String -> IO ()-cacheDelete (Store _ Nothing)    _   = return ()-cacheDelete (Store _ (Just lru)) key = do-    _ <- Lru.delete key lru-    return ()-------------------------------------------------------------------------------------- | Store an item-set :: (Binary a, Typeable a) => Store -> [String] -> a -> IO ()-set store identifier value = do-    encodeFile (storeDirectory store </> key) value-    cacheInsert store key value-  where-    key = hash identifier-------------------------------------------------------------------------------------- | Load an item-get :: (Binary a, Typeable a) => Store -> [String] -> IO (Result a)-get store identifier = do-    -- First check the in-memory map-    ref <- cacheLookup store key-    case ref of-        -- Not found in the map, try the filesystem-        NotFound -> do-            exists <- doesFileExist path-            if not exists-                -- Not found in the filesystem either-                then return NotFound-                -- Found in the filesystem-                else do-                    v <- decodeClose-                    cacheInsert store key v-                    return $ Found v-        -- Found in the in-memory map (or wrong type), just return-        s -> return s-  where-    key  = hash identifier-    path = storeDirectory store </> key--    -- 'decodeFile' from Data.Binary which closes the file ASAP-    decodeClose = do-        h   <- openFile path ReadMode-        lbs <- BL.hGetContents h-        BL.length lbs `seq` hClose h-        return $ decode lbs-------------------------------------------------------------------------------------- | Strict function-isMember :: Store -> [String] -> IO Bool-isMember store identifier = do-    inCache <- cacheIsMember store key-    if inCache then return True else doesFileExist path-  where-    key  = hash identifier-    path = storeDirectory store </> key-------------------------------------------------------------------------------------- | Delete an item-delete :: Store -> [String] -> IO ()-delete store identifier = do-    cacheDelete store key-    deleteFile $ storeDirectory store </> key-  where-    key  = hash identifier-------------------------------------------------------------------------------------- | Delete a file unless it doesn't exist...-deleteFile :: FilePath -> IO ()-deleteFile = handle (\(_ :: IOException) -> return ()) . removeFile-------------------------------------------------------------------------------------- | Mostly meant for internal usage-hash :: [String] -> String-hash = concatMap (printf "%02x") . B.unpack .-    MD5.hash . T.encodeUtf8 . T.pack . intercalate "/"
− src/Hakyll/Core/UnixFilter.hs
@@ -1,159 +0,0 @@-{-# LANGUAGE CPP #-}------------------------------------------------------------------------------------- | A Compiler that supports unix filters.-module Hakyll.Core.UnixFilter-    ( unixFilter-    , unixFilterLBS-    ) where------------------------------------------------------------------------------------import           Control.Concurrent      (forkIO)-import           Control.Concurrent.MVar (newEmptyMVar, putMVar, takeMVar)-import           Control.DeepSeq         (deepseq)-import           Control.Monad           (forM_)-import           Data.ByteString.Lazy    (ByteString)-import qualified Data.ByteString.Lazy    as LB-import           Data.IORef              (newIORef, readIORef, writeIORef)-import           System.Exit             (ExitCode (..))-import           System.IO               (Handle, hClose, hFlush, hGetContents,-                                          hPutStr, hSetEncoding, localeEncoding)-import           System.Process-----------------------------------------------------------------------------------import           Hakyll.Core.Compiler-------------------------------------------------------------------------------------- | Use a unix filter as compiler. For example, we could use the 'rev' program--- as a compiler.------ > rev :: Compiler (Item String)--- > rev = getResourceString >>= withItemBody (unixFilter "rev" [])------ A more realistic example: one can use this to call, for example, the sass--- compiler on CSS files. More information about sass can be found here:------ <http://sass-lang.com/>------ The code is fairly straightforward, given that we use @.scss@ for sass:------ > match "style.scss" $ do--- >     route   $ setExtension "css"--- >     compile $ getResourceString >>=--- >         withItemBody (unixFilter "sass" ["-s", "--scss"]) >>=--- >         return . fmap compressCss-unixFilter :: String           -- ^ Program name-           -> [String]         -- ^ Program args-           -> String           -- ^ Program input-           -> Compiler String  -- ^ Program output-unixFilter = unixFilterWith writer reader-  where-    writer handle input = do-        hSetEncoding handle localeEncoding-        hPutStr handle input-    reader handle = do-        hSetEncoding handle localeEncoding-        out <- hGetContents handle-        deepseq out (return out)-------------------------------------------------------------------------------------- | Variant of 'unixFilter' that should be used for binary files------ > match "music.wav" $ do--- >     route   $ setExtension "ogg"--- >     compile $ getResourceLBS >>= withItemBody (unixFilterLBS "oggenc" ["-"])-unixFilterLBS :: String               -- ^ Program name-              -> [String]             -- ^ Program args-              -> ByteString           -- ^ Program input-              -> Compiler ByteString  -- ^ Program output-unixFilterLBS = unixFilterWith LB.hPutStr $ \handle -> do-    out <- LB.hGetContents handle-    LB.length out `seq` return out-------------------------------------------------------------------------------------- | Overloaded compiler-unixFilterWith :: Monoid o-               => (Handle -> i -> IO ())  -- ^ Writer-               -> (Handle -> IO o)        -- ^ Reader-               -> String                  -- ^ Program name-               -> [String]                -- ^ Program args-               -> i                       -- ^ Program input-               -> Compiler o              -- ^ Program output-unixFilterWith writer reader programName args input = do-    debugCompiler ("Executing external program " ++ programName)-    (output, err, exitCode) <- unsafeCompiler $-        unixFilterIO writer reader programName args input-    forM_ (lines err) debugCompiler-    case exitCode of-        ExitSuccess   -> return output-        ExitFailure e -> fail $-            "Hakyll.Core.UnixFilter.unixFilterWith: " ++-            unwords (programName : args) ++ " gave exit code " ++ show e-------------------------------------------------------------------------------------- | Internally used function-unixFilterIO :: Monoid o-             => (Handle -> i -> IO ())-             -> (Handle -> IO o)-             -> String-             -> [String]-             -> i-             -> IO (o, String, ExitCode)-unixFilterIO writer reader programName args input = do-    -- The problem on Windows is that `proc` is unable to execute-    -- batch stubs (eg. anything created using 'gem install ...') even if its in-    -- `$PATH`. A solution to this issue is to execute the batch file explicitly-    -- using `cmd /c batchfile` but there is no rational way to know where said-    -- batchfile is on the system. Hence, we detect windows using the-    -- CPP and instead of using `proc` to create the process, use `shell`-    -- which will be able to execute everything `proc` can-    -- as well as batch files.-#ifdef mingw32_HOST_OS-    let pr = shell $ unwords (programName : args)-#else-    let pr = proc programName args-#endif--    (Just inh, Just outh, Just errh, pid) <--        createProcess pr-                { std_in  = CreatePipe-                , std_out = CreatePipe-                , std_err = CreatePipe-                }--    -- Create boxes-    lock   <- newEmptyMVar-    outRef <- newIORef mempty-    errRef <- newIORef ""--    -- Write the input to the child pipe-    _ <- forkIO $ writer inh input >> hFlush inh >> hClose inh--    -- Read from stdout-    _ <- forkIO $ do-        out <- reader outh-        hClose outh-        writeIORef outRef out-        putMVar lock ()--    -- Read from stderr-    _ <- forkIO $ do-        hSetEncoding errh localeEncoding-        err <- hGetContents errh-        _   <- deepseq err (return err)-        hClose errh-        writeIORef errRef err-        putMVar lock ()--    -- Get exit code & return-    takeMVar lock-    takeMVar lock-    exitCode <- waitForProcess pid-    out      <- readIORef outRef-    err      <- readIORef errRef-    return (out, err, exitCode)
− src/Hakyll/Core/Util/File.hs
@@ -1,56 +0,0 @@------------------------------------------------------------------------------------ | A module containing various file utility functions-module Hakyll.Core.Util.File-    ( makeDirectories-    , getRecursiveContents-    , removeDirectory-    ) where------------------------------------------------------------------------------------import           Control.Monad       (filterM, forM, when)-import           System.Directory    (createDirectoryIfMissing,-                                      doesDirectoryExist, getDirectoryContents,-                                      removeDirectoryRecursive)-import           System.FilePath     (takeDirectory, (</>))-------------------------------------------------------------------------------------- | Given a path to a file, try to make the path writable by making---   all directories on the path.-makeDirectories :: FilePath -> IO ()-makeDirectories = createDirectoryIfMissing True . takeDirectory-------------------------------------------------------------------------------------- | Get all contents of a directory.-getRecursiveContents :: (FilePath -> IO Bool)  -- ^ Ignore this file/directory-                     -> FilePath               -- ^ Directory to search-                     -> IO [FilePath]          -- ^ List of files found-getRecursiveContents ignore top = go ""-  where-    isProper x-        | x `elem` [".", ".."] = return False-        | otherwise            = not <$> ignore x--    go dir     = do-        dirExists <- doesDirectoryExist (top </> dir)-        if not dirExists-            then return []-            else do-                names <- filterM isProper =<< getDirectoryContents (top </> dir)-                paths <- forM names $ \name -> do-                    let rel = dir </> name-                    isDirectory <- doesDirectoryExist (top </> rel)-                    if isDirectory-                        then go rel-                        else return [rel]--                return $ concat paths------------------------------------------------------------------------------------removeDirectory :: FilePath -> IO ()-removeDirectory fp = do-    e <- doesDirectoryExist fp-    when e $ removeDirectoryRecursive fp
− src/Hakyll/Core/Util/Parser.hs
@@ -1,32 +0,0 @@------------------------------------------------------------------------------------ | Parser utilities-module Hakyll.Core.Util.Parser-    ( metadataKey-    , reservedKeys-    ) where------------------------------------------------------------------------------------import           Control.Applicative ((<|>))-import           Control.Monad       (guard, mzero, void)-import qualified Text.Parsec         as P-import           Text.Parsec.String  (Parser)------------------------------------------------------------------------------------metadataKey :: Parser String-metadataKey = do-    -- Ensure trailing '-' binds to '$' if present.-    let hyphon = P.try $ do-            void $ P.char '-'-            x <- P.lookAhead P.anyChar-            guard $ x /= '$'-            pure '-'--    i <- (:) <$> P.letter <*> P.many (P.alphaNum <|> P.oneOf "_." <|> hyphon)-    if i `elem` reservedKeys then mzero else return i------------------------------------------------------------------------------------reservedKeys :: [String]-reservedKeys = ["if", "else", "endif", "for", "sep", "endfor", "partial"]
− src/Hakyll/Core/Util/String.hs
@@ -1,78 +0,0 @@-{-# LANGUAGE FlexibleContexts #-}------------------------------------------------------------------------------------ | Miscellaneous string manipulation functions.-module Hakyll.Core.Util.String-    ( trim-    , replaceAll-    , splitAll-    , needlePrefix-    ) where------------------------------------------------------------------------------------import Data.Char (isSpace)-import Data.List (isPrefixOf)-import Data.Maybe (listToMaybe)-import Text.Regex.TDFA ((=~~))-------------------------------------------------------------------------------------- | Trim a string (drop spaces, tabs and newlines at both sides).-trim :: String -> String-trim = reverse . trim' . reverse . trim'-  where-    trim' = dropWhile isSpace-------------------------------------------------------------------------------------- | A simple (but inefficient) regex replace funcion-replaceAll :: String              -- ^ Pattern-           -> (String -> String)  -- ^ Replacement (called on capture)-           -> String              -- ^ Source string-           -> String              -- ^ Result-replaceAll pattern f source = replaceAll' source-  where-    replaceAll' src = case listToMaybe (src =~~ pattern) of-        Nothing     -> src-        Just (o, l) ->-            let (before, tmp) = splitAt o src-                (capture, after) = splitAt l tmp-            in before ++ f capture ++ replaceAll' after-------------------------------------------------------------------------------------- | A simple regex split function. The resulting list will contain no empty--- strings.-splitAll :: String    -- ^ Pattern-         -> String    -- ^ String to split-         -> [String]  -- ^ Result-splitAll pattern = filter (not . null) . splitAll'-  where-    splitAll' src = case listToMaybe (src =~~ pattern) of-        Nothing     -> [src]-        Just (o, l) ->-            let (before, tmp) = splitAt o src-            in before : splitAll' (drop l tmp)--------------------------------------------------------------------------------------- | Find the first instance of needle (must be non-empty) in haystack. We--- return the prefix of haystack before needle is matched.------ Examples:------ > needlePrefix "cd" "abcde" = "ab"------ > needlePrefix "ab" "abc" = ""------ > needlePrefix "ab" "xxab" = "xx"------ > needlePrefix "a" "xx" = "xx"-needlePrefix :: String -> String -> Maybe String-needlePrefix needle haystack = go [] haystack-  where-    go _   []                     = Nothing-    go acc xss@(x:xs)-        | needle `isPrefixOf` xss = Just $ reverse acc-        | otherwise               = go (x : acc) xs
− src/Hakyll/Core/Writable.hs
@@ -1,56 +0,0 @@------------------------------------------------------------------------------------ | Describes writable items; items that can be saved to the disk-{-# LANGUAGE FlexibleInstances    #-}-{-# LANGUAGE TypeSynonymInstances #-}-module Hakyll.Core.Writable-    ( Writable (..)-    ) where------------------------------------------------------------------------------------import qualified Data.ByteString                 as SB-import qualified Data.ByteString.Lazy            as LB-import           Data.Word                       (Word8)-import           Text.Blaze.Html                 (Html)-import           Text.Blaze.Html.Renderer.String (renderHtml)------------------------------------------------------------------------------------import           Hakyll.Core.Item-------------------------------------------------------------------------------------- | Describes an item that can be saved to the disk-class Writable a where-    -- | Save an item to the given filepath-    write :: FilePath -> Item a -> IO ()------------------------------------------------------------------------------------instance Writable () where-    write _ _ = return ()------------------------------------------------------------------------------------instance Writable [Char] where-    write p = writeFile p . itemBody------------------------------------------------------------------------------------instance Writable SB.ByteString where-    write p = SB.writeFile p . itemBody------------------------------------------------------------------------------------instance Writable LB.ByteString where-    write p = LB.writeFile p . itemBody------------------------------------------------------------------------------------instance Writable [Word8] where-    write p = write p . fmap SB.pack------------------------------------------------------------------------------------instance Writable Html where-    write p = write p . fmap renderHtml
− src/Hakyll/Init.hs
@@ -1,96 +0,0 @@----------------------------------------------------------------------------------module Main-    ( main-    ) where------------------------------------------------------------------------------------import           Control.Arrow         (first)-import           Control.Monad         (forM_)-import           Data.Char             (isAlphaNum, isNumber)-import           Data.List             (foldl')-import           Data.List             (intercalate, isPrefixOf)-import           Data.Version          (Version (..))-import           System.Directory      (canonicalizePath, copyFile)-import           System.Environment    (getArgs, getProgName)-import           System.Exit           (exitFailure)-import           System.FilePath       (splitDirectories, (</>))------------------------------------------------------------------------------------import           Hakyll.Core.Util.File-import           Paths_hakyll------------------------------------------------------------------------------------main :: IO ()-main = do-    progName <- getProgName-    args     <- getArgs-    srcDir   <- getDataFileName "example"-    files    <- getRecursiveContents (const $ return False) srcDir--    case args of-        -- When the argument begins with hyphens, it's more likely that the user-        -- intends to attempt some arguments like ("--help", "-h", "--version", etc.)-        -- rather than create directory with that name.-        -- If dstDir begins with hyphens, the guard will prevent it from creating-        -- directory with that name so we can fall to the second alternative-        -- which prints a usage info for user.-        [dstDir] | not ("-" `isPrefixOf` dstDir) -> do-            forM_ files $ \file -> do-                let dst = dstDir </> file-                    src = srcDir </> file-                putStrLn $ "Creating " ++ dst-                makeDirectories dst-                copyFile src dst--            name <- makeName dstDir-            let cabalPath = dstDir </> name ++ ".cabal"-            putStrLn $ "Creating " ++ cabalPath-            createCabal cabalPath name-        _ -> do-            putStrLn $ "Usage: " ++ progName ++ " <directory>"-            exitFailure---- | Figure out a good cabal package name from the given (existing) directory--- name-makeName :: FilePath -> IO String-makeName dstDir = do-    canonical <- canonicalizePath dstDir-    return $ case safeLast (splitDirectories canonical) of-        Nothing  -> fallbackName-        Just "/" -> fallbackName-        Just x   -> repair (fallbackName ++) id x-  where-    -- Package name repair code comes from-    -- cabal-install.Distribution.Client.Init.Heuristics-    repair invalid valid x = case dropWhile (not . isAlphaNum) x of-        "" -> repairComponent ""-        x' -> let (c, r) = first repairComponent $ break (not . isAlphaNum) x'-              in c ++ repairRest r-      where repairComponent c | all isNumber c = invalid c-                              | otherwise = valid c-    repairRest = repair id ('-' :)-    fallbackName = "site"--    safeLast = foldl' (\_ x -> Just x) Nothing--createCabal :: FilePath -> String -> IO ()-createCabal path name = do-    writeFile path $ unlines [-        "name:               " ++ name-      , "version:            0.1.0.0"-      , "build-type:         Simple"-      , "cabal-version:      >= 1.10"-      , ""-      , "executable site"-      , "  main-is:          site.hs"-      , "  build-depends:    base == 4.*"-      , "                  , hakyll == " ++ version' ++ ".*"-      , "  ghc-options:      -threaded"-      , "  default-language: Haskell2010"-      ]-  where-    -- Major hakyll version-    version' = intercalate "." . take 2 . map show $ versionBranch version
− src/Hakyll/Main.hs
@@ -1,165 +0,0 @@------------------------------------------------------------------------------------ | Module providing the main hakyll function and command-line argument parsing-{-# LANGUAGE CPP #-}--module Hakyll.Main-    ( hakyll-    , hakyllWith-    , hakyllWithArgs-    , hakyllWithExitCode-    ) where------------------------------------------------------------------------------------import           System.Environment        (getProgName)-import           System.Exit               (ExitCode (ExitSuccess), exitWith)-import           System.IO.Unsafe          (unsafePerformIO)------------------------------------------------------------------------------------import           Data.Monoid               ((<>))-import qualified Options.Applicative       as OA------------------------------------------------------------------------------------import qualified Hakyll.Check              as Check-import qualified Hakyll.Commands           as Commands-import qualified Hakyll.Core.Configuration as Config-import qualified Hakyll.Core.Logger        as Logger-import           Hakyll.Core.Rules-------------------------------------------------------------------------------------- | This usually is the function with which the user runs the hakyll compiler-hakyll :: Rules a -> IO ()-hakyll = hakyllWith Config.defaultConfiguration------------------------------------------------------------------------------------- | A variant of 'hakyll' which allows the user to specify a custom--- configuration-hakyllWith :: Config.Configuration -> Rules a -> IO ()-hakyllWith conf rules = hakyllWithExitCode conf rules >>= exitWith------------------------------------------------------------------------------------- | A variant of 'hakyll' which returns an 'ExitCode'-hakyllWithExitCode :: Config.Configuration -> Rules a -> IO ExitCode-hakyllWithExitCode conf rules =  do-    args <- defaultParser conf-    hakyllWithExitCodeAndArgs conf args rules------------------------------------------------------------------------------------- | A variant of 'hakyll' which expects a 'Configuration' and command-line--- 'Options'. This gives freedom to implement your own parsing.-hakyllWithArgs :: Config.Configuration -> Options -> Rules a -> IO ()-hakyllWithArgs conf args rules =-    hakyllWithExitCodeAndArgs conf args rules >>= exitWith-----------------------------------------------------------------------------------hakyllWithExitCodeAndArgs :: Config.Configuration ->-                              Options -> Rules a -> IO ExitCode-hakyllWithExitCodeAndArgs conf args rules = do-    let args' = optCommand args-        verbosity' = if verbosity args then Logger.Debug else Logger.Message-        check     =-            if internal_links args' then Check.InternalLinks else Check.All--    logger <- Logger.new verbosity'-    invokeCommands args' conf check logger rules-----------------------------------------------------------------------------------defaultParser :: Config.Configuration -> IO Options-defaultParser conf =-    OA.customExecParser (OA.prefs OA.showHelpOnError)-        (OA.info (OA.helper <*> optionParser conf)-        (OA.fullDesc <> OA.progDesc-        (progName ++ " - Static site compiler created with Hakyll")))------------------------------------------------------------------------------------invokeCommands :: Command -> Config.Configuration ->-                  Check.Check -> Logger.Logger -> Rules a -> IO ExitCode-invokeCommands args conf check logger rules =-    case args of-        Build          -> Commands.build conf logger rules-        Check   _      -> Commands.check conf logger check >> ok-        Clean          -> Commands.clean conf logger >> ok-        Deploy         -> Commands.deploy conf-        Preview p      -> Commands.preview conf logger rules p >> ok-        Rebuild        -> Commands.rebuild conf logger rules-        Server  _ _    -> Commands.server conf logger (host args) (port args) >> ok-        Watch   _ p s  -> Commands.watch conf logger (host args) p (not s) rules >> ok-    where-        ok = return ExitSuccess-------------------------------------------------------------------------------------data Options = Options {verbosity :: Bool, optCommand :: Command}-    deriving (Show)--data Command-    = Build-    | Check   {internal_links :: Bool}-    | Clean-    | Deploy-    | Preview {port :: Int}-    | Rebuild-    | Server  {host :: String, port :: Int}-    | Watch   {host :: String, port :: Int, no_server :: Bool }-    deriving (Show)--optionParser :: Config.Configuration -> OA.Parser Options-optionParser conf = Options <$> verboseParser <*> commandParser conf-    where-    verboseParser = OA.switch (OA.long "verbose" <> OA.short 'v' <> OA.help "Run in verbose mode")---commandParser :: Config.Configuration -> OA.Parser Command-commandParser conf = OA.subparser $ foldr ((<>) . produceCommand) mempty commands-    where-    portParser = OA.option OA.auto (OA.long "port" <> OA.help "Port to listen on" <> OA.value (Config.previewPort conf))-    hostParser = OA.strOption (OA.long "host" <> OA.help "Host to bind on" <> OA.value (Config.previewHost conf))--    produceCommand (c,a,b) = OA.command c (OA.info (OA.helper <*> a) (b))--    commands =-        [ ( "build"-          , pure Build-          , OA.fullDesc <> OA.progDesc "Generate the site"-          )-        , ( "check"-          , pure Check <*> OA.switch (OA.long "internal-links" <> OA.help "Check internal links only")-          , OA.fullDesc <> OA.progDesc "Validate the site output"-          )-        , ( "clean"-          , pure Clean-          , OA.fullDesc <> OA.progDesc "Clean up and remove cache"-          )-        , ( "deploy"-          , pure Deploy-          , OA.fullDesc <> OA.progDesc "Upload/deploy your site"-           )-        , ( "preview"-          , pure Preview <*> portParser-          , OA.fullDesc <> OA.progDesc "[DEPRECATED] Please use the watch command"-          )-        , ( "rebuild"-          , pure Rebuild-          , OA.fullDesc <> OA.progDesc "Clean and build again"-          )-        , ( "server"-          , pure Server <*> hostParser <*> portParser-          , OA.fullDesc <> OA.progDesc "Start a preview server"-          )-        , ( "watch"-          , pure Watch <*> hostParser <*> portParser <*> OA.switch (OA.long "no-server" <> OA.help "Disable the built-in web server")-          , OA.fullDesc <> OA.progDesc "Autocompile on changes and start a preview server.  You can watch and recompile without running a server with --no-server."-          )-        ]-------------------------------------------------------------------------------------- | This is necessary because not everyone calls their program the same...-progName :: String-progName = unsafePerformIO getProgName-{-# NOINLINE progName #-}
− src/Hakyll/Preview/Poll.hs
@@ -1,119 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE CPP #-}-module Hakyll.Preview.Poll-    ( watchUpdates-    ) where------------------------------------------------------------------------------------import           Control.Concurrent             (forkIO)-import           Control.Concurrent.MVar        (newEmptyMVar, takeMVar,-                                                 tryPutMVar)-import           Control.Exception              (AsyncException, fromException,-                                                 handle, throw)-import           Control.Monad                  (forever, void, when)-import           System.Directory               (canonicalizePath)-import           System.FilePath                (pathSeparators)-import           System.FSNotify                (Event (..), startManager,-                                                 watchTree)--#ifdef mingw32_HOST_OS-import           Control.Concurrent             (threadDelay)-import           Control.Exception              (IOException, throw, try)-import           System.Directory               (doesFileExist)-import           System.Exit                    (exitFailure)-import           System.FilePath                ((</>))-import           System.IO                      (Handle, IOMode (ReadMode),-                                                 hClose, openFile)-import           System.IO.Error                (isPermissionError)-#endif------------------------------------------------------------------------------------import           Hakyll.Core.Configuration-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-------------------------------------------------------------------------------------- | A thread that watches for updates in a 'providerDirectory' and recompiles--- a site as soon as any changes occur-watchUpdates :: Configuration -> IO Pattern -> IO ()-watchUpdates conf update = do-    let providerDir = providerDirectory conf-    shouldBuild     <- newEmptyMVar-    pattern         <- update-    fullProviderDir <- canonicalizePath $ providerDirectory conf-    manager         <- startManager--    let allowed event = do-            -- Absolute path of the changed file. This must be inside provider-            -- dir, since that's the only dir we're watching.-            let path       = eventPath event-                relative   = dropWhile (`elem` pathSeparators) $-                    drop (length fullProviderDir) path-                identifier = fromFilePath relative--            shouldIgnore <- shouldIgnoreFile conf path-            return $ not shouldIgnore && matches pattern identifier--    -- This thread continually watches the `shouldBuild` MVar and builds-    -- whenever a value is present.-    _ <- forkIO $ forever $ do-        event <- takeMVar shouldBuild-        handle-            (\e -> case fromException e of-                Nothing    -> putStrLn (show e)-                Just async -> throw (async :: AsyncException))-            (update' event providerDir)--    -- Send an event whenever something occurs so that the thread described-    -- above will do a build.-    void $ watchTree manager providerDir (not . isRemove) $ \event -> do-        allowed' <- allowed event-        when allowed' $ void $ tryPutMVar shouldBuild event-  where-#ifndef mingw32_HOST_OS-    update' _     _        = void update-#else-    update' event provider = do-        let path = provider </> eventPath event-        -- on windows, a 'Modified' event is also sent on file deletion-        fileExists <- doesFileExist path--        when fileExists . void $ waitOpen path ReadMode (\_ -> update) 10--    -- continuously attempts to open the file in between sleep intervals-    -- handler is run only once it is able to open the file-    waitOpen :: FilePath -> IOMode -> (Handle -> IO r) -> Integer -> IO r-    waitOpen _    _    _       0 = do-        putStrLn "[ERROR] Failed to retrieve modified file for regeneration"-        exitFailure-    waitOpen path mode handler retries = do-        res <- try $ openFile path mode :: IO (Either IOException Handle)-        case res of-            Left ex -> if isPermissionError ex-                       then do-                           threadDelay 100000-                           waitOpen path mode handler (retries - 1)-                       else throw ex-            Right h -> do-                handled <- handler h-                hClose h-                return handled-#endif------------------------------------------------------------------------------------eventPath :: Event -> FilePath-eventPath evt = evtPath evt-  where-    evtPath (Added p _)    = p-    evtPath (Modified p _) = p-    evtPath (Removed p _)  = p------------------------------------------------------------------------------------isRemove :: Event -> Bool-isRemove (Removed _ _) = True-isRemove _             = False
− src/Hakyll/Preview/Server.hs
@@ -1,35 +0,0 @@------------------------------------------------------------------------------------ | Implements a basic static file server for previewing options-{-# LANGUAGE OverloadedStrings #-}-module Hakyll.Preview.Server-    ( staticServer-    ) where------------------------------------------------------------------------------------import           Data.String-import qualified Network.Wai.Handler.Warp       as Warp-import qualified Network.Wai.Application.Static as Static-import qualified Network.Wai                    as Wai-import           Network.HTTP.Types.Status      (Status)-----------------------------------------------------------------------------------import           Hakyll.Core.Logger    (Logger)-import qualified Hakyll.Core.Logger    as Logger--staticServer :: Logger               -- ^ Logger-             -> FilePath             -- ^ Directory to serve-             -> String               -- ^ Host to bind on-             -> Int                  -- ^ Port to listen on-             -> IO ()                -- ^ Blocks forever-staticServer logger directory host port = do-    Logger.header logger $ "Listening on http://" ++ host ++ ":" ++ show port-    Warp.runSettings warpSettings $-        Static.staticApp (Static.defaultFileServerSettings directory)-  where-    warpSettings = Warp.setLogger noLog-        $ Warp.setHost (fromString host)-        $ Warp.setPort port Warp.defaultSettings--noLog :: Wai.Request -> Status -> Maybe Integer -> IO ()-noLog _ _ _ = return ()
− src/Hakyll/Web/CompressCss.hs
@@ -1,86 +0,0 @@------------------------------------------------------------------------------------ | Module used for CSS compression. The compression is currently in a simple--- state, but would typically reduce the number of bytes by about 25%.-module Hakyll.Web.CompressCss-    ( compressCssCompiler-    , compressCss-    ) where------------------------------------------------------------------------------------import           Data.List               (isPrefixOf)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Item-------------------------------------------------------------------------------------- | Compiler form of 'compressCss'-compressCssCompiler :: Compiler (Item String)-compressCssCompiler = fmap compressCss <$> getResourceString-------------------------------------------------------------------------------------- | Compress CSS to speed up your site.-compressCss :: String -> String-compressCss = compressSeparators . stripComments . compressWhitespace-------------------------------------------------------------------------------------- | Compresses certain forms of separators.-compressSeparators :: String -> String-compressSeparators [] = []-compressSeparators str-    | isConstant  = head str : retainConstants compressSeparators (head str) (drop 1 str)-    | stripFirst  = compressSeparators (drop 1 str)-    | stripSecond = compressSeparators (head str : (drop 2 str))-    | otherwise   = head str : compressSeparators (drop 1 str)-  where-    isConstant  = or $ map (isOfPrefix str) ["\"", "'"]-    stripFirst  = or $ map (isOfPrefix str) $ [";;", ";}"] ++ (map (\c -> " " ++ c) separators)-    stripSecond = or $ map (isOfPrefix str) $ map (\c -> c ++ " ") separators-    separators  = [" ", "{", "}", ":", ";", ",", ">", "+", "!"]------------------------------------------------------------------------------------- | Compresses all whitespace.-compressWhitespace :: String -> String-compressWhitespace [] = []-compressWhitespace str-    | isConstant = head str : retainConstants compressWhitespace (head str) (drop 1 str)-    | replaceOne = compressWhitespace (' ' : (drop 1 str))-    | replaceTwo = compressWhitespace (' ' : (drop 2 str))-    | otherwise  = head str : compressWhitespace (drop 1 str)-  where-    isConstant = or $ map (isOfPrefix str) ["\"", "'"]-    replaceOne = or $ map (isOfPrefix str) ["\t", "\n", "\r"]-    replaceTwo = or $ map (isOfPrefix str) [" \t", " \n", " \r", "  "]------------------------------------------------------------------------------------- | Function that strips CSS comments away.-stripComments :: String -> String-stripComments [] = []-stripComments str-    | isConstant          = head str : retainConstants stripComments (head str) (drop 1 str)-    | isPrefixOf "/*" str = stripComments $ eatComments $ drop 2 str-    | otherwise           = head str : stripComments (drop 1 str)-  where-    isConstant  = or $ map (isOfPrefix str) ["\"", "'"]-    eatComments str'-        | null str' = []-        | isPrefixOf "*/" str' = drop 2 str'-        | otherwise = eatComments $ drop 1 str'------------------------------------------------------------------------------------- | Helper function to handle string constants correctly.-retainConstants :: (String -> String) -> Char -> String -> String-retainConstants f delim str-    | null str = []-    | isPrefixOf [delim] str = head str : f (drop 1 str)-    | otherwise = head str : retainConstants f delim (drop 1 str)------------------------------------------------------------------------------------- | Helper function to determine whether a string is a substring.-isOfPrefix :: String -> String -> Bool-isOfPrefix = flip isPrefixOf
− src/Hakyll/Web/Feed.hs
@@ -1,135 +0,0 @@------------------------------------------------------------------------------------ | A Module that allows easy rendering of RSS feeds.------ The main rendering functions (@renderRss@, @renderAtom@) all assume that--- you pass the list of items so that the most recent entry in the feed is the--- first item in the list.------ Also note that the context should have (at least) the following fields to--- produce a correct feed:------ - @$title$@: Title of the item------ - @$description$@: Description to appear in the feed------ - @$url$@: URL to the item - this is usually set automatically.------ In addition, the posts should be named according to the rules for--- 'Hakyll.Web.Template.Context.dateField'-module Hakyll.Web.Feed-    ( FeedConfiguration (..)-    , renderRss-    , renderAtom-    ) where------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Item-import           Hakyll.Core.Util.String (replaceAll)-import           Hakyll.Web.Template-import           Hakyll.Web.Template.Context-import           Hakyll.Web.Template.List------------------------------------------------------------------------------------import           Paths_hakyll-------------------------------------------------------------------------------------- | This is a data structure to keep the configuration of a feed.-data FeedConfiguration = FeedConfiguration-    { -- | Title of the feed.-      feedTitle       :: String-    , -- | Description of the feed.-      feedDescription :: String-    , -- | Name of the feed author.-      feedAuthorName  :: String-    , -- | Email of the feed author.-      feedAuthorEmail :: String-    , -- | Absolute root URL of the feed site (e.g. @http://jaspervdj.be@)-      feedRoot        :: String-    } deriving (Show, Eq)-------------------------------------------------------------------------------------- | Abstract function to render any feed.-renderFeed :: FilePath                -- ^ Feed template-           -> FilePath                -- ^ Item template-           -> FeedConfiguration       -- ^ Feed configuration-           -> Context String          -- ^ Context for the items-           -> [Item String]           -- ^ Input items-           -> Compiler (Item String)  -- ^ Resulting item-renderFeed feedPath itemPath config itemContext items = do-    feedTpl <- loadTemplate feedPath-    itemTpl <- loadTemplate itemPath--    protectedItems <- mapM (applyFilter protectCDATA) items-    body <- makeItem =<< applyTemplateList itemTpl itemContext' protectedItems-    applyTemplate feedTpl feedContext body-  where-    applyFilter :: (Monad m,Functor f) => (String -> String) -> f String -> m (f String)-    applyFilter tr str = return $ fmap tr str-    protectCDATA :: String -> String-    protectCDATA = replaceAll "]]>" (const "]]&gt;")-    -- Auxiliary: load a template from a datafile-    loadTemplate path = do-        file <- compilerUnsafeIO $ getDataFileName path-        unsafeReadTemplateFile file--    itemContext' = mconcat-        [ itemContext-        , constField "root" (feedRoot config)-        , constField "authorName"  (feedAuthorName config)-        , constField "authorEmail" (feedAuthorEmail config)-        ]--    feedContext = mconcat-         [ bodyField  "body"-         , constField "title"       (feedTitle config)-         , constField "description" (feedDescription config)-         , constField "authorName"  (feedAuthorName config)-         , constField "authorEmail" (feedAuthorEmail config)-         , constField "root"        (feedRoot config)-         , urlField   "url"-         , updatedField-         , missingField-         ]--    -- Take the first "updated" field from all items -- this should be the most-    -- recent.-    updatedField = field "updated" $ \_ -> case items of-        []      -> return "Unknown"-        (x : _) -> unContext itemContext' "updated" [] x >>= \cf -> case cf of-            ListField _ _ -> fail "Hakyll.Web.Feed.renderFeed: Internal error"-            StringField s -> return s-------------------------------------------------------------------------------------- | Render an RSS feed with a number of items.-renderRss :: FeedConfiguration       -- ^ Feed configuration-          -> Context String          -- ^ Item context-          -> [Item String]           -- ^ Feed items-          -> Compiler (Item String)  -- ^ Resulting feed-renderRss config context = renderFeed-    "templates/rss.xml" "templates/rss-item.xml" config-    (makeItemContext "%a, %d %b %Y %H:%M:%S UT" context)-------------------------------------------------------------------------------------- | Render an Atom feed with a number of items.-renderAtom :: FeedConfiguration       -- ^ Feed configuration-           -> Context String          -- ^ Item context-           -> [Item String]           -- ^ Feed items-           -> Compiler (Item String)  -- ^ Resulting feed-renderAtom config context = renderFeed-    "templates/atom.xml" "templates/atom-item.xml" config-    (makeItemContext "%Y-%m-%dT%H:%M:%SZ" context)-------------------------------------------------------------------------------------- | Copies @$updated$@ from @$published$@ if it is not already set.-makeItemContext :: String -> Context a -> Context a-makeItemContext fmt context = mconcat-    [dateField "published" fmt, context, dateField "updated" fmt]
− src/Hakyll/Web/Html.hs
@@ -1,184 +0,0 @@------------------------------------------------------------------------------------ | Provides utilities to manipulate HTML pages-module Hakyll.Web.Html-    ( -- * Generic-      withTags--      -- * Headers-    , demoteHeaders--      -- * Url manipulation-    , getUrls-    , withUrls-    , toUrl-    , toSiteRoot-    , isExternal--      -- * Stripping/escaping-    , stripTags-    , escapeHtml-    ) where------------------------------------------------------------------------------------import           Data.Char                       (digitToInt, intToDigit,-                                                  isDigit, toLower)-import           Data.List                       (isPrefixOf)-import qualified Data.Set                        as S-import           System.FilePath.Posix           (joinPath, splitPath,-                                                  takeDirectory)-import           Text.Blaze.Html                 (toHtml)-import           Text.Blaze.Html.Renderer.String (renderHtml)-import qualified Text.HTML.TagSoup               as TS-import           Network.URI                     (isUnreserved, escapeURIString)-------------------------------------------------------------------------------------- | Map over all tags in the document-withTags :: (TS.Tag String -> TS.Tag String) -> String -> String-withTags f = renderTags' . map f . parseTags'-------------------------------------------------------------------------------------- | Map every @h1@ to an @h2@, @h2@ to @h3@, etc.-demoteHeaders :: String -> String-demoteHeaders = withTags $ \tag -> case tag of-    TS.TagOpen t a -> TS.TagOpen (demote t) a-    TS.TagClose t  -> TS.TagClose (demote t)-    t              -> t-  where-    demote t@['h', n]-        | isDigit n = ['h', intToDigit (min 6 $ digitToInt n + 1)]-        | otherwise = t-    demote t        = t------------------------------------------------------------------------------------isUrlAttribute :: String -> Bool-isUrlAttribute = (`elem` ["src", "href", "data", "poster"])------------------------------------------------------------------------------------getUrls :: [TS.Tag String] -> [String]-getUrls tags = [v | TS.TagOpen _ as <- tags, (k, v) <- as, isUrlAttribute k]-------------------------------------------------------------------------------------- | Apply a function to each URL on a webpage-withUrls :: (String -> String) -> String -> String-withUrls f = withTags tag-  where-    tag (TS.TagOpen s a) = TS.TagOpen s $ map attr a-    tag x                = x-    attr (k, v)          = (k, if isUrlAttribute k then f v else v)-------------------------------------------------------------------------------------- | Customized TagSoup renderer. The default TagSoup renderer escape CSS--- within style tags, and doesn't properly minimize.-renderTags' :: [TS.Tag String] -> String-renderTags' = TS.renderTagsOptions TS.RenderOptions-    { TS.optRawTag   = (`elem` ["script", "style"]) . map toLower-    , TS.optMinimize = (`S.member` minimize) . map toLower-    , TS.optEscape   = id-    }-  where-    -- A list of elements which must be minimized-    minimize = S.fromList-        [ "area", "br", "col", "embed", "hr", "img", "input", "meta", "link"-        , "param"-        ]-------------------------------------------------------------------------------------- | Customized TagSoup parser: do not decode any entities.-parseTags' :: String -> [TS.Tag String]-parseTags' = TS.parseTagsOptions (TS.parseOptions :: TS.ParseOptions String)-    { TS.optEntityData   = \(str, b) -> [TS.TagText $ "&" ++ str ++ [';' | b]]-    , TS.optEntityAttrib = \(str, b) -> ("&" ++ str ++ [';' | b], [])-    }-------------------------------------------------------------------------------------- | Convert a filepath to an URL starting from the site root------ Example:------ > toUrl "foo/bar.html"------ Result:------ > "/foo/bar.html"------ This also sanitizes the URL, e.g. converting spaces into '%20'-toUrl :: FilePath -> String-toUrl url = case url of-    ('/' : xs) -> '/' : sanitize xs-    xs         -> '/' : sanitize xs-  where-    -- Everything but unreserved characters should be escaped as we are-    -- sanitising the path therefore reserved characters which have a-    -- meaning in URI does not appear. Special casing for `/`, because it has-    -- a special meaning in FilePath as well as in URI.-    sanitize = escapeURIString (\c -> c == '/' || isUnreserved c)-------------------------------------------------------------------------------------- | Get the relative url to the site root, for a given (absolute) url-toSiteRoot :: String -> String-toSiteRoot = emptyException . joinPath . map parent-           . filter relevant . splitPath . takeDirectory-  where-    parent            = const ".."-    emptyException [] = "."-    emptyException x  = x-    relevant "."      = False-    relevant "/"      = False-    relevant "./"     = False-    relevant _        = True-------------------------------------------------------------------------------------- | Check if an URL links to an external HTTP(S) source-isExternal :: String -> Bool-isExternal url = any (flip isPrefixOf url) ["http://", "https://", "//"]-------------------------------------------------------------------------------------- | Strip all HTML tags from a string------ Example:------ > stripTags "<p>foo</p>"------ Result:------ > "foo"------ This also works for incomplete tags------ Example:------ > stripTags "<p>foo</p"------ Result:------ > "foo"-stripTags :: String -> String-stripTags []         = []-stripTags ('<' : xs) = stripTags $ drop 1 $ dropWhile (/= '>') xs-stripTags (x : xs)   = x : stripTags xs-------------------------------------------------------------------------------------- | HTML-escape a string------ Example:------ > escapeHtml "Me & Dean"------ Result:------ > "Me &amp; Dean"-escapeHtml :: String -> String-escapeHtml = renderHtml . toHtml
− src/Hakyll/Web/Html/RelativizeUrls.hs
@@ -1,52 +0,0 @@------------------------------------------------------------------------------------ | This module exposes a function which can relativize URL's on a webpage.------ This means that one can deploy the resulting site on--- @http:\/\/example.com\/@, but also on @http:\/\/example.com\/some-folder\/@--- without having to change anything (simply copy over the files).------ To use it, you should use absolute URL's from the site root everywhere. For--- example, use------ > <img src="/images/lolcat.png" alt="Funny zomgroflcopter" />------ in a blogpost. When running this through the relativize URL's module, this--- will result in (suppose your blogpost is located at @\/posts\/foo.html@:------ > <img src="../images/lolcat.png" alt="Funny zomgroflcopter" />-module Hakyll.Web.Html.RelativizeUrls-    ( relativizeUrls-    , relativizeUrlsWith-    ) where------------------------------------------------------------------------------------import           Data.List            (isPrefixOf)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Item-import           Hakyll.Web.Html-------------------------------------------------------------------------------------- | Compiler form of 'relativizeUrls' which automatically picks the right root--- path-relativizeUrls :: Item String -> Compiler (Item String)-relativizeUrls item = do-    route <- getRoute $ itemIdentifier item-    return $ case route of-        Nothing -> item-        Just r  -> fmap (relativizeUrlsWith $ toSiteRoot r) item-------------------------------------------------------------------------------------- | Relativize URL's in HTML-relativizeUrlsWith :: String  -- ^ Path to the site root-                   -> String  -- ^ HTML to relativize-                   -> String  -- ^ Resulting HTML-relativizeUrlsWith root = withUrls rel-  where-    isRel x = "/" `isPrefixOf` x && not ("//" `isPrefixOf` x)-    rel x   = if isRel x then root ++ x else x
− src/Hakyll/Web/Paginate.hs
@@ -1,153 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE OverloadedStrings #-}-module Hakyll.Web.Paginate-    ( PageNumber-    , Paginate (..)-    , buildPaginateWith-    , paginateEvery-    , paginateRules-    , paginateContext-    ) where------------------------------------------------------------------------------------import           Control.Applicative            (empty)-import           Control.Monad                  (forM_, forM)-import qualified Data.Map                       as M-import qualified Data.Set                       as S------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Item-import           Hakyll.Core.Metadata-import           Hakyll.Core.Rules-import           Hakyll.Web.Html-import           Hakyll.Web.Template.Context------------------------------------------------------------------------------------type PageNumber = Int-------------------------------------------------------------------------------------- | Data about paginators-data Paginate = Paginate-    { paginateMap        :: M.Map PageNumber [Identifier]-    , paginateMakeId     :: PageNumber -> Identifier-    , paginateDependency :: Dependency-    }------------------------------------------------------------------------------------paginateNumPages :: Paginate -> Int-paginateNumPages = M.size . paginateMap------------------------------------------------------------------------------------paginateEvery :: Int -> [a] -> [[a]]-paginateEvery n = go-  where-    go [] = []-    go xs = let (y, ys) = splitAt n xs in y : go ys------------------------------------------------------------------------------------buildPaginateWith-    :: MonadMetadata m-    => ([Identifier] -> m [[Identifier]])  -- ^ Group items into pages-    -> Pattern                             -- ^ Select items to paginate-    -> (PageNumber -> Identifier)          -- ^ Identifiers for the pages-    -> m Paginate-buildPaginateWith grouper pattern makeId = do-    ids      <- getMatches pattern-    idGroups <- grouper ids-    let idsSet = S.fromList ids-    return Paginate-        { paginateMap        = M.fromList (zip [1 ..] idGroups)-        , paginateMakeId     = makeId-        , paginateDependency = PatternDependency pattern idsSet-        }------------------------------------------------------------------------------------paginateRules :: Paginate -> (PageNumber -> Pattern -> Rules ()) -> Rules ()-paginateRules paginator rules =-    forM_ (M.toList $ paginateMap paginator) $ \(idx, identifiers) ->-        rulesExtraDependencies [paginateDependency paginator] $-            create [paginateMakeId paginator idx] $-                rules idx $ fromList identifiers-------------------------------------------------------------------------------------- | Get the identifier for a certain page by passing in the page number.-paginatePage :: Paginate -> PageNumber -> Maybe Identifier-paginatePage pag pageNumber-    | pageNumber < 1                      = Nothing-    | pageNumber > (paginateNumPages pag) = Nothing-    | otherwise                           = Just $ paginateMakeId pag pageNumber-------------------------------------------------------------------------------------- | A default paginate context which provides the following keys:--------- * @firstPageNum@--- * @firstPageUrl@--- * @previousPageNum@--- * @previousPageUrl@--- * @nextPageNum@--- * @nextPageUrl@--- * @lastPageNum@--- * @lastPageUrl@--- * @currentPageNum@--- * @currentPageUrl@--- * @numPages@--- * @allPages@-paginateContext :: Paginate -> PageNumber -> Context a-paginateContext pag currentPage = mconcat-    [ field "firstPageNum"    $ \_ -> otherPage 1                 >>= num-    , field "firstPageUrl"    $ \_ -> otherPage 1                 >>= url-    , field "previousPageNum" $ \_ -> otherPage (currentPage - 1) >>= num-    , field "previousPageUrl" $ \_ -> otherPage (currentPage - 1) >>= url-    , field "nextPageNum"     $ \_ -> otherPage (currentPage + 1) >>= num-    , field "nextPageUrl"     $ \_ -> otherPage (currentPage + 1) >>= url-    , field "lastPageNum"     $ \_ -> otherPage lastPage          >>= num-    , field "lastPageUrl"     $ \_ -> otherPage lastPage          >>= url-    , field "currentPageNum"  $ \i -> thisPage i                  >>= num-    , field "currentPageUrl"  $ \i -> thisPage i                  >>= url-    , constField "numPages"   $ show $ paginateNumPages pag-    , Context $ \k _ i -> case k of-        "allPages" -> do-            let ctx =-                    field "isCurrent" (\n -> if fst (itemBody n) == currentPage then return "true" else empty) `mappend`-                    field "num" (num . itemBody) `mappend`-                    field "url" (url . itemBody)--            list <- forM [1 .. lastPage] $-                \n -> if n == currentPage then thisPage i else otherPage n-            items <- mapM makeItem list-            return $ ListField ctx items-        _          -> do-            empty--    ]-  where-    lastPage = paginateNumPages pag--    thisPage i = return (currentPage, itemIdentifier i)-    otherPage n-        | n == currentPage = fail $ "This is the current page: " ++ show n-        | otherwise        = case paginatePage pag n of-            Nothing -> fail $ "No such page: " ++ show n-            Just i  -> return (n, i)--    num :: (Int, Identifier) -> Compiler String-    num = return . show . fst--    url :: (Int, Identifier) -> Compiler String-    url (n, i) = getRoute i >>= \mbR -> case mbR of-        Just r  -> return $ toUrl r-        Nothing -> fail $ "No URL for page: " ++ show n
− src/Hakyll/Web/Pandoc.hs
@@ -1,164 +0,0 @@------------------------------------------------------------------------------------ | Module exporting convenient pandoc bindings-module Hakyll.Web.Pandoc-    ( -- * The basic building blocks-      readPandoc-    , readPandocWith-    , writePandoc-    , writePandocWith-    , renderPandoc-    , renderPandocWith--      -- * Derived compilers-    , pandocCompiler-    , pandocCompilerWith-    , pandocCompilerWithTransform-    , pandocCompilerWithTransformM--      -- * Default options-    , defaultHakyllReaderOptions-    , defaultHakyllWriterOptions-    ) where------------------------------------------------------------------------------------import qualified Data.Set                   as S-import           Text.Pandoc-import           Text.Pandoc.Error          (PandocError (..))------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Item-import           Hakyll.Web.Pandoc.FileType-------------------------------------------------------------------------------------- | Read a string using pandoc, with the default options-readPandoc-    :: Item String             -- ^ String to read-    -> Compiler (Item Pandoc)  -- ^ Resulting document-readPandoc = readPandocWith defaultHakyllReaderOptions-------------------------------------------------------------------------------------- | Read a string using pandoc, with the supplied options-readPandocWith-    :: ReaderOptions           -- ^ Parser options-    -> Item String             -- ^ String to read-    -> Compiler (Item Pandoc)  -- ^ Resulting document-readPandocWith ropt item =-    case traverse (reader ropt (itemFileType item)) item of-        Left (ParseFailure err)  -> fail $-            "Hakyll.Web.Pandoc.readPandocWith: parse failed: " ++ err-        Left (ParsecError _ err) -> fail $-            "Hakyll.Web.Pandoc.readPandocWith: parse failed: " ++ show err-        Right item'              -> return item'-  where-    reader ro t = case t of-        DocBook            -> readDocBook ro-        Html               -> readHtml ro-        LaTeX              -> readLaTeX ro-        LiterateHaskell t' -> reader (addExt ro Ext_literate_haskell) t'-        Markdown           -> readMarkdown ro-        MediaWiki          -> readMediaWiki ro-        OrgMode            -> readOrg ro-        Rst                -> readRST ro-        Textile            -> readTextile ro-        _                  -> error $-            "Hakyll.Web.readPandocWith: I don't know how to read a file of " ++-            "the type " ++ show t ++ " for: " ++ show (itemIdentifier item)--    addExt ro e = ro {readerExtensions = S.insert e $ readerExtensions ro}-------------------------------------------------------------------------------------- | Write a document (as HTML) using pandoc, with the default options-writePandoc :: Item Pandoc  -- ^ Document to write-            -> Item String  -- ^ Resulting HTML-writePandoc = writePandocWith defaultHakyllWriterOptions-------------------------------------------------------------------------------------- | Write a document (as HTML) using pandoc, with the supplied options-writePandocWith :: WriterOptions  -- ^ Writer options for pandoc-                -> Item Pandoc    -- ^ Document to write-                -> Item String    -- ^ Resulting HTML-writePandocWith wopt = fmap $ writeHtmlString wopt-------------------------------------------------------------------------------------- | Render the resource using pandoc-renderPandoc :: Item String -> Compiler (Item String)-renderPandoc =-    renderPandocWith defaultHakyllReaderOptions defaultHakyllWriterOptions-------------------------------------------------------------------------------------- | Render the resource using pandoc-renderPandocWith-    :: ReaderOptions -> WriterOptions -> Item String -> Compiler (Item String)-renderPandocWith ropt wopt item =-    writePandocWith wopt <$> readPandocWith ropt item-------------------------------------------------------------------------------------- | Read a page render using pandoc-pandocCompiler :: Compiler (Item String)-pandocCompiler =-    pandocCompilerWith defaultHakyllReaderOptions defaultHakyllWriterOptions-------------------------------------------------------------------------------------- | A version of 'pandocCompiler' which allows you to specify your own pandoc--- options-pandocCompilerWith :: ReaderOptions -> WriterOptions -> Compiler (Item String)-pandocCompilerWith ropt wopt =-    cached "Hakyll.Web.Pandoc.pandocCompilerWith" $-        pandocCompilerWithTransform ropt wopt id-------------------------------------------------------------------------------------- | An extension of 'pandocCompilerWith' which allows you to specify a custom--- pandoc transformation for the content-pandocCompilerWithTransform :: ReaderOptions -> WriterOptions-                            -> (Pandoc -> Pandoc)-                            -> Compiler (Item String)-pandocCompilerWithTransform ropt wopt f =-    pandocCompilerWithTransformM ropt wopt (return . f)-------------------------------------------------------------------------------------- | Similar to 'pandocCompilerWithTransform', but the transformation--- function is monadic. This is useful when you want the pandoc--- transformation to use the 'Compiler' information such as routes,--- metadata, etc-pandocCompilerWithTransformM :: ReaderOptions -> WriterOptions-                    -> (Pandoc -> Compiler Pandoc)-                    -> Compiler (Item String)-pandocCompilerWithTransformM ropt wopt f =-    writePandocWith wopt <$>-        (traverse f =<< readPandocWith ropt =<< getResourceBody)-------------------------------------------------------------------------------------- | The default reader options for pandoc parsing in hakyll-defaultHakyllReaderOptions :: ReaderOptions-defaultHakyllReaderOptions = def-    { -- The following option causes pandoc to read smart typography, a nice-      -- and free bonus.-      readerSmart = True-    }-------------------------------------------------------------------------------------- | The default writer options for pandoc rendering in hakyll-defaultHakyllWriterOptions :: WriterOptions-defaultHakyllWriterOptions = def-    { -- This option causes literate haskell to be written using '>' marks in-      -- html, which I think is a good default.-      writerExtensions = S.insert Ext_literate_haskell (writerExtensions def)-    , -- We want to have hightlighting by default, to be compatible with earlier-      -- Hakyll releases-      writerHighlight  = True-    }
− src/Hakyll/Web/Pandoc/Biblio.hs
@@ -1,115 +0,0 @@------------------------------------------------------------------------------------ | Wraps pandocs bibiliography handling------ In order to add a bibliography, you will need a bibliography file (e.g.--- @.bib@) and a CSL file (@.csl@). Both need to be compiled with their--- respective compilers ('biblioCompiler' and 'cslCompiler'). Then, you can--- refer to these files when you use 'readPandocBiblio'. This function also--- takes the reader options for completeness -- you can use--- 'defaultHakyllReaderOptions' if you're unsure.--- 'pandocBiblioCompiler' is a convenience wrapper which works like 'pandocCompiler',--- but also takes paths to compiled bibliography and csl files.-{-# LANGUAGE Arrows                     #-}-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-module Hakyll.Web.Pandoc.Biblio-    ( CSL-    , cslCompiler-    , Biblio (..)-    , biblioCompiler-    , readPandocBiblio-    , pandocBiblioCompiler-    ) where------------------------------------------------------------------------------------import           Control.Monad            (liftM, replicateM)-import           Data.Binary              (Binary (..))-import           Data.Default             (def)-import           Data.Typeable            (Typeable)-import           Hakyll.Core.Compiler-import           Hakyll.Core.Identifier-import           Hakyll.Core.Item-import           Hakyll.Core.Writable-import           Hakyll.Web.Pandoc-import           Hakyll.Web.Pandoc.Binary ()-import qualified Text.CSL                 as CSL-import           Text.CSL.Pandoc          (processCites)-import           Text.Pandoc              (Pandoc, ReaderOptions (..))------------------------------------------------------------------------------------data CSL = CSL-    deriving (Show, Typeable)------------------------------------------------------------------------------------instance Binary CSL where-    put CSL = return ()-    get     = return CSL------------------------------------------------------------------------------------instance Writable CSL where-    -- Shouldn't be written.-    write _ _ = return ()------------------------------------------------------------------------------------cslCompiler :: Compiler (Item CSL)-cslCompiler = makeItem CSL------------------------------------------------------------------------------------newtype Biblio = Biblio [CSL.Reference]-    deriving (Show, Typeable)------------------------------------------------------------------------------------instance Binary Biblio where-    -- Ugly.-    get             = do-        len <- get-        Biblio <$> replicateM len get-    put (Biblio rs) = put (length rs) >> mapM_ put rs------------------------------------------------------------------------------------instance Writable Biblio where-    -- Shouldn't be written.-    write _ _ = return ()------------------------------------------------------------------------------------biblioCompiler :: Compiler (Item Biblio)-biblioCompiler = do-    filePath <- toFilePath <$> getUnderlying-    makeItem =<< unsafeCompiler (Biblio <$> CSL.readBiblioFile filePath)------------------------------------------------------------------------------------readPandocBiblio :: ReaderOptions-                 -> Item CSL-                 -> Item Biblio-                 -> (Item String)-                 -> Compiler (Item Pandoc)-readPandocBiblio ropt csl biblio item = do-    -- Parse CSL file, if given-    style <- unsafeCompiler $ CSL.readCSLFile Nothing . toFilePath . itemIdentifier $ csl--    -- We need to know the citation keys, add then *before* actually parsing the-    -- actual page. If we don't do this, pandoc won't even consider them-    -- citations!-    let Biblio refs = itemBody biblio-    pandoc <- itemBody <$> readPandocWith ropt item-    let pandoc' = processCites style refs pandoc--    return $ fmap (const pandoc') item-----------------------------------------------------------------------------------pandocBiblioCompiler :: String -> String -> Compiler (Item String)-pandocBiblioCompiler cslFileName bibFileName = do-    csl <- load $ fromFilePath cslFileName-    bib <- load $ fromFilePath bibFileName-    liftM writePandoc-        (getResourceBody >>= readPandocBiblio def csl bib)
− src/Hakyll/Web/Pandoc/Binary.hs
@@ -1,31 +0,0 @@-{-# OPTIONS_GHC -fno-warn-orphans #-}-{-# LANGUAGE DeriveGeneric              #-}-module Hakyll.Web.Pandoc.Binary where--import           Data.Binary            (Binary (..))--import qualified Text.CSL               as CSL-import qualified Text.CSL.Reference     as REF-import qualified Text.CSL.Style         as STY-import           Text.Pandoc            ------------------------------------------------------------------------------------- orphans--instance Binary REF.CNum-instance Binary REF.Literal-instance Binary REF.RefDate-instance Binary REF.RefType-instance Binary STY.Agent-instance Binary STY.Formatted-instance Binary Inline-instance Binary Block-instance Binary Citation-instance Binary MathType-instance Binary Alignment-instance Binary CitationMode-instance Binary QuoteType-instance Binary Format-instance Binary ListNumberDelim-instance Binary ListNumberStyle-instance Binary CSL.Reference
− src/Hakyll/Web/Pandoc/FileType.hs
@@ -1,74 +0,0 @@------------------------------------------------------------------------------------ | A module dealing with pandoc file extensions and associated file types-module Hakyll.Web.Pandoc.FileType-    ( FileType (..)-    , fileType-    , itemFileType-    ) where------------------------------------------------------------------------------------import           System.FilePath        (splitExtension)------------------------------------------------------------------------------------import           Hakyll.Core.Identifier-import           Hakyll.Core.Item-------------------------------------------------------------------------------------- | Datatype to represent the different file types Hakyll can deal with by--- default-data FileType-    = Binary-    | Css-    | DocBook-    | Html-    | LaTeX-    | LiterateHaskell FileType-    | Markdown-    | MediaWiki-    | OrgMode-    | PlainText-    | Rst-    | Textile-    deriving (Eq, Ord, Show, Read)-------------------------------------------------------------------------------------- | Get the file type for a certain file. The type is determined by extension.-fileType :: FilePath -> FileType-fileType = uncurry fileType' . splitExtension-  where-    fileType' _ ".css"       = Css-    fileType' _ ".dbk"       = DocBook-    fileType' _ ".htm"       = Html-    fileType' _ ".html"      = Html-    fileType' f ".lhs"       = LiterateHaskell $ case fileType f of-        -- If no extension is given, default to Markdown + LiterateHaskell-        Binary -> Markdown-        -- Otherwise, LaTeX + LiterateHaskell or whatever the user specified-        x      -> x-    fileType' _ ".markdown"  = Markdown-    fileType' _ ".mediawiki" = MediaWiki-    fileType' _ ".md"        = Markdown-    fileType' _ ".mdn"       = Markdown-    fileType' _ ".mdown"     = Markdown-    fileType' _ ".mdwn"      = Markdown-    fileType' _ ".mkd"       = Markdown-    fileType' _ ".mkdwn"     = Markdown-    fileType' _ ".org"       = OrgMode-    fileType' _ ".page"      = Markdown-    fileType' _ ".rst"       = Rst-    fileType' _ ".tex"       = LaTeX-    fileType' _ ".text"      = PlainText-    fileType' _ ".textile"   = Textile-    fileType' _ ".txt"       = PlainText-    fileType' _ ".wiki"      = MediaWiki-    fileType' _ _            = Binary  -- Treat unknown files as binary-------------------------------------------------------------------------------------- | Get the file type for the current file-itemFileType :: Item a -> FileType-itemFileType = fileType . toFilePath . itemIdentifier
− src/Hakyll/Web/Redirect.hs
@@ -1,87 +0,0 @@--- | Module used for generating HTML redirect pages. This allows renaming pages--- to avoid breaking existing links without requiring server-side support for--- formal 301 Redirect error codes-module Hakyll.Web.Redirect-    ( Redirect (..)-    , createRedirects-    ) where--import           Control.Applicative    ((<$>))-import           Control.Monad          (forM_)-import           Data.Binary            (Binary (..))-import           Hakyll.Core.Compiler-import           Hakyll.Core.Identifier-import           Hakyll.Core.Routes-import           Hakyll.Core.Rules-import           Hakyll.Core.Writable   (Writable (..))---- | This function exposes a higher-level interface compared to using the--- 'Redirect' type manually.------ This creates, using a database mapping broken URLs to working ones, HTML--- files which will do HTML META tag redirect pages (since, as a static site, we--- can't use web-server-level 301 redirects, and using JS is gross).------ This is useful for sending people using old URLs to renamed versions, dealing--- with common typos etc, and will increase site traffic.  Such broken URLs can--- be found by looking at server logs or by using Google Webmaster Tools.--- Broken URLs must be valid Haskell strings, non-URL-escaped valid POSIX--- filenames, and relative links, since they will be defined in a @hakyll.hs@--- and during generation, written to disk with the filename corresponding to the--- broken URLs.  (Target URLs can be absolute or relative, but should be--- URL-escaped.) So broken incoming links like <http://www.gwern.net/foo/> which--- should be <http://www.gwern.net/foobar> cannot be fixed (since you cannot--- create a HTML file named @"foo/"@ on disk, as that would be a directory).------ An example of a valid association list would be:------ > brokenLinks =--- >     [ ("projects.html", "http://github.com/gwern")--- >     , ("/Black-market archive", "Black-market%20archives")--- >     ]------ In which case the functionality can then be used in `main` with a line like:------ > version "redirects" $ createRedirects brokenLinks------ The 'version' is recommended to separate these items from your other pages.------ The on-disk files can then be uploaded with HTML mimetypes--- (either explicitly by generating and uploading them separately, by--- auto-detection of the filetype, or an upload tool defaulting to HTML--- mimetype, such as calling @s3cmd@ with @--default-mime-type=text/html@) and--- will redirect browsers and search engines going to the old/broken URLs.------ See also <https://groups.google.com/d/msg/hakyll/sWc6zxfh-uM/fUpZPsFNDgAJ>.-createRedirects :: [(Identifier, String)] -> Rules ()-createRedirects redirects =-    forM_ redirects $ \(ident, to) ->-        create [ident] $ do-            route idRoute-            compile $ makeItem $! Redirect to---- | This datatype can be used directly if you want a lower-level interface to--- generate redirects.  For example, if you want to redirect @foo.html@ to--- @bar.jpg@, you can use:------ > create ["foo.html"] $ do--- >     route idRoute--- >     compile $ makeItem $ Redirect "bar.jpg"-data Redirect = Redirect-    { redirectTo :: String-    } deriving (Eq, Ord, Show)--instance Binary Redirect where-    put (Redirect to) = put to-    get = Redirect <$> get--instance Writable Redirect where-    write path = write path . fmap redirectToHtml--redirectToHtml :: Redirect -> String-redirectToHtml (Redirect working) =-    "<!DOCTYPE html><html><head><meta charset=\"utf-8\"/><meta name=\"generator\" content=\"hakyll\"/>" ++-    "<meta http-equiv=\"refresh\" content=\"0; url=" ++ working ++-    "\"><link rel=\"canonical\" href=\"" ++ working ++-    "\"><title>Permanent Redirect</title></head><body><p>The page has moved to: <a href=\"" ++ working ++-    "\">this page</a></p></body></html>"
− src/Hakyll/Web/Tags.hs
@@ -1,344 +0,0 @@------------------------------------------------------------------------------------ | This module containing some specialized functions to deal with tags. It--- assumes you follow some conventions.------ We support two types of tags: tags and categories.------ To use default tags, use 'buildTags'. Tags are placed in a comma-separated--- metadata field like this:------ > ------ > author: Philip K. Dick--- > title: Do androids dream of electric sheep?--- > tags: future, science fiction, humanoid--- > ------ > The novel is set in a post-apocalyptic near future, where the Earth and--- > its populations have been damaged greatly by Nuclear...------ To use categories, use the 'buildCategories' function. Categories are--- determined by the directory a page is in, for example, the post------ > posts/coding/2010-01-28-hakyll-categories.markdown------ will receive the @coding@ category.------ Advanced users may implement custom systems using 'buildTagsWith' if desired.------ In the above example, we would want to create a page which lists all pages in--- the @coding@ category, for example, with the 'Identifier':------ > tags/coding.html------ This is where the first parameter of 'buildTags' and 'buildCategories' comes--- in. In the above case, we used the function:------ > fromCapture "tags/*.html" :: String -> Identifier------ The 'tagsRules' function lets you generate such a page for each tag in the--- 'Rules' monad.-{-# LANGUAGE Arrows                     #-}-{-# LANGUAGE DeriveDataTypeable         #-}-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE OverloadedStrings          #-}-module Hakyll.Web.Tags-    ( Tags (..)-    , getTags-    , buildTagsWith-    , buildTags-    , buildCategories-    , tagsRules-    , renderTags-    , renderTagCloud-    , renderTagCloudWith-    , tagCloudField-    , tagCloudFieldWith-    , renderTagList-    , tagsField-    , tagsFieldWith-    , categoryField-    , sortTagsBy-    , caseInsensitiveTags-    ) where------------------------------------------------------------------------------------import           Control.Arrow                   ((&&&))-import           Control.Monad                   (foldM, forM, forM_, mplus)-import           Data.Char                       (toLower)-import           Data.List                       (intercalate, intersperse,-                                                  sortBy)-import qualified Data.Map                        as M-import           Data.Maybe                      (catMaybes, fromMaybe)-import           Data.Ord                        (comparing)-import qualified Data.Set                        as S-import           System.FilePath                 (takeBaseName, takeDirectory)-import           Text.Blaze.Html                 (toHtml, toValue, (!))-import           Text.Blaze.Html.Renderer.String (renderHtml)-import qualified Text.Blaze.Html5                as H-import qualified Text.Blaze.Html5.Attributes     as A------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Dependencies-import           Hakyll.Core.Identifier-import           Hakyll.Core.Identifier.Pattern-import           Hakyll.Core.Item-import           Hakyll.Core.Metadata-import           Hakyll.Core.Rules-import           Hakyll.Core.Util.String-import           Hakyll.Web.Html-import           Hakyll.Web.Template.Context-------------------------------------------------------------------------------------- | Data about tags-data Tags = Tags-    { tagsMap        :: [(String, [Identifier])]-    , tagsMakeId     :: String -> Identifier-    , tagsDependency :: Dependency-    }-------------------------------------------------------------------------------------- | Obtain tags from a page in the default way: parse them from the @tags@--- metadata field. This can either be a list or a comma-separated string.-getTags :: MonadMetadata m => Identifier -> m [String]-getTags identifier = do-    metadata <- getMetadata identifier-    return $ fromMaybe [] $-        (lookupStringList "tags" metadata) `mplus`-        (map trim . splitAll "," <$> lookupString "tags" metadata)-------------------------------------------------------------------------------------- | Obtain categories from a page.-getCategory :: MonadMetadata m => Identifier -> m [String]-getCategory = return . return . takeBaseName . takeDirectory . toFilePath-------------------------------------------------------------------------------------- | Higher-order function to read tags-buildTagsWith :: MonadMetadata m-              => (Identifier -> m [String])-              -> Pattern-              -> (String -> Identifier)-              -> m Tags-buildTagsWith f pattern makeId = do-    ids    <- getMatches pattern-    tagMap <- foldM addTags M.empty ids-    let set' = S.fromList ids-    return $ Tags (M.toList tagMap) makeId (PatternDependency pattern set')-  where-    -- Create a tag map for one page-    addTags tagMap id' = do-        tags <- f id'-        let tagMap' = M.fromList $ zip tags $ repeat [id']-        return $ M.unionWith (++) tagMap tagMap'------------------------------------------------------------------------------------buildTags :: MonadMetadata m => Pattern -> (String -> Identifier) -> m Tags-buildTags = buildTagsWith getTags------------------------------------------------------------------------------------buildCategories :: MonadMetadata m => Pattern -> (String -> Identifier)-                -> m Tags-buildCategories = buildTagsWith getCategory------------------------------------------------------------------------------------tagsRules :: Tags -> (String -> Pattern -> Rules ()) -> Rules ()-tagsRules tags rules =-    forM_ (tagsMap tags) $ \(tag, identifiers) ->-        rulesExtraDependencies [tagsDependency tags] $-            create [tagsMakeId tags tag] $-                rules tag $ fromList identifiers-------------------------------------------------------------------------------------- | Render tags in HTML (the flexible higher-order function)-renderTags :: (String -> String -> Int -> Int -> Int -> String)-           -- ^ Produce a tag item: tag, url, count, min count, max count-           -> ([String] -> String)-           -- ^ Join items-           -> Tags-           -- ^ Tag cloud renderer-           -> Compiler String-renderTags makeHtml concatHtml tags = do-    -- In tags' we create a list: [((tag, route), count)]-    tags' <- forM (tagsMap tags) $ \(tag, ids) -> do-        route' <- getRoute $ tagsMakeId tags tag-        return ((tag, route'), length ids)--    -- TODO: We actually need to tell a dependency here!--    let -- Absolute frequencies of the pages-        freqs = map snd tags'--        -- The minimum and maximum count found-        (min', max')-            | null freqs = (0, 1)-            | otherwise  = (minimum &&& maximum) freqs--        -- Create a link for one item-        makeHtml' ((tag, url), count) =-            makeHtml tag (toUrl $ fromMaybe "/" url) count min' max'--    -- Render and return the HTML-    return $ concatHtml $ map makeHtml' tags'-------------------------------------------------------------------------------------- | Render a tag cloud in HTML-renderTagCloud :: Double-               -- ^ Smallest font size, in percent-               -> Double-               -- ^ Biggest font size, in percent-               -> Tags-               -- ^ Input tags-               -> Compiler String-               -- ^ Rendered cloud-renderTagCloud = renderTagCloudWith makeLink (intercalate " ")-  where-    makeLink minSize maxSize tag url count min' max' =-        -- Show the relative size of one 'count' in percent-        let diff     = 1 + fromIntegral max' - fromIntegral min'-            relative = (fromIntegral count - fromIntegral min') / diff-            size     = floor $ minSize + relative * (maxSize - minSize) :: Int-        in renderHtml $-            H.a ! A.style (toValue $ "font-size: " ++ show size ++ "%")-                ! A.href (toValue url)-                $ toHtml tag-------------------------------------------------------------------------------------- | Render a tag cloud in HTML-renderTagCloudWith :: (Double -> Double ->-                       String -> String -> Int -> Int -> Int -> String)-                   -- ^ Render a single tag link-                   -> ([String] -> String)-                   -- ^ Concatenate links-                   -> Double-                   -- ^ Smallest font size, in percent-                   -> Double-                   -- ^ Biggest font size, in percent-                   -> Tags-                   -- ^ Input tags-                   -> Compiler String-                   -- ^ Rendered cloud-renderTagCloudWith makeLink cat minSize maxSize =-  renderTags (makeLink minSize maxSize) cat-------------------------------------------------------------------------------------- | Render a tag cloud in HTML as a context-tagCloudField :: String-               -- ^ Destination key-               -> Double-               -- ^ Smallest font size, in percent-               -> Double-               -- ^ Biggest font size, in percent-               -> Tags-               -- ^ Input tags-               -> Context a-               -- ^ Context-tagCloudField key minSize maxSize tags =-  field key $ \_ -> renderTagCloud minSize maxSize tags-------------------------------------------------------------------------------------- | Render a tag cloud in HTML as a context-tagCloudFieldWith :: String-                  -- ^ Destination key-                  -> (Double -> Double ->-                      String -> String -> Int -> Int -> Int -> String)-                  -- ^ Render a single tag link-                  -> ([String] -> String)-                  -- ^ Concatenate links-                  -> Double-                  -- ^ Smallest font size, in percent-                  -> Double-                  -- ^ Biggest font size, in percent-                  -> Tags-                  -- ^ Input tags-                  -> Context a-                  -- ^ Context-tagCloudFieldWith key makeLink cat minSize maxSize tags =-  field key $ \_ -> renderTagCloudWith makeLink cat minSize maxSize tags-------------------------------------------------------------------------------------- | Render a simple tag list in HTML, with the tag count next to the item--- TODO: Maybe produce a Context here-renderTagList :: Tags -> Compiler (String)-renderTagList = renderTags makeLink (intercalate ", ")-  where-    makeLink tag url count _ _ = renderHtml $-        H.a ! A.href (toValue url) $ toHtml (tag ++ " (" ++ show count ++ ")")-------------------------------------------------------------------------------------- | Render tags with links with custom functions to get tags and to--- render links-tagsFieldWith :: (Identifier -> Compiler [String])-              -- ^ Get the tags-              -> (String -> (Maybe FilePath) -> Maybe H.Html)-              -- ^ Render link for one tag-              -> ([H.Html] -> H.Html)-              -- ^ Concatenate tag links-              -> String-              -- ^ Destination field-              -> Tags-              -- ^ Tags structure-              -> Context a-              -- ^ Resulting context-tagsFieldWith getTags' renderLink cat key tags = field key $ \item -> do-    tags' <- getTags' $ itemIdentifier item-    links <- forM tags' $ \tag -> do-        route' <- getRoute $ tagsMakeId tags tag-        return $ renderLink tag route'--    return $ renderHtml $ cat $ catMaybes $ links-------------------------------------------------------------------------------------- | Render tags with links-tagsField :: String     -- ^ Destination key-          -> Tags       -- ^ Tags-          -> Context a  -- ^ Context-tagsField =-  tagsFieldWith getTags simpleRenderLink (mconcat . intersperse ", ")-------------------------------------------------------------------------------------- | Render the category in a link-categoryField :: String     -- ^ Destination key-              -> Tags       -- ^ Tags-              -> Context a  -- ^ Context-categoryField =-  tagsFieldWith getCategory simpleRenderLink (mconcat . intersperse ", ")-------------------------------------------------------------------------------------- | Render one tag link-simpleRenderLink :: String -> (Maybe FilePath) -> Maybe H.Html-simpleRenderLink _   Nothing         = Nothing-simpleRenderLink tag (Just filePath) =-  Just $ H.a ! A.href (toValue $ toUrl filePath) $ toHtml tag-------------------------------------------------------------------------------------- | Sort tags using supplied function. First element of the tuple passed to--- the comparing function is the actual tag name.-sortTagsBy :: ((String, [Identifier]) -> (String, [Identifier]) -> Ordering)-           -> Tags -> Tags-sortTagsBy f t = t {tagsMap = sortBy f (tagsMap t)}-------------------------------------------------------------------------------------- | Sample sorting function that compares tags case insensitively.-caseInsensitiveTags :: (String, [Identifier]) -> (String, [Identifier])-                    -> Ordering-caseInsensitiveTags = comparing $ map toLower . fst
− src/Hakyll/Web/Template.hs
@@ -1,154 +0,0 @@--- | This module provides means for reading and applying 'Template's.------ Templates are tools to convert items into a string. They are perfectly suited--- for laying out your site.------ Let's look at an example template:------ > <html>--- >     <head>--- >         <title>My crazy homepage - $title$</title>--- >     </head>--- >     <body>--- >         <div id="header">--- >             <h1>My crazy homepage - $title$</h1>--- >         </div>--- >         <div id="content">--- >             $body$--- >         </div>--- >         <div id="footer">--- >             By reading this you agree that I now own your soul--- >         </div>--- >     </body>--- > </html>------ As you can see, the format is very simple -- @$key$@ is used to render the--- @$key$@ field from the page, everything else is literally copied. If you want--- to literally insert @\"$key$\"@ into your page (for example, when you're--- writing a Hakyll tutorial) you can use------ > <p>--- >     A literal $$key$$.--- > </p>------ Because of it's simplicity, these templates can be used for more than HTML:--- you could make, for example, CSS or JS templates as well.------ Apart from interpolating @$key$@s from the 'Context' you can also--- use the following macros:------ * @$if(key)$@------ > $if(key)$--- >  <b> Defined </b>--- > $else$--- >  <b> Non-defined </b>--- > $endif$------ This example will print @Defined@ if @key@ is defined in the--- context and @Non-defined@ otherwise. The @$else$@ clause is--- optional.------ * @$for(key)$@------ The @for@ macro is used for enumerating 'Context' elements that are--- lists, i.e. constructed using the 'listField' function. Assume that--- in a context we have an element @listField \"key\" c itms@. Then--- the snippet------ > $for(key)$--- >   $x$--- > $sep$,--- > $endfor$------ would, for each item @i@ in 'itms', lookup @$x$@ in the context @c@--- with item @i@, interpolate it, and join the resulting list with--- @,@.------ Another concrete example one may consider is the following. Given the--- context------ > listField "things" (field "thing" (return . itemBody))--- >    (sequence [makeItem "fruits", makeItem "vegetables"])------ and a template------ >  I like--- >  $for(things)$--- >    fresh $thing$$sep$, and--- >  $endfor$------ the resulting page would look like------ > <p>--- >  I like--- >--- >   fresh fruits, and--- >--- >   fresh vegetables--- > </p>------ The @$sep$@ part can be omitted. Usually, you can get by using the--- 'applyListTemplate' and 'applyJoinListTemplate' functions.------ * @$partial(path)$@------ Loads a template located in a separate file and interpolates it--- under the current context.------ Assuming that the file @test.html@ contains------ > <b>$key$</b>------ The result of rendering------ > <p>--- >   $partial("test.html")$--- > </p>------ is the same as the result of rendering------ > <p>--- >   <b>$key$</b>--- > </p>------ That is, calling @$partial$@ is equivalent to just copying and pasting--- template code.------ In the examples above you can see that the outputs contain a lot of leftover--- whitespace that you may wish to remove. Using @'$-'@ or @'-$'@ instead of--- @'$'@ in a macro strips all whitespace to the left or right of that clause--- respectively. Given the context------ > listField "counts" (field "count" (return . itemBody))--- >    (sequence [makeItem "3", makeItem "2", makeItem "1"])------ and a template------ > <p>--- >     $for(counts)-$--- >       $count$--- >       $-sep$...--- >     $-endfor$--- > </p>------ the resulting page would look like------ > <p>--- >     3...2...1--- > </p>----module Hakyll.Web.Template-    ( Template-    , templateBodyCompiler-    , templateCompiler-    , applyTemplate-    , loadAndApplyTemplate-    , applyAsTemplate-    , readTemplate-    , unsafeReadTemplateFile-    ) where------------------------------------------------------------------------------------import           Hakyll.Web.Template.Internal
− src/Hakyll/Web/Template/Context.hs
@@ -1,379 +0,0 @@----------------------------------------------------------------------------------{-# LANGUAGE CPP                       #-}-{-# LANGUAGE ExistentialQuantification #-}-module Hakyll.Web.Template.Context-    ( ContextField (..)-    , Context (..)-    , field-    , boolField-    , constField-    , listField-    , listFieldWith-    , functionField-    , mapContext--    , defaultContext-    , bodyField-    , metadataField-    , urlField-    , pathField-    , titleField-    , snippetField-    , dateField-    , dateFieldWith-    , getItemUTC-    , getItemModificationTime-    , modificationTimeField-    , modificationTimeFieldWith-    , teaserField-    , teaserFieldWithSeparator-    , missingField-    ) where------------------------------------------------------------------------------------import           Control.Applicative           (Alternative (..))-import           Control.Monad                 (msum)-import           Data.List                     (intercalate)-import           Data.Time.Clock               (UTCTime (..))-import           Data.Time.Format              (formatTime)-import qualified Data.Time.Format              as TF-import           Data.Time.Locale.Compat       (TimeLocale, defaultTimeLocale)-import           Hakyll.Core.Compiler-import           Hakyll.Core.Compiler.Internal-import           Hakyll.Core.Identifier-import           Hakyll.Core.Item-import           Hakyll.Core.Metadata-import           Hakyll.Core.Provider-import           Hakyll.Core.Util.String       (needlePrefix, splitAll)-import           Hakyll.Web.Html-import           System.FilePath               (splitDirectories, takeBaseName)-------------------------------------------------------------------------------------- | Mostly for internal usage-data ContextField-    = StringField String-    | forall a. ListField (Context a) [Item a]-------------------------------------------------------------------------------------- | The 'Context' monoid. Please note that the order in which you--- compose the items is important. For example in------ > field "A" f1 <> field "A" f2------ the first context will overwrite the second. This is especially--- important when something is being composed with--- 'metadataField' (or 'defaultContext'). If you want your context to be--- overwritten by the metadata fields, compose it from the right:------ @--- 'metadataField' \<\> field \"date\" fDate--- @----newtype Context a = Context-    { unContext :: String -> [String] -> Item a -> Compiler ContextField-    }------------------------------------------------------------------------------------instance Monoid (Context a) where-    mempty                          = missingField-    mappend (Context f) (Context g) = Context $ \k a i -> f k a i <|> g k a i------------------------------------------------------------------------------------field' :: String -> (Item a -> Compiler ContextField) -> Context a-field' key value = Context $ \k _ i -> if k == key then value i else empty-------------------------------------------------------------------------------------- | Constructs a new field in the 'Context.'-field-    :: String                      -- ^ Key-    -> (Item a -> Compiler String) -- ^ Function that constructs a value based-                                   -- on the item-    -> Context a-field key value = field' key (fmap StringField . value)-------------------------------------------------------------------------------------- | Creates a 'field' to use with the @$if()$@ template macro.-boolField-    :: String-    -> (Item a -> Bool)-    -> Context a-boolField name f = field name (\i -> if f i-    then pure (error $ unwords ["no string value for bool field:",name])-    else empty)-------------------------------------------------------------------------------------- | Creates a 'field' that does not depend on the 'Item'-constField :: String -> String -> Context a-constField key = field key . const . return------------------------------------------------------------------------------------listField :: String -> Context a -> Compiler [Item a] -> Context b-listField key c xs = listFieldWith key c (const xs)------------------------------------------------------------------------------------listFieldWith-    :: String -> Context a -> (Item b -> Compiler [Item a]) -> Context b-listFieldWith key c f = field' key $ fmap (ListField c) . f------------------------------------------------------------------------------------functionField :: String -> ([String] -> Item a -> Compiler String) -> Context a-functionField name value = Context $ \k args i ->-    if k == name-        then StringField <$> value args i-        else empty------------------------------------------------------------------------------------mapContext :: (String -> String) -> Context a -> Context a-mapContext f (Context c) = Context $ \k a i -> do-    fld <- c k a i-    case fld of-        StringField str -> return $ StringField (f str)-        ListField _ _   -> fail $-            "Hakyll.Web.Template.Context.mapContext: " ++-            "can't map over a ListField!"------------------------------------------------------------------------------------- | A context that allows snippet inclusion. In processed file, use as:------ > ...--- > $snippet("path/to/snippet/")$--- > ...------ The contents of the included file will not be interpolated.----snippetField :: Context String-snippetField = functionField "snippet" f-  where-    f [contentsPath] _ = loadBody (fromFilePath contentsPath)-    f _              i = error $-        "Too many arguments to function 'snippet()' in item " ++-            show (itemIdentifier i)------------------------------------------------------------------------------------- | A context that contains (in that order)------     1. A @$body$@ field------     2. Metadata fields------     3. A @$url$@ 'urlField'------     4. A @$path$@ 'pathField'------     5. A @$title$@ 'titleField'-defaultContext :: Context String-defaultContext =-    bodyField     "body"     `mappend`-    metadataField            `mappend`-    urlField      "url"      `mappend`-    pathField     "path"     `mappend`-    titleField    "title"    `mappend`-    missingField------------------------------------------------------------------------------------teaserSeparator :: String-teaserSeparator = "<!--more-->"-------------------------------------------------------------------------------------- | Constructs a 'field' that contains the body of the item.-bodyField :: String -> Context String-bodyField key = field key $ return . itemBody-------------------------------------------------------------------------------------- | Map any field to its metadata value, if present-metadataField :: Context a-metadataField = Context $ \k _ i -> do-    value <- getMetadataField (itemIdentifier i) k-    maybe empty (return . StringField) value-------------------------------------------------------------------------------------- | Absolute url to the resulting item-urlField :: String -> Context a-urlField key = field key $-    fmap (maybe empty toUrl) . getRoute . itemIdentifier-------------------------------------------------------------------------------------- | Filepath of the underlying file of the item-pathField :: String -> Context a-pathField key = field key $ return . toFilePath . itemIdentifier-------------------------------------------------------------------------------------- | This title 'field' takes the basename of the underlying file by default-titleField :: String -> Context a-titleField = mapContext takeBaseName . pathField-------------------------------------------------------------------------------------- | When the metadata has a field called @published@ in one of the--- following formats then this function can render the date.------   * @Mon, 06 Sep 2010 00:01:00 +0000@------   * @Mon, 06 Sep 2010 00:01:00 UTC@------   * @Mon, 06 Sep 2010 00:01:00@------   * @2010-09-06T00:01:00+0000@------   * @2010-09-06T00:01:00Z@------   * @2010-09-06T00:01:00@------   * @2010-09-06 00:01:00+0000@------   * @2010-09-06 00:01:00@------   * @September 06, 2010 00:01 AM@------ Following date-only formats are supported too (@00:00:00@ for time is--- assumed)------   * @2010-09-06@------   * @September 06, 2010@------ Alternatively, when the metadata has a field called @path@ in a--- @folder/yyyy-mm-dd-title.extension@ format (the convention for pages)--- and no @published@ metadata field set, this function can render--- the date. This pattern matches the file name or directory names--- that begins with @yyyy-mm-dd@ . For example:--- @folder//yyyy-mm-dd-title//dist//main.extension@ .--- In case of multiple matches, the rightmost one is used.--dateField :: String     -- ^ Key in which the rendered date should be placed-          -> String     -- ^ Format to use on the date-          -> Context a  -- ^ Resulting context-dateField = dateFieldWith defaultTimeLocale-------------------------------------------------------------------------------------- | This is an extended version of 'dateField' that allows you to--- specify a time locale that is used for outputting the date. For more--- details, see 'dateField'.-dateFieldWith :: TimeLocale  -- ^ Output time locale-              -> String      -- ^ Destination key-              -> String      -- ^ Format to use on the date-              -> Context a   -- ^ Resulting context-dateFieldWith locale key format = field key $ \i -> do-    time <- getItemUTC locale $ itemIdentifier i-    return $ formatTime locale format time-------------------------------------------------------------------------------------- | Parser to try to extract and parse the time from the @published@--- field or from the filename. See 'dateField' for more information.--- Exported for user convenience.-getItemUTC :: MonadMetadata m-           => TimeLocale        -- ^ Output time locale-           -> Identifier        -- ^ Input page-           -> m UTCTime         -- ^ Parsed UTCTime-getItemUTC locale id' = do-    metadata <- getMetadata id'-    let tryField k fmt = lookupString k metadata >>= parseTime' fmt-        paths          = splitDirectories $ toFilePath id'--    maybe empty' return $ msum $-        [tryField "published" fmt | fmt <- formats] ++-        [tryField "date"      fmt | fmt <- formats] ++-        [parseTime' "%Y-%m-%d" $ intercalate "-" $ take 3 $ splitAll "-" fnCand | fnCand <- reverse paths]-  where-    empty'     = fail $ "Hakyll.Web.Template.Context.getItemUTC: " ++-        "could not parse time for " ++ show id'-    parseTime' = parseTimeM True locale-    formats    =-        [ "%a, %d %b %Y %H:%M:%S %Z"-        , "%Y-%m-%dT%H:%M:%S%Z"-        , "%Y-%m-%d %H:%M:%S%Z"-        , "%Y-%m-%d"-        , "%B %e, %Y %l:%M %p"-        , "%B %e, %Y"-        , "%b %d, %Y"-        ]-------------------------------------------------------------------------------------- | Get the time on which the actual file was last modified. This only works if--- there actually is an underlying file, of couse.-getItemModificationTime-    :: Identifier-    -> Compiler UTCTime-getItemModificationTime identifier = do-    provider <- compilerProvider <$> compilerAsk-    return $ resourceModificationTime provider identifier------------------------------------------------------------------------------------modificationTimeField :: String     -- ^ Key-                      -> String     -- ^ Format-                      -> Context  a -- ^ Resuting context-modificationTimeField = modificationTimeFieldWith defaultTimeLocale------------------------------------------------------------------------------------modificationTimeFieldWith :: TimeLocale  -- ^ Time output locale-                          -> String      -- ^ Key-                          -> String      -- ^ Format-                          -> Context a   -- ^ Resulting context-modificationTimeFieldWith locale key fmt = field key $ \i -> do-    mtime <- getItemModificationTime $ itemIdentifier i-    return $ formatTime locale fmt mtime-------------------------------------------------------------------------------------- | A context with "teaser" key which contain a teaser of the item.--- The item is loaded from the given snapshot (which should be saved--- in the user code before any templates are applied).-teaserField :: String           -- ^ Key to use-            -> Snapshot         -- ^ Snapshot to load-            -> Context String   -- ^ Resulting context-teaserField = teaserFieldWithSeparator teaserSeparator-------------------------------------------------------------------------------------- | A context with "teaser" key which contain a teaser of the item, defined as--- the snapshot content before the teaser separator. The item is loaded from the--- given snapshot (which should be saved in the user code before any templates--- are applied).-teaserFieldWithSeparator :: String           -- ^ Separator to use-                         -> String           -- ^ Key to use-                         -> Snapshot         -- ^ Snapshot to load-                         -> Context String   -- ^ Resulting context-teaserFieldWithSeparator separator key snapshot = field key $ \item -> do-    body <- itemBody <$> loadSnapshot (itemIdentifier item) snapshot-    case needlePrefix separator body of-        Nothing -> fail $-            "Hakyll.Web.Template.Context: no teaser defined for " ++-            show (itemIdentifier item)-        Just t -> return t------------------------------------------------------------------------------------missingField :: Context a-missingField = Context $ \k _ i -> fail $-    "Missing field $" ++ k ++ "$ in context for item " ++-    show (itemIdentifier i)--parseTimeM :: Bool -> TimeLocale -> String -> String -> Maybe UTCTime-#if MIN_VERSION_time(1,5,0)-parseTimeM = TF.parseTimeM-#else-parseTimeM _ = TF.parseTime-#endif
− src/Hakyll/Web/Template/Internal.hs
@@ -1,203 +0,0 @@-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-{-# LANGUAGE ScopedTypeVariables        #-}-module Hakyll.Web.Template.Internal-    ( Template (..)-    , template-    , templateBodyCompiler-    , templateCompiler-    , applyTemplate-    , applyTemplate'-    , loadAndApplyTemplate-    , applyAsTemplate-    , readTemplate-    , unsafeReadTemplateFile--    , module Hakyll.Web.Template.Internal.Element-    , module Hakyll.Web.Template.Internal.Trim-    ) where------------------------------------------------------------------------------------import           Control.Monad.Except                 (MonadError (..))-import           Data.Binary                          (Binary)-import           Data.List                            (intercalate)-import           Data.Typeable                        (Typeable)-import           GHC.Exts                             (IsString (..))-import           Prelude                              hiding (id)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Identifier-import           Hakyll.Core.Item-import           Hakyll.Core.Writable-import           Hakyll.Web.Template.Context-import           Hakyll.Web.Template.Internal.Element-import           Hakyll.Web.Template.Internal.Trim-------------------------------------------------------------------------------------- | Datatype used for template substitutions.-newtype Template = Template-    { unTemplate :: [TemplateElement]-    } deriving (Show, Eq, Binary, Typeable)------------------------------------------------------------------------------------instance Writable Template where-    -- Writing a template is impossible-    write _ _ = return ()------------------------------------------------------------------------------------instance IsString Template where-    fromString = readTemplate-------------------------------------------------------------------------------------- | Wrap the constructor to ensure trim is called.-template :: [TemplateElement] -> Template-template = Template . trim------------------------------------------------------------------------------------readTemplate :: String -> Template-readTemplate = Template . trim . readTemplateElems------------------------------------------------------------------------------------- | Read a template, without metadata header-templateBodyCompiler :: Compiler (Item Template)-templateBodyCompiler = cached "Hakyll.Web.Template.templateBodyCompiler" $ do-    item <- getResourceBody-    file <- getResourceFilePath-    return $ fmap (template . readTemplateElemsFile file) item------------------------------------------------------------------------------------- | Read complete file contents as a template-templateCompiler :: Compiler (Item Template)-templateCompiler = cached "Hakyll.Web.Template.templateCompiler" $ do-    item <- getResourceString-    file <- getResourceFilePath-    return $ fmap (template . readTemplateElemsFile file) item------------------------------------------------------------------------------------applyTemplate :: Template                -- ^ Template-              -> Context a               -- ^ Context-              -> Item a                  -- ^ Page-              -> Compiler (Item String)  -- ^ Resulting item-applyTemplate tpl context item = do-    body <- applyTemplate' (unTemplate tpl) context item-    return $ itemSetBody body item------------------------------------------------------------------------------------applyTemplate'-    :: forall a.-       [TemplateElement] -- ^ Unwrapped Template-    -> Context a         -- ^ Context-    -> Item a            -- ^ Page-    -> Compiler String   -- ^ Resulting item-applyTemplate' tes context x = go tes-  where-    context' :: String -> [String] -> Item a -> Compiler ContextField-    context' = unContext (context `mappend` missingField)--    go = fmap concat . mapM applyElem--    trimError = error $ "Hakyll.Web.Template.applyTemplate: template not " ++-        "fully trimmed."--    -----------------------------------------------------------------------------    applyElem :: TemplateElement -> Compiler String--    applyElem TrimL = trimError--    applyElem TrimR = trimError--    applyElem (Chunk c) = return c--    applyElem (Expr e) = applyExpr e >>= getString e--    applyElem Escaped = return "$"--    applyElem (If e t mf) = (applyExpr e >> go t) `catchError` handler-      where-        handler _ = case mf of-            Nothing -> return ""-            Just f  -> go f--    applyElem (For e b s) = applyExpr e >>= \cf -> case cf of-        StringField _  -> fail $-            "Hakyll.Web.Template.applyTemplateWith: expected ListField but " ++-            "got StringField for expr " ++ show e-        ListField c xs -> do-            sep <- maybe (return "") go s-            bs  <- mapM (applyTemplate' b c) xs-            return $ intercalate sep bs--    applyElem (Partial e) = do-        p             <- applyExpr e >>= getString e-        Template tpl' <- loadBody (fromFilePath p)-        applyTemplate' tpl' context x--    -----------------------------------------------------------------------------    applyExpr :: TemplateExpr -> Compiler ContextField--    applyExpr (Ident (TemplateKey k)) = context' k [] x--    applyExpr (Call (TemplateKey k) args) = do-        args' <- mapM (\e -> applyExpr e >>= getString e) args-        context' k args' x--    applyExpr (StringLiteral s) = return (StringField s)--    ------------------------------------------------------------------------------    getString _ (StringField s) = return s-    getString e (ListField _ _) = fail $-        "Hakyll.Web.Template.applyTemplateWith: expected StringField but " ++-        "got ListField for expr " ++ show e-------------------------------------------------------------------------------------- | The following pattern is so common:------ > tpl <- loadBody "templates/foo.html"--- > someCompiler--- >     >>= applyTemplate tpl context------ That we have a single function which does this:------ > someCompiler--- >     >>= loadAndApplyTemplate "templates/foo.html" context-loadAndApplyTemplate :: Identifier              -- ^ Template identifier-                     -> Context a               -- ^ Context-                     -> Item a                  -- ^ Page-                     -> Compiler (Item String)  -- ^ Resulting item-loadAndApplyTemplate identifier context item = do-    tpl <- loadBody identifier-    applyTemplate tpl context item-------------------------------------------------------------------------------------- | It is also possible that you want to substitute @$key$@s within the body of--- an item. This function does that by interpreting the item body as a template,--- and then applying it to itself.-applyAsTemplate :: Context String          -- ^ Context-                -> Item String             -- ^ Item and template-                -> Compiler (Item String)  -- ^ Resulting item-applyAsTemplate context item =-    let tpl = template $ readTemplateElemsFile file (itemBody item)-        file = toFilePath $ itemIdentifier item-    in applyTemplate tpl context item------------------------------------------------------------------------------------unsafeReadTemplateFile :: FilePath -> Compiler Template-unsafeReadTemplateFile file = do-    tpl <- unsafeCompiler $ readFile file-    pure $ template $ readTemplateElemsFile file tpl-
− src/Hakyll/Web/Template/Internal/Element.hs
@@ -1,298 +0,0 @@------------------------------------------------------------------------------------ | Module containing the elements used in a template.  A template is generally--- just a list of these elements.-{-# LANGUAGE GeneralizedNewtypeDeriving #-}-module Hakyll.Web.Template.Internal.Element-    ( TemplateKey (..)-    , TemplateExpr (..)-    , TemplateElement (..)-    , templateElems-    , readTemplateElems-    , readTemplateElemsFile-    ) where------------------------------------------------------------------------------------import           Control.Applicative     ((<|>))-import           Control.Monad           (void)-import           Data.Binary             (Binary, get, getWord8, put, putWord8)-import           Data.List               (intercalate)-import           Data.Maybe              (isJust)-import           Data.Typeable           (Typeable)-import           GHC.Exts                (IsString (..))-import qualified Text.Parsec             as P-import qualified Text.Parsec.String      as P------------------------------------------------------------------------------------import           Hakyll.Core.Util.Parser------------------------------------------------------------------------------------newtype TemplateKey = TemplateKey String-    deriving (Binary, Show, Eq, Typeable)------------------------------------------------------------------------------------instance IsString TemplateKey where-    fromString = TemplateKey-------------------------------------------------------------------------------------- | Elements of a template.-data TemplateElement-    = Chunk String-    | Expr TemplateExpr-    | Escaped-      -- expr, then, else-    | If TemplateExpr [TemplateElement] (Maybe [TemplateElement])-      -- expr, body, separator-    | For TemplateExpr [TemplateElement] (Maybe [TemplateElement])-      -- filename-    | Partial TemplateExpr-    | TrimL-    | TrimR-    deriving (Show, Eq, Typeable)------------------------------------------------------------------------------------instance Binary TemplateElement where-    put (Chunk string) = putWord8 0 >> put string-    put (Expr e)       = putWord8 1 >> put e-    put  Escaped       = putWord8 2-    put (If e t f)     = putWord8 3 >> put e >> put t >> put f-    put (For e b s)    = putWord8 4 >> put e >> put b >> put s-    put (Partial e)    = putWord8 5 >> put e-    put  TrimL         = putWord8 6-    put  TrimR         = putWord8 7--    get = getWord8 >>= \tag -> case tag of-        0 -> Chunk <$> get-        1 -> Expr <$> get-        2 -> pure Escaped-        3 -> If <$> get <*> get <*> get-        4 -> For <$> get <*> get <*> get-        5 -> Partial <$> get-        6 -> pure TrimL-        7 -> pure TrimR-        _ -> error "Hakyll.Web.Template.Internal: Error reading cached template"-------------------------------------------------------------------------------------- | Expression in a template-data TemplateExpr-    = Ident TemplateKey-    | Call TemplateKey [TemplateExpr]-    | StringLiteral String-    deriving (Eq, Typeable)------------------------------------------------------------------------------------instance Show TemplateExpr where-    show (Ident (TemplateKey k))   = k-    show (Call (TemplateKey k) as) =-        k ++ "(" ++ intercalate ", " (map show as) ++ ")"-    show (StringLiteral s)         = show s------------------------------------------------------------------------------------instance Binary TemplateExpr where-    put (Ident k)         = putWord8 0 >> put k-    put (Call k as)       = putWord8 1 >> put k >> put as-    put (StringLiteral s) = putWord8 2 >> put s--    get = getWord8 >>= \tag -> case tag of-        0 -> Ident         <$> get-        1 -> Call          <$> get <*> get-        2 -> StringLiteral <$> get-        _ -> error "Hakyll.Web.Template.Internal: Error reading cached template"------------------------------------------------------------------------------------readTemplateElems :: String -> [TemplateElement]-readTemplateElems = readTemplateElemsFile "{literal}"------------------------------------------------------------------------------------readTemplateElemsFile :: FilePath -> String -> [TemplateElement]-readTemplateElemsFile file input = case P.parse templateElems file input of-    Left err -> error $ "Cannot parse template: " ++ show err-    Right t  -> t------------------------------------------------------------------------------------templateElems :: P.Parser [TemplateElement]-templateElems = mconcat <$> P.many (P.choice [ lift chunk-                                             , lift escaped-                                             , conditional-                                             , for-                                             , partial-                                             , expr-                                             ])-    where lift = fmap (:[])------------------------------------------------------------------------------------chunk :: P.Parser TemplateElement-chunk = Chunk <$> P.many1 (P.noneOf "$")------------------------------------------------------------------------------------expr :: P.Parser [TemplateElement]-expr = P.try $ do-    trimLExpr <- trimOpen-    e <- expr'-    trimRExpr <- trimClose-    return $ [TrimL | trimLExpr] ++ [Expr e] ++ [TrimR | trimRExpr]------------------------------------------------------------------------------------expr' :: P.Parser TemplateExpr-expr' = stringLiteral <|> call <|> ident------------------------------------------------------------------------------------escaped :: P.Parser TemplateElement-escaped = Escaped <$ P.try (P.string "$$")------------------------------------------------------------------------------------trimOpen :: P.Parser Bool-trimOpen = do-    void $ P.char '$'-    trimLIf <- P.optionMaybe $ P.try (P.char '-')-    pure $ isJust trimLIf------------------------------------------------------------------------------------trimClose :: P.Parser Bool-trimClose = do-    trimIfR <- P.optionMaybe $ P.try (P.char '-')-    void $ P.char '$'-    pure $ isJust trimIfR------------------------------------------------------------------------------------conditional :: P.Parser [TemplateElement]-conditional = P.try $ do-    -- if-    trimLIf <- trimOpen-    void $ P.string "if("-    e <- expr'-    void $ P.char ')'-    trimRIf <- trimClose-    -- then-    thenBranch <- templateElems-    -- else-    elseParse <- opt "else"-    -- endif-    trimLEnd <- trimOpen-    void $ P.string "endif"-    trimREnd <- trimClose--    -- As else is optional we need to sort out where any Trim_s need to go.-    let (thenBody, elseBody) = maybe (thenNoElse, Nothing) thenElse elseParse-            where thenNoElse =-                      [TrimR | trimRIf] ++ thenBranch ++ [TrimL | trimLEnd]--                  thenElse (trimLElse, elseBranch, trimRElse) = (thenB, elseB)-                      where thenB = [TrimR | trimRIf]-                                 ++ thenBranch-                                 ++ [TrimL | trimLElse]--                            elseB = Just $ [TrimR | trimRElse]-                                        ++ elseBranch-                                        ++ [TrimL | trimLEnd]--    pure $ [TrimL | trimLIf] ++ [If e thenBody elseBody] ++ [TrimR | trimREnd]------------------------------------------------------------------------------------for :: P.Parser [TemplateElement]-for = P.try $ do-    -- for-    trimLFor <- trimOpen-    void $ P.string "for("-    e <- expr'-    void $ P.char ')'-    trimRFor <- trimClose-    -- body-    bodyBranch <- templateElems-    -- sep-    sepParse <- opt "sep"-    -- endfor-    trimLEnd <- trimOpen-    void $ P.string "endfor"-    trimREnd <- trimClose--    -- As sep is optional we need to sort out where any Trim_s need to go.-    let (forBody, sepBody) = maybe (forNoSep, Nothing) forSep sepParse-            where forNoSep =-                      [TrimR | trimRFor] ++ bodyBranch ++ [TrimL | trimLEnd]--                  forSep (trimLSep, sepBranch, trimRSep) = (forB, sepB)-                      where forB = [TrimR | trimRFor]-                                ++ bodyBranch-                                ++ [TrimL | trimLSep]--                            sepB = Just $ [TrimR | trimRSep]-                                       ++ sepBranch-                                       ++ [TrimL | trimLEnd]--    pure $ [TrimL | trimLFor] ++ [For e forBody sepBody] ++ [TrimR | trimREnd]------------------------------------------------------------------------------------partial :: P.Parser [TemplateElement]-partial = P.try $ do-    trimLPart <- trimOpen-    void $ P.string "partial("-    e <- expr'-    void $ P.char ')'-    trimRPart <- trimClose--    pure $ [TrimL | trimLPart] ++ [Partial e] ++ [TrimR | trimRPart]------------------------------------------------------------------------------------ident :: P.Parser TemplateExpr-ident = P.try $ Ident <$> key------------------------------------------------------------------------------------call :: P.Parser TemplateExpr-call = P.try $ do-    f <- key-    void $ P.char '('-    P.spaces-    as <- P.sepBy expr' (P.spaces >> P.char ',' >> P.spaces)-    P.spaces-    void $ P.char ')'-    return $ Call f as------------------------------------------------------------------------------------stringLiteral :: P.Parser TemplateExpr-stringLiteral = do-    void $ P.char '\"'-    str <- P.many $ do-        x <- P.noneOf "\""-        if x == '\\' then P.anyChar else return x-    void $ P.char '\"'-    return $ StringLiteral str------------------------------------------------------------------------------------key :: P.Parser TemplateKey-key = TemplateKey <$> metadataKey------------------------------------------------------------------------------------opt :: String -> P.Parser (Maybe (Bool, [TemplateElement], Bool))-opt clause = P.optionMaybe $ P.try $ do-    trimL <- trimOpen-    void $ P.string clause-    trimR <- trimClose-    branch <- templateElems-    pure (trimL, branch, trimR)-
− src/Hakyll/Web/Template/Internal/Trim.hs
@@ -1,95 +0,0 @@------------------------------------------------------------------------------------ | Module for trimming whitespace from tempaltes.-module Hakyll.Web.Template.Internal.Trim-    ( trim-    ) where------------------------------------------------------------------------------------import           Data.Char                            (isSpace)-import           Data.List                            (dropWhileEnd)------------------------------------------------------------------------------------import           Hakyll.Web.Template.Internal.Element------------------------------------------------------------------------------------trim :: [TemplateElement] -> [TemplateElement]-trim = cleanse . canonicalize-------------------------------------------------------------------------------------- | Apply the Trim nodes to the Chunks.-cleanse :: [TemplateElement] -> [TemplateElement]-cleanse = recurse cleanse . process-    where process [] = []-          process (TrimR:Chunk str:ts) = let str' = dropWhile isSpace str-                                         in if null str'-                                                then process ts-                                                -- Might need to TrimL.-                                                else process $ Chunk str':ts--          process (Chunk str:TrimL:ts) = let str' = dropWhileEnd isSpace str-                                         in if null str'-                                                then process ts-                                                else Chunk str':process ts--          process (t:ts) = t:process ts------------------------------------------------------------------------------------- | Enforce the invariant that:------     * Every 'TrimL' has a 'Chunk' to its left.---     * Every 'TrimR' has a 'Chunk' to its right.----canonicalize :: [TemplateElement] -> [TemplateElement]-canonicalize = go-    where go t = let t' = redundant . swap $ dedupe t-                 in if t == t' then t else go t'-------------------------------------------------------------------------------------- | Remove the 'TrimR' and 'TrimL's that are no-ops.-redundant :: [TemplateElement] -> [TemplateElement]-redundant = recurse redundant . process-    where -- Remove the leading 'TrimL's.-          process (TrimL:ts) = process ts-          -- Remove trailing 'TrimR's.-          process ts = foldr trailing [] ts-              where trailing TrimR [] = []-                    trailing x xs     = x:xs-------------------------------------------------------------------------------------- >>> swap $ [TrimR, TrimL]--- [TrimL, TrimR]-swap :: [TemplateElement] -> [TemplateElement]-swap = recurse swap . process-    where process []               = []-          process (TrimR:TrimL:ts) = TrimL:process (TrimR:ts)-          process (t:ts)           = t:process ts-------------------------------------------------------------------------------------- | Remove 'TrimR' and 'TrimL' duplication.-dedupe :: [TemplateElement] -> [TemplateElement]-dedupe = recurse dedupe . process-    where process []               = []-          process (TrimR:TrimR:ts) = process (TrimR:ts)-          process (TrimL:TrimL:ts) = process (TrimL:ts)-          process (t:ts)           = t:process ts-------------------------------------------------------------------------------------- | @'recurse' f t@ applies f to every '[TemplateElement]' in t.-recurse :: ([TemplateElement] -> [TemplateElement])-        -> [TemplateElement]-        -> [TemplateElement]-recurse _ []     = []-recurse f (x:xs) = process x:recurse f xs-    where process y = case y of-                          If e tb eb -> If e (f tb) (f <$> eb)-                          For e t s  -> For e (f t) (f <$> s)-                          _          -> y-
− src/Hakyll/Web/Template/List.hs
@@ -1,91 +0,0 @@------------------------------------------------------------------------------------ | Provides an easy way to combine several items in a list. The applications--- are obvious:------ * A post list on a blog------ * An image list in a gallery------ * A sitemap-{-# LANGUAGE TupleSections #-}-module Hakyll.Web.Template.List-    ( applyTemplateList-    , applyJoinTemplateList-    , chronological-    , recentFirst-    , sortChronological-    , sortRecentFirst-    ) where------------------------------------------------------------------------------------import           Control.Monad               (liftM)-import           Data.List                   (intersperse, sortBy)-import           Data.Ord                    (comparing)-import           Data.Time.Locale.Compat     (defaultTimeLocale)------------------------------------------------------------------------------------import           Hakyll.Core.Compiler-import           Hakyll.Core.Identifier-import           Hakyll.Core.Item-import           Hakyll.Core.Metadata-import           Hakyll.Web.Template-import           Hakyll.Web.Template.Context-------------------------------------------------------------------------------------- | Generate a string of a listing of pages, after applying a template to each--- page.-applyTemplateList :: Template-                  -> Context a-                  -> [Item a]-                  -> Compiler String-applyTemplateList = applyJoinTemplateList ""-------------------------------------------------------------------------------------- | Join a listing of pages with a string in between, after applying a template--- to each page.-applyJoinTemplateList :: String-                      -> Template-                      -> Context a-                      -> [Item a]-                      -> Compiler String-applyJoinTemplateList delimiter tpl context items = do-    items' <- mapM (applyTemplate tpl context) items-    return $ concat $ intersperse delimiter $ map itemBody items'-------------------------------------------------------------------------------------- | Sort pages chronologically. Uses the same method as 'dateField' for--- extracting the date.-chronological :: MonadMetadata m => [Item a] -> m [Item a]-chronological =-    sortByM $ getItemUTC defaultTimeLocale . itemIdentifier-  where-    sortByM :: (Monad m, Ord k) => (a -> m k) -> [a] -> m [a]-    sortByM f xs = liftM (map fst . sortBy (comparing snd)) $-                   mapM (\x -> liftM (x,) (f x)) xs-------------------------------------------------------------------------------------- | The reverse of 'chronological'-recentFirst :: MonadMetadata m => [Item a] -> m [Item a]-recentFirst = liftM reverse . chronological-------------------------------------------------------------------------------------- | Version of 'chronological' which doesn't need the actual items.-sortChronological-    :: MonadMetadata m => [Identifier] -> m [Identifier]-sortChronological ids =-    liftM (map itemIdentifier) $ chronological [Item i () | i <- ids]-------------------------------------------------------------------------------------- | Version of 'recentFirst' which doesn't need the actual items.-sortRecentFirst-    :: MonadMetadata m => [Identifier] -> m [Identifier]-sortRecentFirst ids =-    liftM (map itemIdentifier) $ recentFirst [Item i () | i <- ids]
+ src/Init.hs view
@@ -0,0 +1,96 @@+--------------------------------------------------------------------------------+module Main+    ( main+    ) where+++--------------------------------------------------------------------------------+import           Control.Arrow         (first)+import           Control.Monad         (forM_)+import           Data.Char             (isAlphaNum, isNumber)+import           Data.List             (foldl')+import           Data.List             (intercalate, isPrefixOf)+import           Data.Version          (Version (..))+import           System.Directory      (canonicalizePath, copyFile)+import           System.Environment    (getArgs, getProgName)+import           System.Exit           (exitFailure)+import           System.FilePath       (splitDirectories, (</>))+++--------------------------------------------------------------------------------+import           Hakyll.Core.Util.File+import           Paths_hakyll+++--------------------------------------------------------------------------------+main :: IO ()+main = do+    progName <- getProgName+    args     <- getArgs+    srcDir   <- getDataFileName "example"+    files    <- getRecursiveContents (const $ return False) srcDir++    case args of+        -- When the argument begins with hyphens, it's more likely that the user+        -- intends to attempt some arguments like ("--help", "-h", "--version", etc.)+        -- rather than create directory with that name.+        -- If dstDir begins with hyphens, the guard will prevent it from creating+        -- directory with that name so we can fall to the second alternative+        -- which prints a usage info for user.+        [dstDir] | not ("-" `isPrefixOf` dstDir) -> do+            forM_ files $ \file -> do+                let dst = dstDir </> file+                    src = srcDir </> file+                putStrLn $ "Creating " ++ dst+                makeDirectories dst+                copyFile src dst++            name <- makeName dstDir+            let cabalPath = dstDir </> name ++ ".cabal"+            putStrLn $ "Creating " ++ cabalPath+            createCabal cabalPath name+        _ -> do+            putStrLn $ "Usage: " ++ progName ++ " <directory>"+            exitFailure++-- | Figure out a good cabal package name from the given (existing) directory+-- name+makeName :: FilePath -> IO String+makeName dstDir = do+    canonical <- canonicalizePath dstDir+    return $ case safeLast (splitDirectories canonical) of+        Nothing  -> fallbackName+        Just "/" -> fallbackName+        Just x   -> repair (fallbackName ++) id x+  where+    -- Package name repair code comes from+    -- cabal-install.Distribution.Client.Init.Heuristics+    repair invalid valid x = case dropWhile (not . isAlphaNum) x of+        "" -> repairComponent ""+        x' -> let (c, r) = first repairComponent $ break (not . isAlphaNum) x'+              in c ++ repairRest r+      where repairComponent c | all isNumber c = invalid c+                              | otherwise = valid c+    repairRest = repair id ('-' :)+    fallbackName = "site"++    safeLast = foldl' (\_ x -> Just x) Nothing++createCabal :: FilePath -> String -> IO ()+createCabal path name = do+    writeFile path $ unlines [+        "name:               " ++ name+      , "version:            0.1.0.0"+      , "build-type:         Simple"+      , "cabal-version:      >= 1.10"+      , ""+      , "executable site"+      , "  main-is:          site.hs"+      , "  build-depends:    base == 4.*"+      , "                  , hakyll == " ++ version' ++ ".*"+      , "  ghc-options:      -threaded"+      , "  default-language: Haskell2010"+      ]+  where+    -- Major hakyll version+    version' = intercalate "." . take 2 . map show $ versionBranch version