{-# LANGUAGE ScopedTypeVariables #-} {- This file is part of the vimeta package. It is subject to the license terms in the LICENSE file found in the top-level directory of this distribution and at git://pmade.com/vimeta/LICENSE. No part of the vimeta package, including this file, may be copied, modified, propagated, or distributed except according to the terms contained in the LICENSE file. -} -------------------------------------------------------------------------------- -- | Mapping files can be used to map file names to other information. module Vimeta.Core.MappingFile ( Parser , parseMappingFile ) where -------------------------------------------------------------------------------- -- Library imports: import Control.Applicative hiding ((<|>)) import Control.Monad.Identity import Data.Char (isSpace) import Data.Either import Data.List import Data.Text (Text) import qualified Data.Text.IO as Text import System.Directory (doesFileExist) import System.FilePath (takeExtension) import Text.Parsec -------------------------------------------------------------------------------- -- Local imports: import Vimeta.Core.Vimeta -------------------------------------------------------------------------------- -- The following is a kludge to avoid the "redundant import" warning -- when using GHC >= 7.10.x. This should be removed after we decide -- to stop supporting GHC < 7.10.x. import Prelude -------------------------------------------------------------------------------- -- | Parser type. type Parser a = ParsecT Text () Identity a -------------------------------------------------------------------------------- -- | Internal token used for parsing. data Token a = Comment | Entry FilePath a -------------------------------------------------------------------------------- -- | Parse a mapping file. parseMappingFile :: (MonadIO m) => FilePath -- ^ File name for the mapping file. -> Parser a -- ^ Parser for the second column. -> Vimeta m [(FilePath, a)] parseMappingFile filename p = do contents <- runIO $ Text.readFile filename case runIdentity $ runParserT (mapping p) () filename contents of Left e -> die (show e) Right m -> checkFileMappingOrDie m -------------------------------------------------------------------------------- checkFileMappingOrDie :: (MonadIO m) => [(FilePath, a)] -> Vimeta m [(FilePath, a)] checkFileMappingOrDie xs = do ys <- checkFileMapping xs if null (lefts ys) then return (rights ys) else die $ report (lefts ys) where report :: [(FilePath, a)] -> String report fs = "the following files are listed in the mapping file " ++ "but they don't exist: \n" ++ intercalate "\n" (map fst fs) -------------------------------------------------------------------------------- -- | Checks to see that all of the file names mentioned exist. If a -- file doesn't exist the @m4v@ file extension is added to it and the -- existence checking happens again. checkFileMapping :: (MonadIO m) => [(FilePath, a)] -- ^ The mapping. -> Vimeta m [Either (FilePath, a) (FilePath, a)] checkFileMapping = mapM checkFile where checkFile :: (MonadIO m) => (FilePath, a) -> Vimeta m (Either (FilePath, a) (FilePath, a)) checkFile f@(filename, a) = do let ext = takeExtension filename exists <- runIO (doesFileExist filename) case exists of False | null ext -> checkFile (filename ++ ".m4v", a) | otherwise -> return $ Left f True -> return $ Right f -------------------------------------------------------------------------------- -- | The actual file parser. mapping :: Parser a -> Parser [(FilePath, a)] mapping p = entries <$> manyTill (whitespace <|> comment <|> fileName p) eof where entries :: [Token a] -> [(FilePath, a)] entries = concatMap extract . filter predicate predicate :: Token a -> Bool predicate (Entry _ _) = True predicate Comment = False extract :: Token a -> [(FilePath, a)] extract (Entry f a) = [(f, a)] extract Comment = [] -------------------------------------------------------------------------------- -- | Parse a file name followed by whatever the second column parser -- extracts. fileName :: Parser a -> Parser (Token a) fileName p = do first <- anyChar others <- manyTill anyChar (lookAhead space) a <- spaceWithoutNewline >> p return $ Entry (first:others) a "filename and mapping" -------------------------------------------------------------------------------- -- | Skip whitespace. whitespace :: Parser (Token a) whitespace = skipMany1 space >> return Comment -------------------------------------------------------------------------------- -- | Like whitespace, but doesn't span multiple lines. spaceWithoutNewline :: Parser () spaceWithoutNewline = skipMany1 $ satisfy (\c -> isSpace c && c /= '\n') -------------------------------------------------------------------------------- -- | Skip comments. comment :: Parser (Token a) comment = (char '#' >> manyTill anyChar newline >> return Comment) "comment"