toml-reader-0.1.0.0: src/TOML/Parser.hs
{-# LANGUAGE CPP #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RecordWildCards #-}
{-# LANGUAGE TypeFamilies #-}
{- |
Parse a TOML document.
References:
* https://toml.io/en/v1.0.0
* https://github.com/toml-lang/toml/blob/1.0.0/toml.abnf
-}
module TOML.Parser (
parseTOML,
) where
import Control.Monad (guard, unless, void, when)
import Control.Monad.Combinators.NonEmpty (sepBy1)
import Data.Bifunctor (bimap)
import Data.Char (chr, isDigit, isSpace, ord)
import Data.Fixed (Fixed (..))
import Data.Foldable (foldl', foldlM)
import Data.Functor (($>))
import Data.List.NonEmpty (NonEmpty)
import qualified Data.List.NonEmpty as NonEmpty
import Data.Map.Strict (Map)
import qualified Data.Map.Strict as Map
import Data.Maybe (fromMaybe)
import Data.Text (Text)
import qualified Data.Text as Text
import Data.Time (Day, LocalTime, TimeOfDay, TimeZone)
import qualified Data.Time as Time
import Data.Void (Void)
import qualified Numeric
import Text.Megaparsec hiding (sepBy1)
import Text.Megaparsec.Char hiding (space, space1)
import qualified Text.Megaparsec.Char.Lexer as L
import TOML.Error (NormalizeError (..), TOMLError (..))
import TOML.Utils.Map (getPathLens)
import TOML.Value (Table, Value (..))
parseTOML ::
-- | Name of file (for error messages)
String ->
-- | Input
Text ->
Either TOMLError Value
parseTOML filename input =
case runParser parseTOMLDocument filename input of
Left e -> Left $ ParseError $ Text.pack $ errorBundlePretty e
Right result -> Table <$> normalize result
-- 'Value' generalized to allow for unnormalized + annotated Values.
data GenericValue map key tableMeta arrayMeta
= GenericTable tableMeta (map key (GenericValue map key tableMeta arrayMeta))
| GenericArray arrayMeta [GenericValue map key tableMeta arrayMeta]
| GenericString Text
| GenericInteger Integer
| GenericFloat Double
| GenericBoolean Bool
| GenericOffsetDateTime (LocalTime, TimeZone)
| GenericLocalDateTime LocalTime
| GenericLocalDate Day
| GenericLocalTime TimeOfDay
fromGenericValue ::
(map key (GenericValue map key tableMeta arrayMeta) -> Table) ->
GenericValue map key tableMeta arrayMeta ->
Value
fromGenericValue fromGenericTable = \case
GenericTable _ t -> Table $ fromGenericTable t
GenericArray _ vs -> Array $ map (fromGenericValue fromGenericTable) vs
GenericString x -> String x
GenericInteger x -> Integer x
GenericFloat x -> Float x
GenericBoolean x -> Boolean x
GenericOffsetDateTime x -> OffsetDateTime x
GenericLocalDateTime x -> LocalDateTime x
GenericLocalDate x -> LocalDate x
GenericLocalTime x -> LocalTime x
{--- Parse raw document ---}
type Parser = Parsec Void Text
-- | An unannotated, unnormalized value.
type RawValue = GenericValue LookupMap Key () ()
type Key = NonEmpty Text
type RawTable = LookupMap Key RawValue
newtype LookupMap k v = LookupMap {unLookupMap :: [(k, v)]}
data TOMLDoc = TOMLDoc
{ rootTable :: RawTable
, subTables :: [TableSection]
}
data TableSection = TableSection
{ tableSectionHeader :: TableSectionHeader
, tableSectionTable :: RawTable
}
data TableSectionHeader = SectionTable Key | SectionTableArray Key
parseTOMLDocument :: Parser TOMLDoc
parseTOMLDocument = do
emptyLines
rootTable <- parseRawTable
emptyLines
subTables <- many parseTableSection
emptyLines
eof
return TOMLDoc{..}
parseRawTable :: Parser RawTable
parseRawTable = fmap LookupMap $ many $ parseKeyValue <* endOfLine <* emptyLines
parseTableSection :: Parser TableSection
parseTableSection = do
tableSectionHeader <-
choice
[ SectionTableArray <$> parseHeader "[[" "]]"
, SectionTable <$> parseHeader "[" "]"
]
endOfLine
emptyLines
tableSectionTable <- parseRawTable
emptyLines
return TableSection{..}
where
parseHeader brackStart brackEnd = hsymbol brackStart *> parseKey <* hsymbol brackEnd
parseKeyValue :: Parser (Key, RawValue)
parseKeyValue = do
key <- parseKey
hsymbol "="
value <- parseValue
pure (key, value)
parseKey :: Parser Key
parseKey =
(`sepBy1` try (hsymbol ".")) . choice $
[ parseBasicString
, parseLiteralString
, parseUnquotedKey
]
where
parseUnquotedKey =
takeWhile1P
(Just "[A-Za-z0-9_-]")
(`elem` ['A' .. 'Z'] ++ ['a' .. 'z'] ++ ['0' .. '9'] ++ "-_")
parseValue :: Parser RawValue
parseValue =
choice
[ try $ GenericTable () <$> label "table" parseInlineTable
, try $ GenericArray () <$> label "array" parseInlineArray
, try $ GenericString <$> label "string" parseString
, try $ GenericOffsetDateTime <$> label "offset-datetime" parseOffsetDateTime
, try $ GenericLocalDateTime <$> label "local-datetime" parseLocalDateTime
, try $ GenericLocalDate <$> label "local-date" parseLocalDate
, try $ GenericLocalTime <$> label "local-time" parseLocalTime
, try $ GenericFloat <$> label "float" parseFloat
, try $ GenericInteger <$> label "integer" parseInteger
, try $ GenericBoolean <$> label "boolean" parseBoolean
]
parseInlineTable :: Parser RawTable
parseInlineTable = do
hsymbol "{"
kvs <- parseKeyValue `sepBy` try (hsymbol ",")
hsymbol "}"
return $ LookupMap kvs
parseInlineArray :: Parser [RawValue]
parseInlineArray = do
_ <- char '[' <* emptyLines
vs <- (parseValue <* emptyLines) `sepEndBy` (char ',' <* emptyLines)
_ <- char ']'
return vs
parseString :: Parser Text
parseString =
choice
[ try parseMultilineBasicString
, try parseMultilineLiteralString
, try parseBasicString
, parseLiteralString
]
-- | A string in double quotes.
parseBasicString :: Parser Text
parseBasicString =
label "double-quoted string" $
between (char '"') (char '"') $
fmap Text.pack . many . choice $
[ satisfy isBasicChar
, parseEscaped
]
-- | A string in single quotes.
parseLiteralString :: Parser Text
parseLiteralString =
label "single-quoted string" $
between (char '\'') (char '\'') $
takeWhileP (Just "literal-char") isLiteralChar
-- | A multiline string with three double quotes.
parseMultilineBasicString :: Parser Text
parseMultilineBasicString =
label "double-quoted multiline string" $ do
_ <- string "\"\"\"" *> optional eol
lineContinuation
Text.concat <$> manyTill (mlBasicContent <* lineContinuation) (exactly 3 '"')
where
mlBasicContent =
choice
[ Text.singleton <$> try parseEscaped
, Text.singleton <$> satisfy isBasicChar
, parseMultilineDelimiter '"'
, eol
]
lineContinuation = many (try $ char '\\' *> hspace *> eol *> space) *> pure ()
-- | A multiline string with three single quotes.
parseMultilineLiteralString :: Parser Text
parseMultilineLiteralString =
label "single-quoted multiline string" $ do
_ <- string "'''" *> optional eol
Text.concat <$> manyTill mlLiteralContent (exactly 3 '\'')
where
mlLiteralContent =
choice
[ Text.singleton <$> satisfy isLiteralChar
, parseMultilineDelimiter '\''
, eol
]
parseEscaped :: Parser Char
parseEscaped = char '\\' *> parseEscapedChar
where
parseEscapedChar =
choice
[ char '"'
, char '\\'
, char 'b' $> '\b'
, char 'f' $> '\f'
, char 'n' $> '\n'
, char 'r' $> '\r'
, char 't' $> '\t'
, char 'u' *> unicodeHex 4
, char 'U' *> unicodeHex 8
]
unicodeHex n = do
code <- readHex . Text.pack <$> count n hexDigitChar
guard $ isUnicodeScalar code
pure $ chr code
{- |
Parse the multiline delimiter (" in """ quotes, or ' in ''' quotes), unless
the delimiter indicates the end of the multiline string.
i.e. parse 1 or 2 delimiters, or 4 or 5, which is 1 or 2 delimiters at the
end of a multiline string (then backtrack 3 to mark the end).
-}
parseMultilineDelimiter :: Char -> Parser Text
parseMultilineDelimiter delim =
choice
[ exactly 1 delim
, exactly 2 delim
, do
_ <- lookAhead (exactly 4 delim)
Text.pack <$> count 1 (char delim)
, do
_ <- lookAhead (exactly 5 delim)
Text.pack <$> count 2 (char delim)
]
isBasicChar :: Char -> Bool
isBasicChar c =
case c of
' ' -> True
'\t' -> True
_ | 0x21 <= code && code <= 0x7E -> c /= '"' && c /= '\\'
_ | isNonAscii c -> True
_ -> False
where
code = ord c
isLiteralChar :: Char -> Bool
isLiteralChar c =
case c of
' ' -> True
'\t' -> True
_ | 0x21 <= code && code <= 0x7E -> c /= '\''
_ | isNonAscii c -> True
_ -> False
where
code = ord c
parseOffsetDateTime :: Parser (LocalTime, TimeZone)
parseOffsetDateTime = (,) <$> parseLocalDateTime <*> parseTimezone
where
parseTimezone =
choice
[ char' 'Z' $> Time.utc
, do
applySign <- parseSign
h <- parseHours
_ <- char ':'
m <- parseMinutes
return $ Time.minutesToTimeZone $ applySign $ h * 60 + m
]
parseLocalDateTime :: Parser LocalTime
parseLocalDateTime = do
d <- parseLocalDate
_ <- char' 'T' <|> char ' '
t <- parseLocalTime
return $ Time.LocalTime d t
parseLocalDate :: Parser Day
parseLocalDate = do
y <- parseDecDigits 4
_ <- char '-'
m <- parseDecDigits 2
_ <- char '-'
d <- parseDecDigits 2
maybe empty return $ Time.fromGregorianValid y m d
parseLocalTime :: Parser TimeOfDay
parseLocalTime = do
h <- parseHours
_ <- char ':'
m <- parseMinutes
_ <- char ':'
sInt <- parseSeconds
sFracRaw <- optional $ fmap Text.pack $ char '.' >> some digitChar
let sFrac = MkFixed $ maybe 0 readPicoDigits sFracRaw
return $ Time.TimeOfDay h m (fromIntegral sInt + sFrac)
where
readPicoDigits s = readDec $ Text.take 12 (s <> Text.replicate 12 "0")
parseHours :: Parser Int
parseHours = do
h <- parseDecDigits 2
guard $ 0 <= h && h < 24
return h
parseMinutes :: Parser Int
parseMinutes = do
m <- parseDecDigits 2
guard $ 0 <= m && m < 60
return m
parseSeconds :: Parser Int
parseSeconds = do
s <- parseDecDigits 2
guard $ 0 <= s && s <= 60 -- include 60 for leap seconds
return s
parseFloat :: Parser Double
parseFloat = do
applySign <- parseSign
num <-
choice
[ try normalFloat
, try $ string "inf" $> inf
, try $ string "nan" $> nan
]
pure $ applySign num
where
normalFloat = do
intPart <- parseDecIntRaw
(fracPart, expPart) <-
choice
[ try $ (,) <$> pure "" <*> parseExp
, (,) <$> parseFrac <*> optionalOr "" parseExp
]
pure $ readFloat $ intPart <> fracPart <> expPart
parseExp =
fmap Text.concat . sequence $
[ string' "e"
, parseSignRaw
, parseNumRaw digitChar digitChar
]
parseFrac =
fmap Text.concat . sequence $
[ string "."
, parseNumRaw digitChar digitChar
]
inf = read "Infinity"
nan = read "NaN"
parseInteger :: Parser Integer
parseInteger =
choice
[ try parseBinInt
, try parseOctInt
, try parseHexInt
, parseSignedDecInt
]
where
parseSignedDecInt = do
applySign <- parseSign
num <- readDec <$> parseDecIntRaw
pure $ applySign num
parseHexInt =
parsePrefixedInt readHex "0x" hexDigitChar
parseOctInt =
parsePrefixedInt readOct "0o" octDigitChar
parseBinInt =
parsePrefixedInt readBin "0b" binDigitChar
parsePrefixedInt readInt prefix parseDigit = do
_ <- string prefix
readInt <$> parseNumRaw parseDigit parseDigit
parseBoolean :: Parser Bool
parseBoolean =
choice
[ True <$ string "true"
, False <$ string "false"
]
{--- Normalize into Value ---}
-- | An annotated, normalized Value
type AnnValue = GenericValue Map Text TableMeta ArrayMeta
type AnnTable = Map Text AnnValue
unannotateTable :: AnnTable -> Table
unannotateTable = fmap unannotateValue
unannotateValue :: AnnValue -> Value
unannotateValue = fromGenericValue unannotateTable
data TableType
= -- | An inline table, e.g. "a.b" in:
--
-- @
-- a.b = { c = 1 }
-- @
InlineTable
| -- | A table created implicitly from a nested key, e.g. "a" in:
--
-- @
-- a.b = 1
-- @
ImplicitKey
| -- | An explicitly named section, e.g. "a.b.c" and "a.b" but not "a" in:
--
-- @
-- [a.b.c]
-- [a.b]
-- @
ExplicitSection
| -- | An implicitly created section, e.g. "a" in:
--
-- @
-- [a.b]
-- @
--
-- Can later be converted into an explicit section
ImplicitSection
deriving (Eq)
data TableMeta = TableMeta
{ tableType :: TableType
}
data ArrayMeta = ArrayMeta
{ isStaticArray :: Bool
}
newtype NormalizeM a = NormalizeM
{ runNormalizeM :: Either NormalizeError a
}
instance Functor NormalizeM where
fmap f = NormalizeM . fmap f . runNormalizeM
instance Applicative NormalizeM where
pure = NormalizeM . pure
NormalizeM f <*> NormalizeM x = NormalizeM (f <*> x)
instance Monad NormalizeM where
m >>= f = NormalizeM $ runNormalizeM . f =<< runNormalizeM m
normalizeError :: NormalizeError -> NormalizeM a
normalizeError = NormalizeM . Left
normalize :: TOMLDoc -> Either TOMLError Table
normalize = bimap NormalizeError unannotateTable . runNormalizeM . normalize'
normalize' :: TOMLDoc -> NormalizeM AnnTable
normalize' TOMLDoc{..} = do
root <- flattenTable rootTable
foldlM mergeTableSection root subTables
where
mergeTableSection :: AnnTable -> TableSection -> NormalizeM AnnTable
mergeTableSection baseTable TableSection{..} = do
case tableSectionHeader of
SectionTable key ->
mergeTableSectionTable key tableSectionTable baseTable
SectionTableArray key ->
mergeTableSectionArray key tableSectionTable baseTable
mergeTableSectionTable :: Key -> RawTable -> AnnTable -> NormalizeM AnnTable
mergeTableSectionTable sectionKey table baseTable =
setValueAtPath valueAtPathOptions sectionKey baseTable $ \mVal -> do
tableToExtend <-
case mVal of
-- if a value doesn't already exist, initialize an empty Map
Nothing -> pure Map.empty
-- if a Table already exists at the path ...
Just existingValue@(GenericTable meta existingTable) ->
case tableType meta of
-- ... and is an inline table, error
InlineTable -> duplicateKeyError existingValue
-- ... and was created as a nested key elsewhere, error
ImplicitKey -> extendTableError
-- ... and was created as a Table section explicitly defined elsewhere, error
ExplicitSection -> duplicateSectionError
-- ... otherwise, return the existing table
_ -> pure existingTable
-- if some other Value already exists at the path, error
Just existingValue -> duplicateKeyError existingValue
mergedTable <-
mergeRawTable
MergeOptions{recurseImplicitSections = False}
tableToExtend
table
let newTableMeta = TableMeta{tableType = ExplicitSection}
pure $ GenericTable newTableMeta mergedTable
where
valueAtPathOptions =
ValueAtPathOptions
{ shouldRecurse = \case
InlineTable -> False
ImplicitKey -> False
ExplicitSection -> True
ImplicitSection -> True
, implicitType = ImplicitSection
, makeMidPathNotTableError = nonTableInNestedKeyError sectionKey table
}
duplicateKeyError existingValue =
normalizeError
DuplicateKeyError
{ _path = sectionKey
, _existingValue = unannotateValue existingValue
, _valueToSet = Table $ rawTableToApproxTable table
}
extendTableError =
normalizeError
ExtendTableError
{ _path = sectionKey
, _originalKey = sectionKey
}
duplicateSectionError =
normalizeError
DuplicateSectionError
{ _sectionKey = sectionKey
}
mergeTableSectionArray :: Key -> RawTable -> AnnTable -> NormalizeM AnnTable
mergeTableSectionArray sectionKey table baseTable = do
setValueAtPath valueAtPathOptions sectionKey baseTable $ \mVal -> do
(meta, currArray) <-
case mVal of
-- if nothing exists, initialize an empty array
Nothing -> do
let meta = ArrayMeta{isStaticArray = False}
pure (meta, [])
-- if an array exists, insert table to the end of the array
Just (GenericArray meta existingArray)
| not (isStaticArray meta) ->
pure (meta, existingArray)
-- otherwise, error
Just existingValue ->
normalizeError
ImplicitArrayForDefinedKeyError
{ _path = sectionKey
, _existingValue = unannotateValue existingValue
, _tableSection = rawTableToApproxTable table
}
let newTableMeta = TableMeta{tableType = ExplicitSection}
newTable <- GenericTable newTableMeta <$> flattenTable table
pure $ GenericArray meta $ currArray <> [newTable]
where
valueAtPathOptions =
ValueAtPathOptions
{ shouldRecurse = \case
InlineTable -> False
ImplicitKey -> False
ExplicitSection -> True
ImplicitSection -> True
, implicitType = ImplicitSection
, makeMidPathNotTableError = \history existingValue ->
NonTableInNestedImplicitArrayError
{ _path = history
, _existingValue = unannotateValue existingValue
, _sectionKey = sectionKey
, _tableSection = rawTableToApproxTable table
}
}
flattenTable :: RawTable -> NormalizeM AnnTable
flattenTable =
mergeRawTable
MergeOptions{recurseImplicitSections = True}
Map.empty
data MergeOptions = MergeOptions
{ recurseImplicitSections :: Bool
}
mergeRawTable :: MergeOptions -> AnnTable -> RawTable -> NormalizeM AnnTable
mergeRawTable MergeOptions{..} baseTable table = foldlM insertRawValue baseTable (unLookupMap table)
where
insertRawValue accTable (key, rawValue) = do
let valueAtPathOptions =
ValueAtPathOptions
{ shouldRecurse = \case
InlineTable -> False
ImplicitKey -> True
ExplicitSection -> True
ImplicitSection -> recurseImplicitSections
, implicitType = ImplicitKey
, makeMidPathNotTableError = nonTableInNestedKeyError key table
}
setValueAtPath valueAtPathOptions key accTable $ \case
Nothing -> fromRawValue rawValue
Just existingValue ->
normalizeError
DuplicateKeyError
{ _path = key
, _existingValue = unannotateValue existingValue
, _valueToSet = rawValueToApproxValue rawValue
}
fromRawValue = \case
GenericTable _ rawTable -> do
let meta = TableMeta{tableType = InlineTable}
GenericTable meta <$> flattenTable rawTable
GenericArray _ rawValues -> do
let meta = ArrayMeta{isStaticArray = True}
GenericArray meta <$> mapM fromRawValue rawValues
GenericString x -> pure (GenericString x)
GenericInteger x -> pure (GenericInteger x)
GenericFloat x -> pure (GenericFloat x)
GenericBoolean x -> pure (GenericBoolean x)
GenericOffsetDateTime x -> pure (GenericOffsetDateTime x)
GenericLocalDateTime x -> pure (GenericLocalDateTime x)
GenericLocalDate x -> pure (GenericLocalDate x)
GenericLocalTime x -> pure (GenericLocalTime x)
data ValueAtPathOptions = ValueAtPathOptions
{ shouldRecurse :: TableType -> Bool
, implicitType :: TableType
, makeMidPathNotTableError :: Key -> AnnValue -> NormalizeError
}
-- | Implementation for makeMidPathNotTableError for NonTableInNestedKeyError
nonTableInNestedKeyError :: Key -> RawTable -> (Key -> AnnValue -> NormalizeError)
nonTableInNestedKeyError key table = \history existingValue ->
NonTableInNestedKeyError
{ _path = history
, _existingValue = unannotateValue existingValue
, _originalKey = key
, _originalValue = Table $ rawTableToApproxTable table
}
setValueAtPath ::
ValueAtPathOptions ->
Key ->
AnnTable ->
(Maybe AnnValue -> NormalizeM AnnValue) ->
NormalizeM AnnTable
setValueAtPath ValueAtPathOptions{..} fullKey initialTable f = do
(mValue, setValue) <- getPathLens doRecurse fullKey initialTable
setValue <$> f mValue
where
doRecurse history = \case
-- If nothing exists, recurse into a new empty Map
Nothing -> do
let newTableMeta = TableMeta{tableType = implicitType}
pure (Map.empty, GenericTable newTableMeta)
-- If a Table exists, recurse into it
Just (GenericTable meta subTable) -> do
unless (shouldRecurse $ tableType meta) $
normalizeError
ExtendTableError
{ _path = history
, _originalKey = fullKey
}
pure (subTable, GenericTable meta)
-- If an Array exists, recurse into the last Table, per spec:
-- Any reference to an array of tables points to the
-- most recently defined table element of the array.
Just (GenericArray aMeta vs)
| Just vs' <- NonEmpty.nonEmpty vs
, GenericTable tMeta subTable <- NonEmpty.last vs' -> do
when (isStaticArray aMeta) $
normalizeError $
ExtendTableInInlineArrayError history fullKey
pure (subTable, GenericArray aMeta . snoc (NonEmpty.init vs') . GenericTable tMeta)
-- If something else exists, throw error with makeMidPathNotTableError
Just v -> normalizeError $ makeMidPathNotTableError history v
snoc xs x = xs <> [x]
-- | Convert a RawTable into a Table, for use in errors + debugging.
rawTableToApproxTable :: RawTable -> Table
rawTableToApproxTable =
Map.fromList
. map (\(k, v) -> (Text.intercalate "." $ NonEmpty.toList k, rawValueToApproxValue v))
. unLookupMap
-- | Convert a RawValue into a Value, for use in errors + debugging.
rawValueToApproxValue :: RawValue -> Value
rawValueToApproxValue = fromGenericValue rawTableToApproxTable
{--- Parser Helpers ---}
-- | https://github.com/toml-lang/toml/blob/1.0.0/toml.abnf#L38
isNonAscii :: Char -> Bool
isNonAscii c = (0x80 <= code && code <= 0xD7FF) || (0xE000 <= code && code <= 0x10FFFF)
where
code = ord c
-- | https://unicode.org/glossary/#unicode_scalar_value
isUnicodeScalar :: Int -> Bool
isUnicodeScalar code = (0x0 <= code && code <= 0xD7FF) || (0xE000 <= code && code <= 0x10FFFF)
-- | Returns "", "-", or "+"
parseSignRaw :: Parser Text
parseSignRaw = optionalOr "" (string "-" <|> string "+")
parseSign :: Num a => Parser (a -> a)
parseSign = do
sign <- parseSignRaw
pure $ if sign == "-" then negate else id
parseDecIntRaw :: Parser Text
parseDecIntRaw =
choice
[ try $ parseNumRaw (satisfy $ \c -> isDigit c && c /= '0') digitChar
, Text.singleton <$> digitChar
]
parseDecDigits :: (Show a, Num a, Eq a) => Int -> Parser a
parseDecDigits n = readDec . Text.pack <$> count n digitChar
parseNumRaw :: Parser Char -> Parser Char -> Parser Text
parseNumRaw parseLeadingDigit parseDigit = do
leading <- parseLeadingDigit
rest <- many $ optional (char '_') *> parseDigit
pure $ Text.pack $ leading : rest
{--- Parser Utilities ---}
hsymbol :: Text -> Parser ()
hsymbol s = hspace >> string s >> hspace >> pure ()
-- | Parse trailing whitespace/trailing comments + newline
endOfLine :: Parser ()
endOfLine = L.space hspace1 skipComments empty >> (void eol <|> eof) >> pure ()
-- | Parse spaces, newlines, and comments
emptyLines :: Parser ()
emptyLines = L.space space1 skipComments empty
skipComments :: Parser ()
skipComments = do
_ <- string "#"
void . many $ do
c <- satisfy (/= '\n')
let code = ord c
case c of
'\r' -> void $ lookAhead (char '\n')
_
| (0x00 <= code && code <= 0x08) || (0x0A <= code && code <= 0x1F) || code == 0x7F ->
fail $ "Comment has invalid character: \\" <> show code
_ -> pure ()
space, space1 :: Parser ()
space = void $ many parseSpace
space1 = void $ some parseSpace
-- | TOML does not support bare '\r' without '\n'.
parseSpace :: Parser ()
parseSpace = void (satisfy (\c -> isSpace c && c /= '\r')) <|> void (string "\r\n")
#if !MIN_VERSION_megaparsec(9,0,0)
hspace :: Parser ()
hspace = void $ takeWhileP (Just "white space") isHSpace
hspace1 :: Parser ()
hspace1 = void $ takeWhile1P (Just "white space") isHSpace
isHSpace :: Char -> Bool
isHSpace x = isSpace x && x /= '\n' && x /= '\r'
#endif
optionalOr :: a -> Parser a -> Parser a
optionalOr def = fmap (fromMaybe def) . optional
exactly :: Int -> Char -> Parser Text
exactly n c = try $ Text.pack <$> count n (char c) <* notFollowedBy (char c)
{--- Read Helpers ---}
-- | Assumes string satisfies @all isDigit@.
readFloat :: (Show a, RealFrac a) => Text -> a
readFloat = runReader Numeric.readFloat
-- | Assumes string satisfies @all isDigit@.
readDec :: (Show a, Num a, Eq a) => Text -> a
readDec = runReader Numeric.readDec
-- | Assumes string satisfies @all isHexDigit@.
readHex :: (Show a, Num a, Eq a) => Text -> a
readHex = runReader Numeric.readHex
-- | Assumes string satisfies @all isOctDigit@.
readOct :: (Show a, Num a, Eq a) => Text -> a
readOct = runReader Numeric.readOct
-- | Assumes string satisfies @all (`elem` "01")@.
readBin :: (Show a, Num a) => Text -> a
readBin = foldl' go 0 . Text.unpack
where
go acc x =
let digit
| x == '0' = 0
| x == '1' = 1
| otherwise = error $ "readBin got unexpected digit: " <> show x
in 2 * acc + digit
runReader :: Show a => ReadS a -> Text -> a
runReader rdr digits =
case rdr $ Text.unpack digits of
[(x, "")] -> x
result -> error $ "Unexpectedly unable to parse " <> show digits <> ": " <> show result