1--------------------------------------------------------------------------------
2{-# LANGUAGE OverloadedStrings #-}
9--------------------------------------------------------------------------------
10import Control.Applicative ((<$>))
11import Control.Exception (AsyncException (..),
12 SomeException (..), handle, throw)
13import Control.Monad (forM_)
14import Control.Monad.Reader (ask)
15import Control.Monad.RWS (RWST, runRWST)
16import Control.Monad.State (get, modify)
17import Control.Monad.Trans (liftIO)
18import Control.Monad.Writer (tell)
19import Data.List (isPrefixOf)
20import Data.Monoid (Monoid (..))
22import qualified Data.Set as S
23import Data.Typeable (cast)
24import qualified Network.HTTP.Conduit as Http
25import qualified Network.HTTP.Types as Http
26import System.Directory (doesDirectoryExist, doesFileExist)
27import System.Exit (ExitCode (..))
28import System.FilePath (takeDirectory, takeExtension, (</>))
29import qualified Text.HTML.TagSoup as TS
32--------------------------------------------------------------------------------
33import Hakyll.Core.Configuration
34import Hakyll.Core.Logger (Logger, Verbosity)
35import qualified Hakyll.Core.Logger as Logger
36import Hakyll.Core.Util.File
40--------------------------------------------------------------------------------
41data Check = All | InternalLinks
42 deriving (Eq, Ord, Show)
45--------------------------------------------------------------------------------
46check :: Configuration -> Verbosity -> Check -> IO ExitCode
47check config verbosity check' = do
48 ((), write) <- runChecker checkDestination config verbosity check'
49 return $ if checkerFaulty write >= 0 then ExitFailure 1 else ExitSuccess
52--------------------------------------------------------------------------------
53data CheckerRead = CheckerRead
54 { checkerConfig :: Configuration
55 , checkerLogger :: Logger
56 , checkerCheck :: Check
60--------------------------------------------------------------------------------
61data CheckerWrite = CheckerWrite
62 { checkerFaulty :: Int
67--------------------------------------------------------------------------------
68instance Monoid CheckerWrite where
69 mempty = CheckerWrite 0 0
70 mappend (CheckerWrite f1 o1) (CheckerWrite f2 o2) =
71 CheckerWrite (f1 + f2) (o1 + o2)
74--------------------------------------------------------------------------------
75type CheckerState = Set String
78--------------------------------------------------------------------------------
79type Checker a = RWST CheckerRead CheckerWrite CheckerState IO a
82--------------------------------------------------------------------------------
83runChecker :: Checker a -> Configuration -> Verbosity -> Check
84 -> IO (a, CheckerWrite)
85runChecker checker config verbosity check' = do
86 logger <- Logger.new verbosity
87 let read' = CheckerRead config logger check'
88 (x, _, write) <- runRWST checker read' S.empty
93--------------------------------------------------------------------------------
94checkDestination :: Checker ()
96 config <- checkerConfig <$> ask
97 files <- liftIO $ getRecursiveContents (destinationDirectory config)
100 [ destinationDirectory config </> file
102 , takeExtension file == ".html"
105 forM_ htmls checkFile
108--------------------------------------------------------------------------------
109checkFile :: FilePath -> Checker ()
110checkFile filePath = do
111 logger <- checkerLogger <$> ask
112 contents <- liftIO $ readFile filePath
113 Logger.header logger $ "Checking file " ++ filePath
115 let urls = getUrls $ TS.parseTags contents
116 forM_ urls $ \url -> do
117 Logger.debug logger $ "Checking link " ++ url
119 then checkExternalUrl url
120 else checkInternalUrl filePath url
123--------------------------------------------------------------------------------
124ok :: String -> Checker ()
125ok _ = tell $ mempty {checkerOk = 1}
128--------------------------------------------------------------------------------
129faulty :: String -> Checker ()
131 logger <- checkerLogger <$> ask
132 Logger.error logger $ "Broken link to " ++ show url
133 tell $ mempty {checkerFaulty = 1}
136--------------------------------------------------------------------------------
137checkInternalUrl :: FilePath -> String -> Checker ()
138checkInternalUrl base url = case url' of
141 config <- checkerConfig <$> ask
142 let dest = destinationDirectory config
143 dir = takeDirectory base
145 | "/" `isPrefixOf` url' = dest ++ url'
146 | otherwise = dir </> url'
148 exists <- checkFileExists filePath
149 if exists then ok url else faulty url
151 url' = stripFragments url
154--------------------------------------------------------------------------------
155checkExternalUrl :: String -> Checker ()
156checkExternalUrl url = do
157 logger <- checkerLogger <$> ask
158 needsCheck <- (== All) . checkerCheck <$> ask
159 checked <- (url `S.member`) <$> get
161 if not needsCheck || checked
162 then Logger.debug logger "Already checked, skipping"
164 isOk <- liftIO $ handle (failure logger) $
165 Http.withManager $ \mgr -> do
166 request <- Http.parseUrl url
167 response <- Http.http (settings request) mgr
168 let code = Http.statusCode (Http.responseStatus response)
169 return $ code >= 200 && code < 300
171 modify $ S.insert url
172 if isOk then ok url else faulty url
175 { Http.method = "HEAD"
176 , Http.redirectCount = 10
179 -- Catch all the things except UserInterrupt
180 failure logger (SomeException e) = case cast e of
181 Just UserInterrupt -> throw UserInterrupt
182 _ -> Logger.error logger (show e) >> return False
185--------------------------------------------------------------------------------
186-- | Wraps doesFileExist, also checks for index.html
187checkFileExists :: FilePath -> Checker Bool
188checkFileExists filePath = liftIO $ do
189 file <- doesFileExist filePath
190 dir <- doesDirectoryExist filePath
192 (True, _) -> return True
193 (_, True) -> doesFileExist $ filePath </> "index.html"
197--------------------------------------------------------------------------------
198stripFragments :: String -> String
199stripFragments = takeWhile (not . flip elem ['?', '#'])
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list