godot-megaparsec-0.2.2.0: src/Godot/Parser/Resource.hs
{-|
Module : Godot.Parser.Resource
Description : Megaparsec parser for the Godot resource file format.
Copyright : (c) Winston Hartnett, 2021
License : GPL-3
Maintainer : whartnett@gmail.com
Stability : experimental
Portability : POSIX
A parser for Godot resource file formats. Currently only supports auto-generated
`tscn` and `gdns` files.
-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Godot.Parser.Resource
(GodotValue(..)
,GodotSection(..)
,TscnDescriptor(..)
,TscnParsed(..)
,OtherDescriptor(..)
,OtherParsed(..)
,GdnsDescriptor(..)
,GdnsParsed(..)
,GodotParsed(..)
,ExtResource(..)
,SubResource(..)
,Node(..)
,Connection(..)
,Resource(..)
,tscnParser
,gdnsParser
,otherParser
,godotParser) where
import Control.Applicative ((<|>),liftA2)
import Control.Monad (unless)
import Data.Char (isAlphaNum,isDigit,isUpper)
import Data.Either (fromRight)
import Data.Functor (($>))
import qualified Data.HashMap.Lazy as M
import qualified Data.HashSet as S
import Data.Maybe (fromJust)
import qualified Data.Text as T
import qualified Data.Text.Read as T
import Data.Void
import GHC.Generics (Generic)
import Prelude hiding (exponent)
import qualified Text.Megaparsec as P
import qualified Text.Megaparsec.Char as P
import qualified Text.Megaparsec.Char.Lexer as P (decimal,signed)
type Parser = P.Parsec Void T.Text
optionalSign :: Parser T.Text
optionalSign = P.string "-" <|> P.string "+"
godotFloatP :: Parser Float
godotFloatP = do
sign <- P.option "" optionalSign
let takeDigits = P.takeWhile1P Nothing isDigit
rational <- takeDigits <> P.string "." <> takeDigits
exponent <- P.option "" (P.string "e" <> P.option "" optionalSign <> takeDigits)
pure . fst . fromRight undefined . T.rational $ sign <> rational <> exponent
godotIntP :: Parser Int
godotIntP = P.signed P.space P.decimal
godotBoolP :: Parser Bool
godotBoolP = (P.string "true" $> True) <|> (P.string "false" $> False)
stringP :: Parser T.Text
stringP = P.char '"' *> P.takeWhileP Nothing (/= '"') <* P.char '"'
godotStringP :: Parser T.Text
godotStringP = stringP
godotArrP :: Parser [GodotValue]
godotArrP = do
P.char '['
P.space
P.manyTill (do
gVal <- godotValueP
P.char ','
P.space
pure gVal) (P.char ']')
godotDictP :: Parser (M.HashMap T.Text GodotValue)
godotDictP = do
P.char '{'
P.space
let kvParser = liftA2 (,) stringP (P.char ':' *> P.hspace *> godotValueP)
kvs <- kvParser `P.sepBy` (P.char ',' *> P.newline *> P.hspace)
P.space
P.char '}'
pure . M.fromList $ kvs
godotConstructorP :: Parser (T.Text, [GodotValue])
godotConstructorP = do
let isGodotIdent c = isAlphaNum c || c == '@'
constructorName
<- P.takeWhile1P Nothing isGodotIdent -- TODO Causes problems w/ other delimiters
P.char '('
P.space
constructorArgs <- godotValueP `P.sepBy` (P.char ',' *> P.hspace)
P.space
P.char ')'
pure (constructorName, constructorArgs)
godotNullP :: Parser GodotValue
godotNullP = P.string "null" $> GodotNull
godotValueP :: Parser GodotValue
godotValueP = do
nc <- T.head . P.stateInput <$> P.getParserState
case nc of
'"' -> GodotString <$> godotStringP
'[' -> GodotArr <$> godotArrP
'{' -> GodotDict <$> godotDictP
't' -> GodotBool <$> godotBoolP
'f' -> GodotBool <$> godotBoolP
'n' -> godotNullP
l
| isUpper l || l == '@' -> GodotConstructor <$> godotConstructorP
_ -> P.try (GodotFloat <$> godotFloatP) <|> P.try (GodotInt <$> godotIntP)
-- | Values parsed from a Tscn file.
--
-- Constructors are `(constructor name, constructor args)`.
data GodotValue
= GodotConstructor (T.Text, [GodotValue])
| GodotInt Int
| GodotFloat Float
| GodotBool Bool
| GodotString T.Text
| GodotDict (M.HashMap T.Text GodotValue)
| GodotArr [GodotValue]
| GodotNull
deriving (Show,Generic,Eq)
-- There aren't any lenses to unwrap sum types AFAIK :/
-- Surely there's a better way to do this.
unGodotConstructor k = fmap (\(GodotConstructor (n, a)) -> (n, a)) . M.lookup k
unGodotConstructor' k = fromJust . unGodotConstructor k
unGodotInt k = fmap (\(GodotInt i) -> i) . M.lookup k
unGodotInt' k = fromJust . unGodotInt k
unGodotFloat k = fmap (\(GodotFloat i) -> i) . M.lookup k
unGodotFloat' k = fromJust . unGodotInt k
unGodotBool k = fmap (\(GodotBool i) -> i) . M.lookup k
unGodotBool' k = fromJust . unGodotBool k
unGodotString k = fmap (\(GodotString i) -> i) . M.lookup k
unGodotString' k = fromJust . unGodotString k
unGodotDict k = fmap (\(GodotDict i) -> i) . M.lookup k
unGodotDict' k = fromJust . unGodotDict k
unGodotArr k = fmap (\(GodotArr i) -> i) . M.lookup k
unGodotArr' k = fromJust . unGodotArr k
collectRest its = M.filterWithKey (\k _ -> k `S.member` S.fromList its)
data ExtResource =
ExtResource
{ _extResourcePath :: T.Text
, _extResourceTy :: T.Text
, _extResourceId :: Int
-- | Other header information.
, _extResourceHeaders :: M.HashMap T.Text GodotValue
-- | Body of the configuration entry.
, _extResourceEntries :: M.HashMap T.Text GodotValue
}
deriving (Show, Generic)
data SubResource =
SubResource
{ _subResourceTy :: T.Text
, _subResourceId :: Int
-- | Other header information.
, _subResourceHeaders :: M.HashMap T.Text GodotValue
-- | Body of the configuration entry.
, _subResourceEntries :: M.HashMap T.Text GodotValue
}
deriving (Show, Generic)
data Node =
Node
{ _nodeTy :: Maybe T.Text
, _nodeName :: T.Text
-- | If `Nothing`, then this node is the root.
, _nodeParent :: Maybe T.Text
-- | Instance refers to an `ExtResource` ID, usually listed at the top of a file.
, _nodeInst :: Maybe Int
, _nodeInstPlaceholder :: Maybe T.Text
, _nodeOwner :: Maybe T.Text
, _nodeIndex :: Maybe Int
, _nodeGroups :: Maybe [T.Text]
-- | Other header information.
, _nodeHeaders :: M.HashMap T.Text GodotValue
-- | Body of the configuration entry.
, _nodeEntries :: M.HashMap T.Text GodotValue
}
deriving (Show, Generic)
data Connection =
Connection
{ _connectionSignal :: T.Text
, _connectionFrom :: T.Text
, _connectionTo :: T.Text
, _connectionMethod :: T.Text
-- | Other header information.
, _connectionHeaders :: M.HashMap T.Text GodotValue
-- | Body of the configuration entry.
, _connectionEntries :: M.HashMap T.Text GodotValue
}
deriving (Show, Generic)
data Resource =
Resource
{ _resourceResourceName :: Maybe T.Text
, _resourceClassName :: Maybe T.Text
, _resourceLibrary :: Maybe (T.Text, [GodotValue])
}
deriving (Show, Generic)
-- | Godot resource section prefixed with a bracket-enclosed header, optionally
-- with body entries.
--
-- Header entries not specified in a record are accessed with the relevant `headers` field.
-- Likewise, body entries not specified are accessed with the `entries` field.
-- Note that explicitly specified section fields are not duplicated in `headers` and
-- `entries` fields.
data GodotSection
= ExtResourceSection ExtResource
| SubResourceSection SubResource
| NodeSection Node
| ConnectionSection Connection
| ResourceSection Resource
| OtherSection
{ _otherSectionHeader :: T.Text
, _otherSectionHeaders :: M.HashMap T.Text GodotValue
, _otherSectionEntries :: M.HashMap T.Text GodotValue
}
deriving (Show,Generic)
-- | `tscn` file descriptor.
data TscnDescriptor =
TscnDescriptor
{ _tscnDescriptorLoadSteps :: Int
, _tscnDescriptorFormat :: Int
}
deriving (Show,Generic)
-- | Parsed `tscn` file.
data TscnParsed =
TscnParsed
{ _tscnParsedDescriptor :: TscnDescriptor
, _tscnParsedSections :: [GodotSection]
}
deriving (Show,Generic)
-- | `gdns` file descriptor.
data GdnsDescriptor =
GdnsDescriptor
{ _gdnsDescriptorTy :: T.Text
, _gdnsDescriptorLoadSteps :: Int
, _gdnsDescriptorFormat :: Int
}
deriving (Show,Generic)
-- | Parsed `gdns` file.
data GdnsParsed =
GdnsParsed
{ _gdnsParsedDescriptor :: GdnsDescriptor
, _gdnsParsedSections :: [GodotSection]
}
deriving (Show,Generic)
-- | An unknown file descriptor.
data OtherDescriptor =
OtherDescriptor
{ _otherDescriptorHeaderName :: T.Text
, _otherDescriptorHeaders :: M.HashMap T.Text GodotValue
}
deriving (Show,Generic)
-- | An unknown file parsing result.
data OtherParsed =
OtherParsed
{ _otherParsedDescriptor :: OtherDescriptor
, _otherParsedSections :: [GodotSection]
}
deriving (Show,Generic)
-- | Parsed godot resource file.
data GodotParsed
= Tscn TscnParsed
| Gdns GdnsParsed
| Other OtherParsed
deriving (Show,Generic)
tscnHeaderKVP :: Parser (T.Text, GodotValue)
tscnHeaderKVP = liftA2 (,) (P.takeWhileP Nothing (/= '=')) (P.char '=' *> godotValueP)
headerKvs :: Parser (M.HashMap T.Text GodotValue)
headerKvs = M.fromList <$> tscnHeaderKVP `P.sepBy` P.char ' '
bodyAndKvs :: Parser (T.Text, M.HashMap T.Text GodotValue, M.HashMap T.Text GodotValue)
bodyAndKvs = do
headerName <- P.char '[' *> P.takeWhile1P Nothing (/= ' ') <* P.char ' '
headerKvs' <- headerKvs
P.char ']'
P.space
let tscnBodyP = do
let parseKV =
liftA2 (,) (P.takeWhileP Nothing (/= ' '))
(P.string " = " *> godotValueP <* P.newline)
M.fromList <$> P.manyTill parseKV (P.try (P.newline $> ()) <|> P.try P.eof)
emptyBodyP = pure M.empty
body <- P.try tscnBodyP <|> P.try emptyBodyP
(headerName, headerKvs', body) <$ P.space
-- | Parse a section header nam, header key-values, and body key-values using a provided
-- conversion function.
headerWrapper
:: T.Text
-> (M.HashMap T.Text GodotValue -> M.HashMap T.Text GodotValue -> a)
-> Parser a
headerWrapper targetSect p = do
(headerName, headerKvs', bodyKvs) <- bodyAndKvs
unless (headerName == targetSect) (fail "mismatched expected header")
pure $ p headerKvs' bodyKvs
-- | Parse a `[sub_resource]` section.
tscnSubResourceP :: Parser SubResource
tscnSubResourceP =
headerWrapper "sub_resource"
(\kvs bodyKvs -> SubResource (unGodotString' "type" kvs) (unGodotInt' "id" kvs)
(collectRest ["type", "id"] kvs) bodyKvs)
-- | Parse an `[ext_resource]` section.
tscnExtResourceP :: Parser ExtResource
tscnExtResourceP =
headerWrapper "ext_resource"
(\kvs bodyKvs -> ExtResource (unGodotString' "path" kvs)
(unGodotString' "type" kvs) (unGodotInt' "id" kvs)
(collectRest ["path", "type", "id"] kvs) bodyKvs)
-- | Parse a `[node]` section.
tscnNodeP :: Parser Node
tscnNodeP =
headerWrapper "node"
(\kvs bodyKvs -> Node (unGodotString "type" kvs) (unGodotString' "name" kvs)
(unGodotString "parent" kvs)
((\(GodotInt i) -> i) . head . snd <$> unGodotConstructor "instance" kvs)
(unGodotString "instance_placeholder" kvs) (unGodotString "owner" kvs)
(unGodotInt "index" kvs) (map (\(GodotString i) -> i) <$> unGodotArr "groups" kvs)
(collectRest
[ "path"
, "type"
, "parent"
, "name"
, "instance"
, "instance_placeholder"
, "owner"
, "index"
, "groups"] kvs) bodyKvs)
-- | Parse a `[connection]` section.
tscnConnectionP :: Parser Connection
tscnConnectionP =
headerWrapper "connection"
(\kvs bodyKvs -> Connection (unGodotString' "signal" kvs)
(unGodotString' "from" kvs) (unGodotString' "to" kvs) (unGodotString' "method" kvs)
(collectRest ["signal", "from", "to", "method"] kvs) bodyKvs)
-- | Parse an unspecified section.
otherP :: Parser GodotSection
otherP = do
(headerName, headerKvs', bodyKvs) <- bodyAndKvs
pure $ OtherSection headerName headerKvs' bodyKvs
-- | Parse a `tscn` file.
tscnParser :: Parser TscnParsed
tscnParser = do
kvs <- P.string "[gd_scene " *> headerKvs <* P.char ']' <* P.space
let loadSteps = unGodotInt' "load_steps" kvs
format = unGodotInt' "format" kvs
sectionP =
P.choice
(map P.try
[ConnectionSection <$> tscnConnectionP, ExtResourceSection <$> tscnExtResourceP, SubResourceSection <$> tscnSubResourceP, NodeSection <$> tscnNodeP, otherP])
sections <- P.manyTill sectionP P.eof
pure $ TscnParsed (TscnDescriptor loadSteps format) sections
-- | Parse a `[resource]` section.
resourceP :: Parser Resource
resourceP =
headerWrapper "resource"
(\_ bodyKvs -> Resource (unGodotString "resource_name" bodyKvs)
(unGodotString "class_name" bodyKvs) (unGodotConstructor "library" bodyKvs))
-- | Parse a `gdns` file.
gdnsParser :: Parser GdnsParsed
gdnsParser = do
kvs <- P.string "[gd_resource " *> headerKvs <* P.char ']' <* P.space
let ty = unGodotString' "type" kvs
loadSteps = unGodotInt' "load_steps" kvs
format = unGodotInt' "format" kvs
sectionP = P.choice (map P.try [ExtResourceSection <$> tscnExtResourceP, ResourceSection <$> resourceP, otherP])
sections <- P.manyTill sectionP P.eof
pure $ GdnsParsed (GdnsDescriptor ty loadSteps format) sections
-- | Parse an unknown resource file.
otherParser :: Parser OtherParsed
otherParser = do
hName <- P.char '[' *> P.takeWhile1P Nothing (/= ' ') <* P.char ' '
hKvs <- headerKvs <* P.char ']' <* P.space
sections <- P.manyTill otherP P.eof
pure $ OtherParsed (OtherDescriptor hName hKvs) sections
-- | Parse some Godot resource file.
godotParser :: Parser GodotParsed
godotParser =
P.choice (map P.try [Tscn <$> tscnParser, Gdns <$> gdnsParser, Other <$> otherParser])