{-# LANGUAGE NoImplicitPrelude, GeneralizedNewtypeDeriving, OverloadedStrings #-}
module Yarn.Lock.Parse
( PackageFields(..), Package
, Parser
, packageList
, packageEntry
, field, nestedField, simpleField
, packageKeys
) where
import Protolude hiding (try, some, many)
import qualified Data.Char as Ch
import qualified Data.List.NonEmpty as NE
import qualified Data.Text as T
import qualified Data.Map.Strict as M
import Control.Monad (fail)
import Text.Megaparsec as MP
import qualified Text.Megaparsec.Char as MP
import qualified Text.Megaparsec.Char.Lexer as MPL
import qualified Yarn.Lock.Types as YLT
type Parser = Parsec Void Text
newtype PackageFields = PackageFields (Map Text (Either Text PackageFields))
deriving (Show, Eq, Semigroup, Monoid)
type Package = YLT.Keyed (SourcePos, PackageFields)
packageList :: Parser [Package]
packageList = MP.many $ (skipMany (comment <|> MP.string "\n")) *> packageEntry
where
comment :: Parser (Tokens Text)
comment = MP.char '#' *> takeWhileP Nothing (/= '\n')
packageEntry :: Parser (YLT.Keyed (SourcePos, PackageFields))
packageEntry = label "package entry" $ do
pos <- getSourcePos
(keys, pkgs) <- nonIndented
$ indentedFieldsWithHeader packageKeys
pure $ YLT.Keyed keys (pos, pkgs)
packageKeys :: Parser (NE.NonEmpty YLT.PackageKey)
packageKeys = label "package keys" $ do
firstEls <- many (try $ lexeme $ packageKey ":," <* MP.char ',')
lastEl <- packageKey ":" <* MP.char ':'
pure $ NE.fromList $ firstEls <> [lastEl]
packageKey :: [Char] -> Parser YLT.PackageKey
packageKey separators = inString (pkgKey "\"")
<|> pkgKey separators
<?> "package key"
where
pkgKey :: [Char] -> Parser YLT.PackageKey
pkgKey valueChars = label "package key" $ do
key <- someTextOf (MP.noneOf valueChars)
case breakDrop '@' key of
("", rest) -> case breakDrop '@' rest of
("", _) -> emptyKeyErr key
(scopedName, ver) -> YLT.PackageKey
<$> scoped (T.cons '@' scopedName) <*> pure ver
(name, ver) -> pure $ YLT.PackageKey (YLT.SimplePackageKey name) ver
emptyKeyErr :: Text -> Parser a
emptyKeyErr key = fail
("packagekey: package name can not be empty (is: "
<> toS key <> ")")
breakDrop :: Char -> Text -> (Text, Text)
breakDrop c str = case T.breakOn (T.singleton c) str of
(s, "") -> (s, "")
(s, s') -> (s, T.drop 1 s')
scoped n = maybe
(fail $ "packageKey: scoped variable must be of form @scope/package"
<> " (is: " <> toS n <> ")")
pure $ YLT.parsePackageKeyName n
field :: Parser (Text, Either Text PackageFields)
field = try nested <|> simple <?> "field"
where
simple = fmap Left <$> simpleField
nested = fmap Right <$> nestedField
simpleField :: Parser (Text, Text)
simpleField = (,) <$> lexeme (strSymbolChars <|> symbolChars)
<*> (strValueChars <|> valueChars)
<?> "simple field"
where
valueChars, strValueChars :: Parser Text
valueChars = someTextOf (MP.noneOf ("\n\r\"" :: [Char]))
strSymbolChars = inString $ symbolChars
strValueChars = inString $ valueChars
<|> (pure T.empty <?> "an empty value field")
nestedField :: Parser (Text, PackageFields)
nestedField = label "nested field" $
indentedFieldsWithHeader (symbolChars <* MP.char ':')
indentedFieldsWithHeader :: Parser a -> Parser (a, PackageFields)
indentedFieldsWithHeader header = indentBlock $ do
hdr <- header
pure $ MPL.IndentSome Nothing
(\fields -> pure (hdr, toPfs fields)) field
where
toPfs :: [(Text, Either Text PackageFields)] -> PackageFields
toPfs = PackageFields . M.fromList
symbolChars :: Parser Text
symbolChars = label "key symbol" $ someTextOf $ MP.satisfy
(\c -> Ch.isAscii c &&
(Ch.isLower c || Ch.isUpper c || Ch.isNumber c || c `elem` special))
where special = "-_.@/" :: [Char]
someTextOf :: Parser Char -> Parser Text
someTextOf c = T.pack <$> some c
inString :: Parser a -> Parser a
inString = between (MP.char '"') (MP.char '"')
space :: Parser ()
space = MPL.space (void MP.spaceChar)
(MPL.skipLineComment "# ")
(void $ MP.satisfy (const False))
lexeme :: Parser a -> Parser a
lexeme = MPL.lexeme space
nonIndented :: Parser a -> Parser a
nonIndented = MPL.nonIndented space
indentBlock :: Parser (MPL.IndentOpt Parser a b)
-> Parser a
indentBlock = MPL.indentBlock space