----------------------------------------------------------------------------- -- | -- Module : Haddock.Backends.Html.Themes -- Copyright : (c) Mark Lentczner 2010 -- License : BSD-like -- -- Maintainer : haddock@projects.haskell.org -- Stability : experimental -- Portability : portable ----------------------------------------------------------------------------- module Haddock.Backends.Xhtml.Themes ( Themes, getThemes, cssFiles, styleSheet, stylePickers, styleMenu ) where import Haddock.Backends.Xhtml.Utils (onclick) import Haddock.Options import Control.Monad (liftM) import Data.Char (toLower) import Data.Either (lefts, rights) import Data.List (nub, partition) import Data.Maybe (isJust, listToMaybe) import System.Directory import System.FilePath import Text.XHtml hiding ( name, title, p, quote, () ) import qualified Text.XHtml as XHtml -------------------------------------------------------------------------------- -- * CSS Themes -------------------------------------------------------------------------------- data Theme = Theme { themeName :: String, themeHref :: String, themeFiles :: [FilePath] } type Themes = [Theme] type PossibleTheme = Either String Theme type PossibleThemes = Either String Themes -- | Find a theme by name (case insensitive match) findTheme :: String -> Themes -> Maybe Theme findTheme s = listToMaybe . filter ((== ls).lower.themeName) where lower = map toLower ls = lower s -- | Standard theme used by default standardTheme :: FilePath -> IO PossibleThemes standardTheme libDir = liftM (liftEither (take 1)) (defaultThemes libDir) -- | Default themes that are part of Haddock; added with --default-themes -- First default theme is the standard theme. At present, hard coded to "Ocean" -- if present. defaultThemes :: FilePath -> IO PossibleThemes defaultThemes libDir = do themeDirs <- getDirectoryItems (libDir "html" "themes") themes <- mapM directoryTheme themeDirs return $ liftEither shuffle $ sequenceEither themes where shuffle ts = let (a,b) = partition isStd ts in a ++ b isStd c = themeName c == "Ocean" -- | Build a theme from a single .css file singleFileTheme :: FilePath -> IO PossibleTheme singleFileTheme path = if isCssFilePath path then retRight $ Theme name file [path] else errMessage "File extension isn't .css" path where name = takeBaseName path file = takeFileName path -- | Build a theme from a directory directoryTheme :: FilePath -> IO PossibleTheme directoryTheme path = do items <- getDirectoryItems path case filter isCssFilePath items of [cf] -> retRight $ Theme (takeBaseName path) (takeFileName cf) items [] -> errMessage "No .css file in theme directory" path _ -> errMessage "More than one .css file in theme directory" path -- | Check if we have a built in theme doesBuiltInExist :: IO PossibleThemes -> String -> IO Bool doesBuiltInExist pts s = pts >>= return . either (const False) test where test = isJust . findTheme s -- | Find a built in theme builtInTheme :: IO PossibleThemes -> String -> IO PossibleTheme builtInTheme pts s = pts >>= return . either Left fetch where fetch = maybe (Left ("Unknown theme: " ++ s)) Right . findTheme s -------------------------------------------------------------------------------- -- * CSS Theme Arguments -------------------------------------------------------------------------------- -- | Process input flags for CSS Theme arguments getThemes :: FilePath -> [Flag] -> IO PossibleThemes getThemes libDir flags = liftM concatEither (mapM themeFlag flags) >>= someTheme where themeFlag :: Flag -> IO (Either String Themes) themeFlag (Flag_CSS path) = (liftM . liftEither) (:[]) (theme path) themeFlag (Flag_BuiltInThemes) = builtIns themeFlag _ = retRight [] theme :: FilePath -> IO PossibleTheme theme path = pick path [(doesFileExist, singleFileTheme), (doesDirectoryExist, directoryTheme), (doesBuiltInExist builtIns, builtInTheme builtIns)] "Theme not found" pick :: FilePath -> [(FilePath -> IO Bool, FilePath -> IO PossibleTheme)] -> String -> IO PossibleTheme pick path [] msg = errMessage msg path pick path ((test,build):opts) msg = do pass <- test path if pass then build path else pick path opts msg someTheme :: Either String Themes -> IO (Either String Themes) someTheme (Right []) = standardTheme libDir someTheme est = return est builtIns = defaultThemes libDir errMessage :: String -> FilePath -> IO (Either String a) errMessage msg path = return (Left msg') where msg' = "Error: " ++ msg ++ ": \"" ++ path ++ "\"\n" retRight :: a -> IO (Either String a) retRight = return . Right -------------------------------------------------------------------------------- -- * File Utilities -------------------------------------------------------------------------------- getDirectoryItems :: FilePath -> IO [FilePath] getDirectoryItems path = getDirectoryContents path >>= return . map (combine path) . filter notDot where notDot s = s /= "." && s /= ".." isCssFilePath :: FilePath -> Bool isCssFilePath path = takeExtension path == ".css" -------------------------------------------------------------------------------- -- * Style Sheet Utilities -------------------------------------------------------------------------------- cssFiles :: Themes -> [String] cssFiles ts = nub $ concatMap themeFiles ts styleSheet :: Themes -> Html styleSheet ts = toHtml $ zipWith mkLink rels ts where rels = ("stylesheet" : repeat "alternate stylesheet") mkLink aRel t = thelink ! [ href (themeHref t), rel aRel, thetype "text/css", XHtml.title (themeName t) ] << noHtml stylePickers :: Themes -> [Html] stylePickers ts = map mkPicker ts where mkPicker t = let js = "setActiveStyleSheet('" ++ themeHref t ++ "'); return false;" in anchor ! [href "#", onclick js] << themeName t styleMenu :: Themes -> Html styleMenu [] = noHtml styleMenu [_] = noHtml styleMenu ts = thediv ! [identifier "style-menu-holder"] << [ anchor ! [ href "#", onclick js ] << "Style \9662", unordList (stylePickers ts) ! [ identifier "style-menu", theclass "hide" ] ] where js = "styleMenu(); return false;" -------------------------------------------------------------------------------- -- * Either Utilities -------------------------------------------------------------------------------- -- These three routines are here because Haddock does not have access to the -- Control.Monad.Error module which supplies the Functor and Monad instances -- for Either String. sequenceEither :: [Either a b] -> Either a [b] sequenceEither es = maybe (Right $ rights es) Left (listToMaybe (lefts es)) liftEither :: (b -> c) -> Either a b -> Either a c liftEither f = either Left (Right . f) concatEither :: [Either a [b]] -> Either a [b] concatEither = liftEither concat . sequenceEither