module Toml.Parser
( ParseException (..)
, parse
, arrayP
, boolP
, doubleP
, integerP
, keyP
, keyValP
, textP
, tableHeaderP
, tomlP
) where
import Control.Applicative (Alternative (..))
import Control.Applicative.Combinators (between, manyTill, sepEndBy, skipMany)
import Control.Monad (void)
import Data.Char (digitToInt)
import Data.List (foldl')
import Data.Semigroup ((<>))
import Data.Text (Text)
import Data.Void (Void)
import Text.Megaparsec (Parsec, parseErrorPretty', try)
import Text.Megaparsec.Char (alphaNumChar, anyChar, char, oneOf, space1)
import Toml.PrefixTree (Key (..), Piece (..), fromList)
import Toml.Type (AnyValue, TOML (..), UValue (..), typeCheck)
import qualified Control.Applicative.Combinators.NonEmpty as NC
import qualified Data.HashMap.Lazy as HashMap
import qualified Data.Text as Text
import qualified Text.Megaparsec as Mega (parse)
import qualified Text.Megaparsec.Char.Lexer as L
type Parser = Parsec Void Text
sc :: Parser ()
sc = L.space space1 lineComment blockComment
where
lineComment = L.skipLineComment "#"
blockComment = empty
lexeme :: Parser a -> Parser a
lexeme = L.lexeme sc
text :: Text -> Parser Text
text = L.symbol sc
text_ :: Text -> Parser ()
text_ = void . text
doubleP :: Parser Double
doubleP = L.signed sc $ lexeme L.float
bareKeyP :: Parser Text
bareKeyP = lexeme $ Text.pack <$> bareStrP
where
bareStrP :: Parser String
bareStrP = some $ alphaNumChar <|> char '_' <|> char '-'
literalStringP :: Parser Text
literalStringP = lexeme $ Text.pack <$> (char '\'' *> anyChar `manyTill` char '\'')
basicStringP :: Parser Text
basicStringP = lexeme $ Text.pack <$> (char '"' *> anyChar `manyTill` char '"')
textP :: Parser Text
textP = literalStringP <|> basicStringP
quote :: Text -> Text -> Text
quote q t = q <> t <> q
keyComponentP :: Parser Piece
keyComponentP = Piece <$> (bareKeyP <|> (quote "\"" <$> basicStringP) <|> (quote "'" <$> literalStringP))
keyP :: Parser Key
keyP = Key <$> NC.sepBy1 keyComponentP (char '.')
tableNameP :: Parser Key
tableNameP = lexeme $ between (char '[') (char ']') keyP
integerP :: Parser Integer
integerP = lexeme $ binary <|> octal <|> hexadecimal <|> decimal
where
decimal = L.signed sc L.decimal
binary = try (char '0' >> char 'b') >> mkNum 2 <$> (some binDigitChar)
octal = try (char '0' >> char 'o') >> L.octal
hexadecimal = try (char '0' >> char 'x') >> L.hexadecimal
binDigitChar = oneOf ['0', '1']
mkNum b = foldl' (step b) 0
step b a c = a * b + fromIntegral (digitToInt c)
boolP :: Parser Bool
boolP = False <$ text "false"
<|> True <$ text "true"
arrayP :: Parser [UValue]
arrayP = lexeme $ between (char '[' *> sc) (char ']') elements
where
elements :: Parser [UValue]
elements = valueP `sepEndBy` spComma <* skipMany (text ",")
spComma :: Parser ()
spComma = char ',' *> sc
valueP :: Parser UValue
valueP = UBool <$> boolP
<|> UDouble <$> try doubleP
<|> UInteger <$> integerP
<|> UText <$> textP
<|> UArray <$> arrayP
keyValP :: Parser (Key, AnyValue)
keyValP = do
k <- keyP
text_ "="
uval <- valueP
case typeCheck uval of
Left err -> fail $ show err
Right v -> pure (k, v)
tableHeaderP :: Parser (Key, TOML)
tableHeaderP = do
k <- tableNameP
toml <- makeToml <$> many keyValP
pure (k, toml)
where
makeToml :: [(Key, AnyValue)] -> TOML
makeToml kv = TOML (HashMap.fromList kv) mempty
tomlP :: Parser TOML
tomlP = do
sc
kvs <- many keyValP
tables <- many tableHeaderP
pure TOML { tomlPairs = HashMap.fromList kvs
, tomlTables = fromList tables
}
newtype ParseException = ParseException Text
deriving (Show, Eq)
parse :: Text -> Either ParseException TOML
parse t = case Mega.parse tomlP "" t of
Left err -> Left $ ParseException $ Text.pack $ parseErrorPretty' t err
Right toml -> Right toml