[Basic HTML rendering except items Manuel M T Chakravarty **20060427225105] { addfile ./Config.hs hunk ./Config.hs 1 +-- |A simple feed generator: Configuration management and command line parsing +-- +-- Copyright (c) 2006 Manuel M T Chakravarty +-- +-- License: +-- +--- Description --------------------------------------------------------------- +-- +-- Language: Haskell 98 +-- + +module Config ( + + -- * File suffixes + chanSuffix, itemSuffix, htmlSuffix, rssSuffix, + + -- * Configuration record + Config(..), + + -- * Command line argument parsing + processArgs + +) where + +-- hierachical libraries +-- + +import Control.Monad ( + when, foldM) + +import System.Console.GetOpt ( + ArgOrder(..), OptDescr(..), ArgDescr(..), getOpt, usageInfo) +import System.Environment ( + getArgs, getProgName) + +-- lambdaFeed +import Error ( + abortWithIO, exitSuccess) + + +-- Version information +-- ------------------- + +name = "lambdaFeed" +versnum = "0.1.0" +versnick = "\"Lambdas for all!\"" +date = "26 Apr 2006" +version = name ++ ", version " ++ versnum ++ " " ++ versnick ++ ", " ++ date +copyright = "Copyright (c) 2006 Manuel M T Chakravarty" +disclaimer = "This software is distributed under the \ + \terms of the GNU Public Licence.\n\ + \NO WARRANTY WHATSOEVER IS PROVIDED. \ + \See ." + + +-- Constants +-- --------- + +-- Suffixes +-- +chanSuffix = "lfc" +itemSuffix = "lfi" +htmlSuffix = "html" +rssSuffix = "xml" + + +-- Dynamic configuration +-- --------------------- + +-- Configuration +-- +data Config = Config { + debugCfg :: Int, + feedDirCfg :: FilePath, + htmlDirCfg :: FilePath, + rssDirCfg :: FilePath, + verboseCfg :: Bool, + quietCfg :: Bool + } + +-- Default configuration +-- +dftConfig = Config { + debugCfg = 0, + feedDirCfg = "feed", + htmlDirCfg = "", + rssDirCfg = "", + verboseCfg = False, + quietCfg = False + } + +-- Available options +-- +data Option = Debug + | FeedDirOpt FilePath + | Help + | HTMLDirOpt FilePath + | RSSDirOpt FilePath + | OutputOpt FilePath + | Verbose + | Version + | Quiet + deriving Eq + +-- Option description +-- +options :: [OptDescr Option] +options = [ Option ['d'] ["debug"] (NoArg Debug) + "produces extra diagnostic output; repeat for more" + , Option ['f'] ["feed"] (ReqArg FeedDirOpt "DIR") + "feed directory with channel descriptions (default: feed/)" + , Option ['h', '?'] ["help"] (NoArg Help) + "this help message" + , Option [] ["htmldir"] (ReqArg HTMLDirOpt "DIR") + "target directory for generated HTML files" + , Option [] ["rssdir"] (ReqArg RSSDirOpt "DIR") + "target directory for generated RSS 2.0 files" + , Option ['o'] ["output"] (ReqArg OutputOpt "DIR") + "sets target directory for both HTML and RSS 2.0 files" + , Option ['v'] ["verbose"] (NoArg Verbose) + "print summary information (default: off)" + , Option ['V'] ["version"] (NoArg Version) + "version information" + , Option ['q'] ["quiet"] (NoArg Quiet) + "suppress all output, including parser warnings" + ] + +-- Option processing +-- +processArgs :: IO Config +processArgs = + do + args <- getArgs + case getOpt RequireOrder options args of + (opts, [] , [] ) -> processVersion opts >>= + processHelp >>= + processConfig dftConfig + (opts, args, [] ) -> abort [unrecErr ++ unwords args] + (_ , _ , errs) -> abort errs + where + unrecErr = "Unrecognised arguments: " + +-- Print version information if `Help' or `Version' requested and remove any +-- `Version' options. +-- +processVersion :: [Option] -> IO [Option] +processVersion opts = + do + when (Version `elem` opts || Help `elem` opts) $ do + putStrLn version + putStrLn copyright + putStrLn disclaimer + return $ filter (/= Version) opts + +-- Print help information if `Help' requested and terminate successfully. +-- +processHelp :: [Option] -> IO [Option] +processHelp opts = + do + name <- getProgName + let header = "\nUsage: " ++ name ++ " [ option... ]\n" + when (Help `elem` opts) $ do + putStrLn $ usageInfo header options + exitSuccess + return opts + +-- Process configuration options. +-- +processConfig :: Config -> [Option] -> IO Config +processConfig = foldM processOneOption + where + processOneOption config Debug = + return $ config {debugCfg = debugCfg config + 1} + processOneOption config (FeedDirOpt dir) = + return $ config {feedDirCfg = dir} + processOneOption config (HTMLDirOpt dir) = + return $ config {htmlDirCfg = dir} + processOneOption config (RSSDirOpt dir) = + return $ config {rssDirCfg = dir} + processOneOption config (OutputOpt dir) = + return $ config {htmlDirCfg = dir, rssDirCfg = dir} + processOneOption config Verbose = + return $ config {verboseCfg = True} + processOneOption config Quiet = + return $ config {quietCfg = True} + +-- Error during parsing command line options +-- +abort :: [String] -> IO a +abort = + abortWithIO . (++ ["Try the option `--help' on its own for more \ + \information."]) + hunk ./Date.hs 13 - Date(..), parseDate, + Date(..), addMinutes, parseDate, hunk ./Date.hs 28 - Month(..), ClockTime, CalendarTime(..), toUTCTime, toClockTime, - formatCalendarTime) + Month(..), ClockTime, TimeDiff(..), CalendarTime(..), noTimeDiff, + addToClockTime, toUTCTime, toClockTime, formatCalendarTime) hunk ./Date.hs 48 +-- Advance a date specification by the specified number of minutes. +-- +addMinutes :: Date -> Int -> Date +addMinutes (Date ct) min = + Date $ addToClockTime (noTimeDiff {tdMin = min}) ct + hunk ./Error.hs 13 - elementErrStr, abortWith + + -- * Error formatting + elementErrStr, + + -- * Program termination + abortWith, abortWithIO, exitSuccess + hunk ./Error.hs 22 +-- hierachical libraries +import System.Environment ( + getProgName) +import System.Exit ( + ExitCode(..), exitWith, exitFailure) + + hunk ./Error.hs 41 +-- Terminate with a set of errors (in IO code). +-- +abortWithIO :: [String] -> IO a +abortWithIO errs = + do + name <- getProgName + putStrLn $ name ++ ": FATAL ERROR" + putStr (unlines errs) + exitFailure hunk ./Error.hs 51 +-- Terminate sucessfully. +-- +exitSuccess :: IO a +exitSuccess = exitWith ExitSuccess hunk ./Feed.hs 16 - Enclosure(..), GUID(..), Source(..), + Enclosure(..), GUID(..), Source(..), Info(..), hunk ./Feed.hs 67 - itemsChan :: items + itemsChan :: items, + infoChan :: Info -- internal info about a channel hunk ./Feed.hs 135 +-- lambdaFeed specific information about a channel +-- +data Info = Info { + fnameInfo :: FilePath -- basename of .lfc + } + deriving Show + hunk ./Feed.hs 166 - itemsChan = error "channel missing items (after copy)" + itemsChan = error "channel missing items (after copy)", + infoChan = infoChan chan hunk ./Feed.hs 193 - itemsChan = error "channel missing items" + itemsChan = error "channel missing items", + infoChan = error "channel missing info" hunk ./LambdaFeed.hs 65 +-- * Synthetic channels: Channels that don't have an item directory, but +-- instead instructions on how to synthesise them from other channels by +-- eg, merging and filtering. +-- +-- * Don't regenerate a channel if neither its description nor items changed. +-- hunk ./LambdaFeed.hs 72 --- URLs, same for email addresses. +-- URLs, same for email addresses, but only warnings in case of errors. hunk ./LambdaFeed.hs 77 +-- * Use plugins to do the reading and writing of various formats and for +-- formatting the feed. Also for configuration as in Yi. +-- hunk ./LambdaFeed.hs 90 - when, foldM, liftM) + liftM) hunk ./LambdaFeed.hs 97 -import System.Console.GetOpt ( - ArgOrder(..), OptDescr(..), ArgDescr(..), getOpt, usageInfo) +import Numeric ( + showFFloat) + +import System.CPUTime ( + getCPUTime) hunk ./LambdaFeed.hs 104 -import System.Environment ( - getArgs, getProgName) -import System.Exit ( - ExitCode(..), exitWith, exitFailure) hunk ./LambdaFeed.hs 110 +import Config ( + version, + chanSuffix, itemSuffix, htmlSuffix, rssSuffix, + Config(..), processArgs) hunk ./LambdaFeed.hs 117 - elementErrStr, abortWith) + elementErrStr, abortWith, abortWithIO, exitSuccess) hunk ./LambdaFeed.hs 120 - Enclosure(..), GUID(..), Source(..), + Enclosure(..), GUID(..), Source(..), Info(..), hunk ./LambdaFeed.hs 123 +import HTML ( + channelToHTML) +import RSS ( + channelToRSS) hunk ./LambdaFeed.hs 132 --- Version information --- ------------------- - -name = "lambdaFeed" -versnum = "0.1.0" -versnick = "\"Lambdas for all!\"" -date = "26 Apr 2006" -version = name ++ ", version " ++ versnum ++ " " ++ versnick ++ ", " ++ date -copyright = "Copyright (c) 2006 Manuel M T Chakravarty" -disclaimer = "This software is distributed under the \ - \terms of the GNU Public Licence.\n\ - \NO WARRANTY WHATSOEVER IS PROVIDED. \ - \See ." - - --- Configuration --- ------------- - --- Constants --- -chanSuffix = "lfc" -itemSuffix = "lfi" - --- Configuration --- -data Config = Config { - debugCfg :: Int, - feedDirCfg :: FilePath, - verboseCfg :: Bool, - quietCfg :: Bool - } - --- Default configuration --- -dftConfig = Config { - debugCfg = 0, - feedDirCfg = "feed", - verboseCfg = False, - quietCfg = False - } - --- Available options --- -data Option = Debug - | FeedDirOpt FilePath - | Help - | Verbose - | Version - | Quiet - deriving Eq - --- Option description --- -options :: [OptDescr Option] -options = [ Option ['d'] ["debug"] (NoArg Debug) - "produces extra diagnostic output; repeat for more" - , Option ['f'] ["feed"] (ReqArg FeedDirOpt "DIR") - "feed directory with channel descriptions (default: feed/)" - , Option ['h', '?'] ["help"] (NoArg Help) - "this help message" - , Option ['v'] ["verbose"] (NoArg Verbose) - "print summary information (default: off)" - , Option ['V'] ["version"] (NoArg Version) - "version information" - , Option ['q'] ["quiet"] (NoArg Quiet) - "suppress all output, including parser warnings" - ] - --- Option processing --- -processOptions :: [String] -> IO Config -processOptions args = - case getOpt RequireOrder options args of - (opts, [] , [] ) -> processVersion opts >>= - processHelp >>= - processConfig dftConfig - (opts, args, [] ) -> abortWithOptionsErrors [unrecErr ++ unwords args] - (_ , _ , errs) -> abortWithOptionsErrors errs - where - unrecErr = "Unrecognised arguments: " - --- Print version information if `Help' or `Version' requested and remove any --- `Version' options. --- -processVersion :: [Option] -> IO [Option] -processVersion opts = - do - when (Version `elem` opts || Help `elem` opts) $ do - putStrLn version - putStrLn copyright - putStrLn disclaimer - return $ filter (/= Version) opts - --- Print help information if `Help' requested and terminate successfully. --- -processHelp :: [Option] -> IO [Option] -processHelp opts = - do - name <- getProgName - let header = "\nUsage: " ++ name ++ " [ option... ]\n" - when (Help `elem` opts) $ do - putStrLn $ usageInfo header options - exitWith ExitSuccess - return opts - --- Process configuration options. --- -processConfig :: Config -> [Option] -> IO Config -processConfig = foldM processOneOption - where - processOneOption config Debug = - return $ config {debugCfg = debugCfg config + 1} - processOneOption config (FeedDirOpt dir) = - return $ config {feedDirCfg = dir} - processOneOption config Verbose = - return $ config {verboseCfg = True} - processOneOption config Quiet = - return $ config {quietCfg = True} - --- Error during parsing command line options --- -abortWithOptionsErrors :: [String] -> IO a -abortWithOptionsErrors errs = - do - putStrLn (unlines errs) - putStrLn "Try the option `--help' on its own for more information." - exitFailure - - hunk ./LambdaFeed.hs 137 -data Summary = Summary +data Summary = Summary { + -- number of items for each cannel + channelItemsSummary :: [Int] + } hunk ./LambdaFeed.hs 176 + generatorChan = version, hunk ./LambdaFeed.hs 178 - itemsChan = dirname fname itemsChan chan + itemsChan = dirname fname itemsChan chan, hunk ./LambdaFeed.hs 180 + infoChan = Info { + fnameInfo = (stripSuffix . basename) fname + } hunk ./LambdaFeed.hs 217 --- Write feed data to RSS files. +-- Generate the requested feed representation. +-- +formatFeed :: Config -> Feed [Item] -> IO (Feed [Item]) +formatFeed config feed = + return feed + +-- Write feed data to HTML and RSS files. hunk ./LambdaFeed.hs 226 -writeFeed config feed = +writeFeed config (Feed chans) = hunk ./LambdaFeed.hs 228 - return $ undefined + mapM generateHTML chans + mapM generateRSS chans + return $ Summary {channelItemsSummary = map (length . itemsChan) chans} + where + generateHTML chan = + do + let fname = htmlDirCfg config (fnameInfo . infoChan) chan <.> + htmlSuffix + writeFile fname (channelToHTML config chan) + generateRSS chan = + do + let fname = rssDirCfg config (fnameInfo . infoChan) chan <.> + rssSuffix + writeFile fname (channelToRSS config chan) + hunk ./LambdaFeed.hs 249 - return () + pico <- getCPUTime + let noChannels = length (channelItemsSummary summary) + noItems = sum (channelItemsSummary summary) + secs = fromInteger (pico `div` 1000000000) / 1000 :: Float + sayV config $ "Processed " ++ show noItems ++ " item(s) in " ++ + show noChannels ++ " channel(s) in " ++ + showFFloat (Just 3) secs "s." hunk ./LambdaFeed.hs 599 +-- Remove the suffix from a filename. +-- +stripSuffix :: FilePath -> String +stripSuffix fname | null fname' = fname + | otherwise = reverse (tail fname') + where + fname' = dropWhile (/= '.') . reverse $ fname + +-- Drop the directory of a path. +-- +basename :: FilePath -> String +basename fname | null fname' = fname + | otherwise = fname' + where + fname' = reverse . takeWhile (/= '/') . reverse $ fname + hunk ./LambdaFeed.hs 630 +-- Join a suffix to a file name. +-- +(<.>) :: FilePath -> String -> FilePath +fname <.> "" = fname +fname <.> suffix = fname ++ "." ++ suffix + hunk ./LambdaFeed.hs 665 - args <- getArgs - config <- processOptions args - (readFeed config >>= writeFeed config >>= printSummary config) - `catch` \e -> abort (show e) - exitWith ExitSuccess - where - abort err = - do - name <- getProgName - putStrLn $ name ++ ": " ++ err + config <- processArgs + ( readFeed config + >>= formatFeed config + >>= writeFeed config + >>= printSummary config) + `catch` \e -> abortWithIO [show e] + exitSuccess }