dojang-0.1.0: src/Dojang/Syntax/FilePathExpression/Parser.hs
{-# LANGUAGE ImportQualifiedPost #-}
{-# LANGUAGE OverloadedStrings #-}
module Dojang.Syntax.FilePathExpression.Parser
( ParseErrorBundle
, bareComponent
, environmentVariable
, errorBundlePretty
, filePathExpression
, filePathExpression'
, parseFilePathExpression
, parse
, root
, substitution
, substitutionConditional
, substitutionDefault
, substitutionInBraces
, substitutionModifierAfterColon
) where
import Dojang.Types.FilePathExpression
( EnvironmentVariable
, FilePathExpression (..)
)
import Control.Applicative (Alternative ((<|>)), optional)
import Control.Monad (void)
import Data.Char (isAlpha, isAlphaNum, isAscii, isControl)
import Data.Text (Text, cons, pack)
import Data.Void (Void)
import Text.Megaparsec
( MonadParsec (eof, takeWhile1P, takeWhileP)
, Parsec
, label
, many
, oneOf
, parse
, satisfy
, some
, try
, (<?>)
)
import Text.Megaparsec.Char (char, string)
import Text.Megaparsec.Error (ParseErrorBundle)
import Text.Megaparsec.Error qualified (errorBundlePretty)
type Parser = Parsec Void Text
-- | Parse a 'FilePathExpression' from a 'Text'.
parseFilePathExpression
:: FilePath
-- ^ The name of source file.
-> Text
-- ^ The input text.
-> Either (ParseErrorBundle Text Void) FilePathExpression
-- ^ The result of parsing.
parseFilePathExpression = parse $ filePathExpression <* eof
-- | A parser for 'FilePathExpression'.
filePathExpression :: Parser FilePathExpression
filePathExpression = filePathExpression' True []
{-# INLINE filePathExpression #-}
filePathExpression' :: Bool -> [Char] -> Parser FilePathExpression
filePathExpression' begin disallowedBareChars = do
root' <- if begin then optional $ try root else return Nothing
head' <- case root' of
Nothing -> do
firstComponent <- component begin disallowedBareChars
components <- many $ component False disallowedBareChars
return $ foldl Concatenation firstComponent components
Just r -> do
components <- many $ component False disallowedBareChars
return $ case components of
[] -> r
c : cs -> PathSeparator r $ foldl Concatenation c cs
betweenSeparators <- many $ try $ do
void $ oneOf ['/', '\\']
components' <- some $ component False disallowedBareChars
return $ foldl1 Concatenation components'
let topExpr = foldl PathSeparator head' betweenSeparators
void $ optional $ oneOf ['/', '\\']
return topExpr
{-# INLINE filePathExpression' #-}
component :: Bool -> [Char] -> Parser FilePathExpression
component begin disallowedBareChars =
bareComponent disallowedBareChars
<|> substitutionInBraces begin
<|> substitution
{-# INLINE component #-}
bareComponent :: [Char] -> Parser FilePathExpression
bareComponent disallowedChars = do
component' <- takeWhile1P (Just "component") isValidChar
return $ BareComponent component'
where
isValidChar :: Char -> Bool
isValidChar = not . isInvalidChar
isInvalidChar :: Char -> Bool
isInvalidChar c =
isControl c
|| c
== '/'
|| c
== '\\'
|| c
== '$'
|| c
== '\r'
|| c
== '\n'
|| c
`elem` disallowedChars
{-# INLINE bareComponent #-}
root :: Parser FilePathExpression
root =
driveLetter <|> rootSlash
where
driveLetter :: Parser FilePathExpression
driveLetter = do
letter <- satisfy (`elem` ['A' .. 'Z']) <?> "drive letter"
void $ char ':'
void $ satisfy (`elem` ['/', '\\'])
return $ Root $ Just letter
rootSlash :: Parser FilePathExpression
rootSlash = char '/' >> return (Root Nothing)
environmentVariable :: Parser EnvironmentVariable
environmentVariable = label "environment variable" $ do
first <- satisfy (\c -> isAscii c && isAlpha c || c == '_')
rest <- takeWhileP Nothing (\c -> isAscii c && isAlphaNum c || c == '_')
return $ cons first rest
{-# INLINE environmentVariable #-}
substitution :: Parser FilePathExpression
substitution =
char '$' >> Substitution <$> environmentVariable
{-# INLINE substitution #-}
substitutionInBraces :: Bool -> Parser FilePathExpression
substitutionInBraces begin = do
void $ string "${"
envVar <- environmentVariable
colon <- optional $ char ':'
expr <- case colon of
Nothing -> return $ Substitution envVar
Just _ -> substitutionModifierAfterColon begin envVar
void $ char '}'
return expr
{-# INLINE substitutionInBraces #-}
substitutionModifierAfterColon
:: Bool
-> EnvironmentVariable
-> Parser FilePathExpression
substitutionModifierAfterColon begin envVar =
substitutionDefault begin envVar <|> substitutionConditional begin envVar
{-# INLINE substitutionModifierAfterColon #-}
substitutionDefault :: Bool -> EnvironmentVariable -> Parser FilePathExpression
substitutionDefault begin envVar = do
void $ char '-'
SubstitutionWithDefault envVar <$> filePathExpression' begin "}"
{-# INLINE substitutionDefault #-}
substitutionConditional
:: Bool -> EnvironmentVariable -> Parser FilePathExpression
substitutionConditional begin envVar = do
void $ char '+'
ConditionalSubstitution envVar <$> filePathExpression' begin "}"
{-# INLINE substitutionConditional #-}
-- | Basically the same as 'Text.Megaparsec.Error.errorBundlePretty' but
-- returns a 'Text' instead of a 'String'.
errorBundlePretty :: ParseErrorBundle Text Void -> Text
errorBundlePretty = pack . Text.Megaparsec.Error.errorBundlePretty