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 (intercalate, isPrefixOf)
20import Data.Monoid (Monoid (..))
22import qualified Data.Set as S
23import Data.Typeable (cast)
24import GHC.Exts (fromString)
25import qualified Network.HTTP.Conduit as Http
26import qualified Network.HTTP.Types as Http
27import System.Directory (doesDirectoryExist, doesFileExist)
28import System.Exit (ExitCode (..))
29import System.FilePath (takeDirectory, takeExtension, (</>))
30import qualified Text.HTML.TagSoup as TS
33--------------------------------------------------------------------------------
34import Data.Version (versionBranch)
35import Hakyll.Core.Configuration
36import Hakyll.Core.Logger (Logger, Verbosity)
37import qualified Hakyll.Core.Logger as Logger
38import Hakyll.Core.Util.File
40import qualified Paths_hakyll as Paths_hakyll
43--------------------------------------------------------------------------------
44data Check = All | InternalLinks
45 deriving (Eq, Ord, Show)
48--------------------------------------------------------------------------------
49check :: Configuration -> Verbosity -> Check -> IO ExitCode
50check config verbosity check' = do
51 ((), write) <- runChecker checkDestination config verbosity check'
52 return $ if checkerFaulty write >= 0 then ExitFailure 1 else ExitSuccess
55--------------------------------------------------------------------------------
56data CheckerRead = CheckerRead
57 { checkerConfig :: Configuration
58 , checkerLogger :: Logger
59 , checkerCheck :: Check
63--------------------------------------------------------------------------------
64data CheckerWrite = CheckerWrite
65 { checkerFaulty :: Int
70--------------------------------------------------------------------------------
71instance Monoid CheckerWrite where
72 mempty = CheckerWrite 0 0
73 mappend (CheckerWrite f1 o1) (CheckerWrite f2 o2) =
74 CheckerWrite (f1 + f2) (o1 + o2)
77--------------------------------------------------------------------------------
78type CheckerState = Set String
81--------------------------------------------------------------------------------
82type Checker a = RWST CheckerRead CheckerWrite CheckerState IO a
85--------------------------------------------------------------------------------
86runChecker :: Checker a -> Configuration -> Verbosity -> Check
87 -> IO (a, CheckerWrite)
88runChecker checker config verbosity check' = do
89 logger <- Logger.new verbosity
90 let read' = CheckerRead config logger check'
91 (x, _, write) <- runRWST checker read' S.empty
96--------------------------------------------------------------------------------
97checkDestination :: Checker ()
99 config <- checkerConfig <$> ask
101 getRecursiveContents (const False) (destinationDirectory config)
104 [ destinationDirectory config </> file
106 , takeExtension file == ".html"
109 forM_ htmls checkFile
112--------------------------------------------------------------------------------
113checkFile :: FilePath -> Checker ()
114checkFile filePath = do
115 logger <- checkerLogger <$> ask
116 contents <- liftIO $ readFile filePath
117 Logger.header logger $ "Checking file " ++ filePath
119 let urls = getUrls $ TS.parseTags contents
120 forM_ urls $ \url -> do
121 Logger.debug logger $ "Checking link " ++ url
123 then checkExternalUrl url
124 else checkInternalUrl filePath url
127--------------------------------------------------------------------------------
128ok :: String -> Checker ()
129ok _ = tell $ mempty {checkerOk = 1}
132--------------------------------------------------------------------------------
133faulty :: String -> Checker ()
135 logger <- checkerLogger <$> ask
136 Logger.error logger $ "Broken link to " ++ show url
137 tell $ mempty {checkerFaulty = 1}
140--------------------------------------------------------------------------------
141checkInternalUrl :: FilePath -> String -> Checker ()
142checkInternalUrl base url = case url' of
145 config <- checkerConfig <$> ask
146 let dest = destinationDirectory config
147 dir = takeDirectory base
149 | "/" `isPrefixOf` url' = dest ++ url'
150 | otherwise = dir </> url'
152 exists <- checkFileExists filePath
153 if exists then ok url else faulty url
155 url' = stripFragments url
158--------------------------------------------------------------------------------
159checkExternalUrl :: String -> Checker ()
160checkExternalUrl url = do
161 logger <- checkerLogger <$> ask
162 needsCheck <- (== All) . checkerCheck <$> ask
163 checked <- (url `S.member`) <$> get
165 if not needsCheck || checked
166 then Logger.debug logger "Already checked, skipping"
168 isOk <- liftIO $ handle (failure logger) $
169 Http.withManager $ \mgr -> do
170 request <- Http.parseUrl url
171 response <- Http.http (settings request) mgr
172 let code = Http.statusCode (Http.responseStatus response)
173 return $ code >= 200 && code < 300
175 modify $ S.insert url
176 if isOk then ok url else faulty url
178 -- Add additional request info
180 { Http.method = "HEAD"
181 , Http.redirectCount = 10
182 , Http.requestHeaders = ("User-Agent", ua) : Http.requestHeaders r
185 -- Nice user agent info
186 ua = fromString $ "hakyll-check/" ++
187 (intercalate "." $ map show $ versionBranch $ Paths_hakyll.version)
189 -- Catch all the things except UserInterrupt
190 failure logger (SomeException e) = case cast e of
191 Just UserInterrupt -> throw UserInterrupt
192 _ -> Logger.error logger (show e) >> return False
195--------------------------------------------------------------------------------
196-- | Wraps doesFileExist, also checks for index.html
197checkFileExists :: FilePath -> Checker Bool
198checkFileExists filePath = liftIO $ do
199 file <- doesFileExist filePath
200 dir <- doesDirectoryExist filePath
202 (True, _) -> return True
203 (_, True) -> doesFileExist $ filePath </> "index.html"
207--------------------------------------------------------------------------------
208stripFragments :: String -> String
209stripFragments = 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