-- compile with: ghc -Wall -O2 --make footnotes.hs module Main (main, doFiles, putFootnotes) where import System.Environment (getArgs) import System.Console.GetOpt import Control.Exception (Exception, catchJust) import Control.Monad (foldM, guard) import Data.Array.IArray (Array, array, (!)) import qualified Data.ByteString.Lazy.Char8 as B import qualified Data.IntMap as M import Data.List (isSuffixOf) import Data.Maybe (mapMaybe) data Flag = Appearance deriving (Show, Eq) options :: [OptDescr Flag] options = [Option ['a'] ["reorder-by-appearance"] (NoArg Appearance) "Reorder footnotes by order of appearence"] -- main: process the command-line options for footnotes. main :: IO () main = do args <- getArgs case getOpt Permute options args of (opts, files, []) -> doFiles files $ Appearance `elem` opts (_, _, errors) -> ioError $ userError $ concat errors ++ usageInfo header options where header = "Usage: footnotes [OPTION...] [files...]" -- doFiles: read input either from given files or from stdin, -- if none were given, and print the output to stdout. doFiles :: [FilePath] -> Bool -> IO () doFiles [] useBody = B.getContents >>= putFootnotes useBody doFiles files useBody = mapM_ doFile files where doFile file = B.readFile file >>= putFootnotes useBody type RefsMap = M.IntMap Int -- maps old to new refs. data RefsMapState = RMS !RefsMap Int -- keeps track of map and counter. type FootsMap = Array Int (IO ()) -- sorted footnotes. sepLine :: B.ByteString -- the seperator line. sepLine = B.pack "@footnote:" emptyRMS :: RefsMapState -- an empty RefsMap and its counter. emptyRMS = RMS M.empty 0 -- putFootnotes: print s with renumbered footnotes. putFootnotes :: Bool -> B.ByteString -> IO () putFootnotes True s = do -- by order of appearance in body. let (body, _ : foots) = break (== sepLine) $ B.lines s (RMS m n) <- foldM (putLineSubRefs putNewRef) emptyRMS body B.putStrLn sepLine let fm = array (1, n) $ mapMaybe (subRef m) foots mapM_ (lookupPutFoot fm) [1..n] putFootnotes False s = do -- by order of the footnotes. let (_ : foots) = dropWhile (/= sepLine) $ B.lines s let st@(RMS m _) = foldl insertFootRef emptyRMS foots let (body, _ : foots') = break (== sepLine) $ B.lines s mapM_ (putLineSubRefs putUnknownRef st) body B.putStrLn sepLine mapM_ (\l -> maybe (B.putStrLn l) snd $ subRef m l) foots' -- putLineSubRefs: print body line l with substituted references, -- evaluate doUnknownRef for references not in m, return new state. putLineSubRefs :: (Int -> RefsMapState -> IO RefsMapState) -> RefsMapState -> B.ByteString -> IO RefsMapState putLineSubRefs doUnknownRef st@(RMS m _) l = case B.elemIndex '[' l of Nothing -> B.putStrLn l >> return st Just i -> do let (before, after) = B.splitAt (i + 1) l B.putStr before let parse = do (n, rest) <- B.readInt after (']', _) <- B.uncons rest return (n, rest) case parse of Nothing -> putLineSubRefs doUnknownRef st after Just (n, rest) -> case M.lookup n m of Just newN -> do putStr $ show newN putLineSubRefs doUnknownRef st rest Nothing -> do newSt <- doUnknownRef n st putLineSubRefs doUnknownRef newSt rest -- insertRef: return new RefsMapState with oldN inserted. insertRef :: RefsMapState -> Int -> RefsMapState insertRef (RMS m n) oldN = RMS (M.insert oldN n' m) n' where n' = n+1 -- subRef: substitute the reference in footnote line l according to m, -- return its number and an IO action for printing the new l. subRef :: RefsMap -> B.ByteString -> Maybe (Int, IO ()) subRef m l = do (oldN, rest) <- parseFoot l n <- M.lookup oldN m return (n, putStr ('[' : show n) >> B.putStrLn rest) -- parseFoot: return number and the rest after it from footnote line l. parseFoot :: B.ByteString -> Maybe (Int, B.ByteString) parseFoot l = do ('[', l') <- B.uncons l (n, rest) <- B.readInt l' (']', _) <- B.uncons rest return (n, rest) -- putNewRef :: print new ref for oldN and return new RefsMapState. putNewRef :: Int -> RefsMapState -> IO RefsMapState putNewRef oldN st = putStr (show n) >> return newSt where newSt@(RMS _ n) = insertRef st oldN -- lookupPutFoot: return the IO action for footnote number n in fm. lookupPutFoot :: FootsMap -> Int -> IO () lookupPutFoot fm n = catchJust undefElements (fm ! n) $ \_ -> do putStr ('[' : show n) putStrLn "] ### missing footnote ###" -- undefElements: exception predicate for undefined array elements. undefElements :: Exception -> Maybe () undefElements e = guard $ "undefined array element" `isSuffixOf` show e -- insertFootRef: insert the ref from footnote line l into st. insertFootRef :: RefsMapState -> B.ByteString -> RefsMapState insertFootRef st l = maybe st (insertRef st . fst) $ parseFoot l -- putUnknownRef: print dummy for a reference without a footnote. putUnknownRef :: Int -> RefsMapState -> IO RefsMapState putUnknownRef _ st = putStr "?" >> return st