1--------------------------------------------------------------------------------
3{-# LANGUAGE OverloadedStrings #-}
10--------------------------------------------------------------------------------
11import Control.Applicative ((<$>))
12import Control.Monad (forM_)
13import Control.Monad.Reader (ask)
14import Control.Monad.RWS (RWST, runRWST)
15import Control.Monad.Trans (liftIO)
16import Control.Monad.Writer (tell)
17import Data.List (isPrefixOf)
18import Data.Monoid (Monoid (..))
20import qualified Data.Set as S
21import System.Directory (doesDirectoryExist, doesFileExist)
22import System.Exit (ExitCode (..))
23import System.FilePath (takeDirectory, takeExtension, (</>))
24import qualified Text.HTML.TagSoup as TS
27--------------------------------------------------------------------------------
29import Control.Exception (AsyncException (..),
30 SomeException (..), handle, throw)
31import Control.Monad.State (get, modify)
32import Data.List (intercalate)
33import Data.Typeable (cast)
34import Data.Version (versionBranch)
35import GHC.Exts (fromString)
36import qualified Network.HTTP.Conduit as Http
37import qualified Network.HTTP.Types as Http
38import qualified Paths_hakyll as Paths_hakyll
42--------------------------------------------------------------------------------
43import Hakyll.Core.Configuration
44import Hakyll.Core.Logger (Logger, Verbosity)
45import qualified Hakyll.Core.Logger as Logger
46import Hakyll.Core.Util.File
50--------------------------------------------------------------------------------
51data Check = All | InternalLinks
52 deriving (Eq, Ord, Show)
55--------------------------------------------------------------------------------
56check :: Configuration -> Verbosity -> Check -> IO ExitCode
57check config verbosity check' = do
58 ((), write) <- runChecker checkDestination config verbosity check'
59 return $ if checkerFaulty write >= 0 then ExitFailure 1 else ExitSuccess
62--------------------------------------------------------------------------------
63data CheckerRead = CheckerRead
64 { checkerConfig :: Configuration
65 , checkerLogger :: Logger
66 , checkerCheck :: Check
70--------------------------------------------------------------------------------
71data CheckerWrite = CheckerWrite
72 { checkerFaulty :: Int
77--------------------------------------------------------------------------------
78instance Monoid CheckerWrite where
79 mempty = CheckerWrite 0 0
80 mappend (CheckerWrite f1 o1) (CheckerWrite f2 o2) =
81 CheckerWrite (f1 + f2) (o1 + o2)
84--------------------------------------------------------------------------------
85type CheckerState = Set String
88--------------------------------------------------------------------------------
89type Checker a = RWST CheckerRead CheckerWrite CheckerState IO a
92--------------------------------------------------------------------------------
93runChecker :: Checker a -> Configuration -> Verbosity -> Check
94 -> IO (a, CheckerWrite)
95runChecker checker config verbosity check' = do
96 logger <- Logger.new verbosity
97 let read' = CheckerRead
98 { checkerConfig = config
99 , checkerLogger = logger
100 , checkerCheck = check'
103 (x, _, write) <- runRWST checker read' S.empty
108--------------------------------------------------------------------------------
109checkDestination :: Checker ()
111 config <- checkerConfig <$> ask
113 getRecursiveContents (const False) (destinationDirectory config)
116 [ destinationDirectory config </> file
118 , takeExtension file == ".html"
121 forM_ htmls checkFile
124--------------------------------------------------------------------------------
125checkFile :: FilePath -> Checker ()
126checkFile filePath = do
127 logger <- checkerLogger <$> ask
128 contents <- liftIO $ readFile filePath
129 Logger.header logger $ "Checking file " ++ filePath
131 let urls = getUrls $ TS.parseTags contents
132 forM_ urls $ \url -> do
133 Logger.debug logger $ "Checking link " ++ url
134 checkUrl filePath url
137--------------------------------------------------------------------------------
138checkUrl :: FilePath -> String -> Checker ()
140 | isExternal url = checkExternalUrl url
141 | hasProtocol url = skip "Unknown protocol, skipping"
142 | otherwise = checkInternalUrl filePath url
144 validProtoChars = ['A'..'Z'] ++ ['a'..'z'] ++ ['0'..'9'] ++ "+-."
145 hasProtocol str = case break (== ':') str of
146 (proto, ':' : _) -> all (`elem` validProtoChars) proto
150--------------------------------------------------------------------------------
151ok :: String -> Checker ()
152ok _ = tell $ mempty {checkerOk = 1}
155--------------------------------------------------------------------------------
156skip :: String -> Checker ()
158 logger <- checkerLogger <$> ask
159 Logger.debug logger $ reason
160 tell $ mempty {checkerOk = 1}
162--------------------------------------------------------------------------------
163faulty :: String -> Checker ()
165 logger <- checkerLogger <$> ask
166 Logger.error logger $ "Broken link to " ++ show url
167 tell $ mempty {checkerFaulty = 1}
170--------------------------------------------------------------------------------
171checkInternalUrl :: FilePath -> String -> Checker ()
172checkInternalUrl base url = case url' of
175 config <- checkerConfig <$> ask
176 let dest = destinationDirectory config
177 dir = takeDirectory base
179 | "/" `isPrefixOf` url' = dest ++ url'
180 | otherwise = dir </> url'
182 exists <- checkFileExists filePath
183 if exists then ok url else faulty url
185 url' = stripFragments url
188--------------------------------------------------------------------------------
189checkExternalUrl :: String -> Checker ()
191checkExternalUrl url = do
192 logger <- checkerLogger <$> ask
193 needsCheck <- (== All) . checkerCheck <$> ask
194 checked <- (url `S.member`) <$> get
196 if not needsCheck || checked
197 then Logger.debug logger "Already checked, skipping"
199 isOk <- liftIO $ handle (failure logger) $
200 Http.withManager $ \mgr -> do
201 request <- Http.parseUrl url
202 response <- Http.http (settings request) mgr
203 let code = Http.statusCode (Http.responseStatus response)
204 return $ code >= 200 && code < 300
206 modify $ S.insert url
207 if isOk then ok url else faulty url
209 -- Add additional request info
211 { Http.method = "HEAD"
212 , Http.redirectCount = 10
213 , Http.requestHeaders = ("User-Agent", ua) : Http.requestHeaders r
216 -- Nice user agent info
217 ua = fromString $ "hakyll-check/" ++
218 (intercalate "." $ map show $ versionBranch $ Paths_hakyll.version)
220 -- Catch all the things except UserInterrupt
221 failure logger (SomeException e) = case cast e of
222 Just UserInterrupt -> throw UserInterrupt
223 _ -> Logger.error logger (show e) >> return False
225checkExternalUrl _ = return ()
230--------------------------------------------------------------------------------
231-- | Wraps doesFileExist, also checks for index.html
232checkFileExists :: FilePath -> Checker Bool
233checkFileExists filePath = liftIO $ do
234 file <- doesFileExist filePath
235 dir <- doesDirectoryExist filePath
237 (True, _) -> return True
238 (_, True) -> doesFileExist $ filePath </> "index.html"
242--------------------------------------------------------------------------------
243stripFragments :: String -> String
244stripFragments = 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