packages feed

godot-megaparsec-0.2.5.0: src/Godot/Parser/Resource.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE ViewPatterns #-}

{- |
Module      : Godot.Parser.Resource
Description : Parser for Godot resource files.
Copyright   : (c) Winston Hartnett, 2022
License     : MIT
Maintainer  : winstonhartnett@gmail.com
Stability   : experimental
Portability : POSIX

Godot's resource files are automatically generated by the editor. This parser
only supports automatically generated resource files for

  - Nodes with 'nodeP'
  - GDExtension configs with 'gdExtensionP'
  - Other resource files with 'otherP'
-}
module Godot.Parser.Resource (
  Parser,
  KvAnn,
  GdValue (..),

  -- * Value parsers
  valP,
  floatP,
  intP,
  boolP,
  stringP,
  arrP,
  dictP,
  cstrP,
  nullP,

  -- * Resource file parsers
  headerP,
  bodyP,
  bodyHeaderP,

  -- ** Resource file formats
  ExtResource (..),
  extResourceP,
  SubResource (..),
  subResourceP,
  Node (..),
  nodeP,
  Connection (..),
  connectionP,
  Resource (..),
  resourceP,
  GdSection (..),
  otherP,
  TscnParsed (..),
  tscnP,
  GdExtensionParsed (..),
  gdExtensionP,
  UnknownParsed (..),
  unknownP,

  -- * General resource file parsing
  GdParsed (..),
  parsedP,
) where

import Control.Applicative (asum, (<|>))
import Control.Lens hiding (from, to)
import Control.Monad.State.Strict (MonadState (get), MonadTrans (lift), StateT (runStateT), modify')
import Data.Char (isAlphaNum, isDigit, isUpper)
import Data.Either (fromRight)
import Data.Functor (($>))
import Data.Generics.Labels ()
import Data.Generics.Sum
import qualified Data.HashMap.Strict as HM
import qualified Data.HashSet as HS
import Data.Maybe (fromJust)
import qualified Data.Text as T
import qualified Data.Text.Read as T
import Data.Void (Void)
import GHC.Generics (Generic)

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

-- | Wrapped Godot resource format values.
data GdValue
  = GdCstr (T.Text, [GdValue])
  | GdInt Int
  | GdFloat Double
  | GdBool Bool
  | GdString T.Text
  | GdDict (HM.HashMap T.Text GdValue)
  | GdArr [GdValue]
  | GdNull
  deriving (Show, Generic)

infixl 3 <||>
(<||>) :: Parser a -> Parser a -> Parser a
a <||> b = P.try a <|> P.try b

--------------------------------------------------------------------------------

-- | Parse any resource value.
valP :: Parser GdValue
valP = asum $ map (\(t, p) -> (P.try $ P.lookAhead (P.satisfy t) *> p)) ms
 where
  ms =
    [ ((== '"'), GdString <$> stringP)
    , ((== '['), GdArr <$> arrP)
    , ((== '{'), GdDict <$> dictP)
    , (\c -> c == 't' || c == 'f', GdBool <$> boolP)
    , ((== 'n'), nullP)
    , (\c -> isUpper c || c == '@', GdCstr <$> cstrP)
    , (const True, P.try (GdFloat <$> floatP) <|> P.try (GdInt <$> intP))
    ]

-- | Parse resource float.
floatP :: Parser Double
floatP = do
  sign <- opt optSign
  rational <- takeDigits <> P.string "." <> takeDigits
  exponent' <- opt $ P.string "e" <> opt optSign <> takeDigits
  pure
    . fst
    . fromRight undefined
    . T.rational
    $ sign <> rational <> exponent'
 where
  optSign = P.string "-" <|> P.string "+"
  opt = P.option ""
  takeDigits = P.takeWhile1P Nothing isDigit

-- | Parser resource int.
intP :: Parser Int
intP = P.signed P.space P.decimal

-- | Parse resource bool.
boolP :: Parser Bool
boolP = (P.string "true" $> True) <|> (P.string "false" $> False)

-- Parse resource string.
stringP :: Parser T.Text
stringP = qt *> P.takeWhileP Nothing (/= '"') <* qt
 where
  qt = P.char '"'

-- | Parse array of resource values.
arrP :: Parser [GdValue]
arrP = do
  P.char '[' *> P.space
  flip P.manyTill (P.char ']') $
    valP <* P.char ',' <* P.space

-- | Parse dictionary of resource values.
dictP :: Parser (HM.HashMap T.Text GdValue)
dictP = P.between (P.char '{' *> P.space) (P.space *> P.char '}') kvs
 where
  kvs = fmap HM.fromList $ kvP `P.sepBy` (P.char ',' *> P.newline *> P.hspace)
  kvP = (,) <$> stringP <*> (P.char ':' *> P.hspace *> valP)

-- | Parse resource constructor.
cstrP :: Parser (T.Text, [GdValue])
cstrP = do
  cstrName <- cstrNameP
  cstrArgs <-
    P.between
      (P.char '(' *> P.space)
      (P.space *> P.char ')')
      cstrArgsP
  pure (cstrName, cstrArgs)
 where
  isGdIdent c = isAlphaNum c || c == '@'
  cstrNameP = P.takeWhile1P Nothing isGdIdent
  cstrArgsP = valP `P.sepBy` (P.char ',' *> P.hspace)

-- | Parse null.
nullP :: Parser GdValue
nullP = P.string "null" $> GdNull

--------------------------------------------------------------------------------

-- | A generic mapping from either a header or body key to a 'GdValue'.
type KvAnn = HM.HashMap T.Text GdValue

-- | Parse resource header keys-values.
headerKvsP :: Parser KvAnn
headerKvsP = HM.fromList <$> kvP `P.sepBy` P.char ' '
 where
  kvP = (,) <$> P.takeWhileP Nothing (/= '=') <*> (P.char '=' *> valP)

-- | Parse a resource header.
headerP :: Parser (T.Text, KvAnn)
headerP = do
  name' <- P.char '[' *> P.takeWhile1P Nothing (/= ' ') <* P.char ' '
  kvs <- headerKvsP
  pure (name', kvs)

-- | Parse resource body.
bodyP :: Parser KvAnn
bodyP = HM.fromList <$> P.manyTill kvP ((P.newline $> ()) <||> P.eof)
 where
  kvP =
    (,)
      <$> P.takeWhile1P Nothing (/= ' ')
      <*> (P.string " = " *> valP <* P.newline)

-- | Parse body and header keys.
bodyHeaderP :: Parser (T.Text, KvAnn, KvAnn)
bodyHeaderP = do
  (headerName', headerKvs') <- headerP
  P.char ']' *> P.space
  body <- bodyP <||> mempty
  (headerName', headerKvs', body) <$ P.space

type Consumer = StateT (KvAnn, KvAnn, HS.HashSet T.Text, HS.HashSet T.Text) Maybe

{- |
Parser for a headered Godot file.

'Consumer' is a wrapper that tracks which keys of the generic key-value
maps for the header and body were processed. Processed keys won't appear in the
generic 'headers' and 'entries' sections of output data.
-}
headeredP :: T.Text -> Consumer (KvAnn -> KvAnn -> a) -> Parser a
headeredP sc p = do
  (headerName', headerKvs, bodyKvs) <- bodyHeaderP
  let kvRes = runStateT p (headerKvs, bodyKvs, mempty, mempty)
  if headerName' == sc
    then case kvRes of
      Just (s, (headerKvs', bodyKvs', consumedHeader, consumedBody)) ->
        pure $ s (collectRest consumedHeader headerKvs') (collectRest consumedBody bodyKvs')
      Nothing -> fail $ "parser interior failed"
    else fail "mismatch expected header"
 where
  collectRest its = HM.filterWithKey (\k _ -> not $ k `HS.member` its)

--------------------------------------------------------------------------------

{- |
Some Lens trickery that matches a key with a particular 'GdValue'
constructor, unwraps it, and marks it as processed in 'Consumer'.
-}
jq ::
  forall c s a b.
  (At s, AsConstructor c (IxValue s) (IxValue s) a a) =>
  Index s ->
  (Maybe a -> b) ->
  s ->
  b
jq k f h = f $ h ^? at k . _Just . _Ctor @c

-- | Match a key from the header.
jh ::
  forall c a.
  (AsConstructor c (IxValue KvAnn) (IxValue KvAnn) a a) =>
  Index KvAnn ->
  Consumer a
jh k = do
  modify' (over _3 (HS.insert k))
  jq @c k lift . view _1 =<< get

-- | Optionally match a key from the header.
jh' ::
  forall c a.
  (AsConstructor c (IxValue KvAnn) (IxValue KvAnn) a a) =>
  Index KvAnn ->
  Consumer (Maybe a)
jh' k = do
  res <- jq @c k id . view _1 <$> get
  case res of
    Just r -> do
      modify' (over _3 (HS.insert k))
      pure $ Just r
    Nothing -> pure Nothing

-- | Match a key from the body.
jb ::
  forall c a.
  (AsConstructor c (IxValue KvAnn) (IxValue KvAnn) a a) =>
  Index KvAnn ->
  Consumer a
jb k = jq @c k lift . view _2 =<< get

-- | Optionally match a key from the body.
jb' ::
  forall c a.
  (AsConstructor c (IxValue KvAnn) (IxValue KvAnn) a a) =>
  Index KvAnn ->
  Consumer (Maybe a)
jb' k = do
  res <- jq @c k id . view _2 <$> get
  case res of
    Just r -> do
      modify' (over _4 (HS.insert k))
      pure $ Just r
    Nothing -> pure Nothing

--------------------------------------------------------------------------------

data ExtResource = MkExtResource
  { path :: T.Text
  , type' :: T.Text
  , id' :: Int
  , headers :: KvAnn
  , entries :: KvAnn
  }
  deriving (Show, Generic)

-- | Parser for an ext_resource section.
extResourceP :: Parser ExtResource
extResourceP =
  headeredP
    "ext_resource"
    $ MkExtResource
      <$> jh @"GdString" "path"
      <*> jh @"GdString" "type"
      <*> jh @"GdInt" "id"

data SubResource = MkSubResource
  { type' :: T.Text
  , id' :: Int
  , headers :: KvAnn
  , entries :: KvAnn
  }
  deriving (Show, Generic)

-- | Parser for a sub_resource section.
subResourceP :: Parser SubResource
subResourceP =
  headeredP
    "sub_resource"
    $ MkSubResource
      <$> jh @"GdString" "type"
      <*> jh @"GdInt" "id"

data Node = MkNode
  { type' :: Maybe T.Text
  , name :: T.Text
  , parent :: Maybe T.Text
  , instance' :: Maybe Int
  , instancePlaceholder :: Maybe T.Text
  , owner :: Maybe T.Text
  , index :: Maybe Int
  , groups :: Maybe [T.Text]
  , headers :: KvAnn
  , entries :: KvAnn
  }
  deriving (Show, Generic)

-- | Parser for a node section.
nodeP :: Parser Node
nodeP =
  headeredP
    "node"
    $ MkNode
      <$> jh' @"GdString" "type"
      <*> jh @"GdString" "name"
      <*> jh' @"GdString" "parent"
      <*> (jh' @"GdCstr" "instance" <&> (^? (_Just . _2 . ix 0 . _Ctor @"GdInt")))
      <*> jh' @"GdString" "instance_placeholder"
      <*> jh' @"GdString" "owner"
      <*> jh' @"GdInt" "index"
      <*> ( jh' @"GdArr" "groups"
              & over
                ( mapped
                    . _Just
                    . mapped
                )
                (fromJust . preview (_Ctor @"GdString"))
          )

data Connection = MkConnection
  { signal :: T.Text
  , from :: T.Text
  , to :: T.Text
  , method :: T.Text
  , headers :: KvAnn
  , entries :: KvAnn
  }
  deriving (Show, Generic)

-- | Parser for a connection section.
connectionP :: Parser Connection
connectionP =
  headeredP
    "connection"
    $ MkConnection
      <$> jh @"GdString" "signal"
      <*> jh @"GdString" "from"
      <*> jh @"GdString" "to"
      <*> jh @"GdString" "method"

data Resource = MkResource
  { name :: Maybe T.Text
  , className :: Maybe T.Text
  , library :: Maybe (T.Text, [GdValue])
  }
  deriving (Show, Generic)

-- | Parser for a resource section.
resourceP :: Parser Resource
resourceP =
  headeredP
    "resource"
    $ do
      signal' <- jb' @"GdString" "signal"
      from' <- jb' @"GdString" "from"
      to' <- jb' @"GdCstr" "to"
      pure \_ _ -> MkResource signal' from' to'

data GdSection
  = ExtResourceSc ExtResource
  | SubResourceSc SubResource
  | ConnectionSc Connection
  | ResourceSc Resource
  | NodeSc Node
  | OtherSc
      { header :: T.Text
      , headers :: KvAnn
      , entries :: KvAnn
      }
  deriving (Show, Generic)

-- | Parser for an unknown section.
otherP :: Parser GdSection
otherP = do
  (headerName', headerKvs', bodyKvs) <- bodyHeaderP
  pure $ OtherSc headerName' headerKvs' bodyKvs

data TscnParsed = MkTscnParsed
  { loadSteps :: Int
  , format :: Int
  , sections :: [GdSection]
  }
  deriving (Show, Generic)

-- | Parser for a `tscn` file.
tscnP :: Parser TscnParsed
tscnP = do
  kvs <- P.string "[gd_scene " *> headerKvsP <* P.char ']' <* P.space
  let loadSteps' = jq @"GdInt" "load_steps" fromJust kvs
      format' = jq @"GdInt" "format" fromJust kvs
  sections' <- P.manyTill sectionP P.eof
  pure $ MkTscnParsed loadSteps' format' sections'
 where
  sectionP =
    P.choice $
      map
        P.try
        [ ConnectionSc <$> connectionP
        , ExtResourceSc <$> extResourceP
        , SubResourceSc <$> subResourceP
        , NodeSc <$> nodeP
        ]

data GdExtensionParsed = MkGdExtensionParsed
  { entrySymbol :: T.Text
  , libraries :: HM.HashMap T.Text T.Text
  }
  deriving (Show, Generic)

-- | Parser for a `gdextension` file.
gdExtensionP :: Parser GdExtensionParsed
gdExtensionP = do
  (_, _, HM.toList -> [("entry_symbol", GdString entryLib)]) <- bodyHeaderP
  (_, _, libs) <- bodyHeaderP
  let libs' = HM.map (fromJust . preview (_Ctor @"GdString")) libs
  pure $ MkGdExtensionParsed entryLib libs'

data UnknownParsed = MkUnknownParsed
  { headerName :: T.Text
  , headers :: KvAnn
  , sections :: [GdSection]
  }
  deriving (Show, Generic)

-- | Parser for an unknown file.
unknownP :: Parser UnknownParsed
unknownP = do
  headerName' <- P.char '[' *> P.takeWhile1P Nothing (/= ' ') <* P.char ' '
  headers' <- headerKvsP <* P.char ']' <* P.space
  sections' <- P.manyTill otherP P.eof
  pure $ MkUnknownParsed headerName' headers' sections'

data GdParsed
  = MkTscn TscnParsed
  | MkGdExtension GdExtensionParsed
  | MkUnknown UnknownParsed
  deriving (Show, Generic)

-- | Parse any Godot file.
parsedP :: Parser GdParsed
parsedP =
  P.choice $
    map
      P.try
      [ MkTscn <$> tscnP
      , MkGdExtension <$> gdExtensionP
      , MkUnknown <$> unknownP
      ]