1--------------------------------------------------------------------------------
3{-# LANGUAGE OverloadedStrings #-}
10--------------------------------------------------------------------------------
11import Control.Monad (forM_)
12import Control.Monad.Reader (ask)
13import Control.Monad.RWS (RWST, runRWST)
14import Control.Monad.Trans (liftIO)
15import Control.Monad.Trans.Resource (runResourceT)
16import Control.Monad.Writer (tell)
17import Data.List (isPrefixOf)
19import qualified Data.Set as S
20import Network.URI (unEscapeString)
21import System.Directory (doesDirectoryExist,
23import System.Exit (ExitCode (..))
24import System.FilePath (takeDirectory, takeExtension,
26import qualified Text.HTML.TagSoup as TS
29--------------------------------------------------------------------------------
31import Control.Exception (AsyncException (..),
32 SomeException (..), handle,
34import Control.Monad.State (get, modify)
35import Data.List (intercalate)
36import Data.Typeable (cast)
37import Data.Version (versionBranch)
38import GHC.Exts (fromString)
39import qualified Network.HTTP.Conduit as Http
40import qualified Network.HTTP.Types as Http
41import qualified Paths_hakyll as Paths_hakyll
45--------------------------------------------------------------------------------
46import Hakyll.Core.Configuration
47import Hakyll.Core.Logger (Logger)
48import qualified Hakyll.Core.Logger as Logger
49import Hakyll.Core.Util.File
53--------------------------------------------------------------------------------
54data Check = All | InternalLinks
55 deriving (Eq, Ord, Show)
58--------------------------------------------------------------------------------
59check :: Configuration -> Logger -> Check -> IO ExitCode
60check config logger check' = do
61 ((), write) <- runChecker checkDestination config logger check'
62 return $ if checkerFaulty write > 0 then ExitFailure 1 else ExitSuccess
65--------------------------------------------------------------------------------
66data CheckerRead = CheckerRead
67 { checkerConfig :: Configuration
68 , checkerLogger :: Logger
69 , checkerCheck :: Check
73--------------------------------------------------------------------------------
74data CheckerWrite = CheckerWrite
75 { checkerFaulty :: Int
80--------------------------------------------------------------------------------
81instance Monoid CheckerWrite where
82 mempty = CheckerWrite 0 0
83 mappend (CheckerWrite f1 o1) (CheckerWrite f2 o2) =
84 CheckerWrite (f1 + f2) (o1 + o2)
87--------------------------------------------------------------------------------
88type CheckerState = Set String
91--------------------------------------------------------------------------------
92type Checker a = RWST CheckerRead CheckerWrite CheckerState IO a
95--------------------------------------------------------------------------------
96runChecker :: Checker a -> Configuration -> Logger -> Check
97 -> IO (a, CheckerWrite)
98runChecker checker config logger check' = do
99 let read' = CheckerRead
100 { checkerConfig = config
101 , checkerLogger = logger
102 , checkerCheck = check'
105 (x, _, write) <- runRWST checker read' S.empty
110--------------------------------------------------------------------------------
111checkDestination :: Checker ()
113 config <- checkerConfig <$> ask
114 files <- liftIO $ getRecursiveContents
115 (const $ return False) (destinationDirectory config)
118 [ destinationDirectory config </> file
120 , takeExtension file == ".html"
123 forM_ htmls checkFile
126--------------------------------------------------------------------------------
127checkFile :: FilePath -> Checker ()
128checkFile filePath = do
129 logger <- checkerLogger <$> ask
130 contents <- liftIO $ readFile filePath
131 Logger.header logger $ "Checking file " ++ filePath
133 let urls = getUrls $ TS.parseTags contents
134 forM_ urls $ \url -> do
135 Logger.debug logger $ "Checking link " ++ url
136 checkUrl filePath url
139--------------------------------------------------------------------------------
140checkUrl :: FilePath -> String -> Checker ()
142 | isExternal url = checkExternalUrl url
143 | hasProtocol url = skip "Unknown protocol, skipping"
144 | otherwise = checkInternalUrl filePath url
146 validProtoChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "+-."
147 hasProtocol str = case break (== ':') str of
148 (proto, ':' : _) -> all (`elem` validProtoChars) proto
152--------------------------------------------------------------------------------
153ok :: String -> Checker ()
154ok _ = tell $ mempty {checkerOk = 1}
157--------------------------------------------------------------------------------
158skip :: String -> Checker ()
160 logger <- checkerLogger <$> ask
161 Logger.debug logger $ reason
162 tell $ mempty {checkerOk = 1}
164--------------------------------------------------------------------------------
165faulty :: String -> Checker ()
167 logger <- checkerLogger <$> ask
168 Logger.error logger $ "Broken link to " ++ show url
169 tell $ mempty {checkerFaulty = 1}
172--------------------------------------------------------------------------------
173checkInternalUrl :: FilePath -> String -> Checker ()
174checkInternalUrl base url = case url' of
177 config <- checkerConfig <$> ask
178 let dest = destinationDirectory config
179 dir = takeDirectory base
181 | "/" `isPrefixOf` url' = dest ++ url'
182 | otherwise = dir </> url'
184 exists <- checkFileExists filePath
185 if exists then ok url else faulty url
187 url' = stripFragments $ unEscapeString url
190--------------------------------------------------------------------------------
191checkExternalUrl :: String -> Checker ()
193checkExternalUrl url = do
194 logger <- checkerLogger <$> ask
195 needsCheck <- (== All) . checkerCheck <$> ask
196 checked <- (url `S.member`) <$> get
198 if not needsCheck || checked
199 then Logger.debug logger "Already checked, skipping"
201 isOk <- liftIO $ handle (failure logger) $ do
202 mgr <- Http.newManager Http.tlsManagerSettings
204 request <- Http.parseUrl urlToCheck
205 response <- Http.http (settings request) mgr
206 let code = Http.statusCode (Http.responseStatus response)
207 return $ code >= 200 && code < 300
209 modify $ if schemeRelative url
210 then S.insert urlToCheck . S.insert url
212 if isOk then ok url else faulty url
214 -- Add additional request info
216 { Http.method = "HEAD"
217 , Http.redirectCount = 10
218 , Http.requestHeaders = ("User-Agent", ua) : Http.requestHeaders r
221 -- Nice user agent info
222 ua = fromString $ "hakyll-check/" ++
223 (intercalate "." $ map show $ versionBranch $ Paths_hakyll.version)
225 -- Catch all the things except UserInterrupt
226 failure logger (SomeException e) = case cast e of
227 Just UserInterrupt -> throw UserInterrupt
228 _ -> Logger.error logger (show e) >> return False
230 -- Check scheme-relative links
231 schemeRelative = isPrefixOf "//"
232 urlToCheck = if schemeRelative url then "http:" ++ url else url
234checkExternalUrl _ = return ()
239--------------------------------------------------------------------------------
240-- | Wraps doesFileExist, also checks for index.html
241checkFileExists :: FilePath -> Checker Bool
242checkFileExists filePath = liftIO $ do
243 file <- doesFileExist filePath
244 dir <- doesDirectoryExist filePath
246 (True, _) -> return True
247 (_, True) -> doesFileExist $ filePath </> "index.html"
251--------------------------------------------------------------------------------
252stripFragments :: String -> String
253stripFragments = 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