{-# LANGUAGE GeneralizedNewtypeDeriving #-} {-# LANGUAGE DeriveGeneric #-} -- | Data types and functions for representing a simplified form of PureScript -- code, intended for use in e.g. HTML documentation. module Language.PureScript.Docs.RenderedCode.Types ( RenderedCodeElement(..) , asRenderedCodeElement , ContainingModule(..) , asContainingModule , containingModuleToMaybe , maybeToContainingModule , fromContainingModule , fromQualified , Namespace(..) , Link(..) , FixityAlias , RenderedCode , asRenderedCode , outputWith , sp , parens , syntax , keyword , keywordForall , keywordData , keywordNewtype , keywordType , keywordClass , keywordInstance , keywordWhere , keywordFixity , keywordKind , keywordAs , ident , dataCtor , typeCtor , typeOp , typeVar , kind , alias , aliasName ) where import Prelude.Compat import GHC.Generics (Generic) import Control.DeepSeq (NFData) import Control.Monad.Error.Class (MonadError(..)) import Data.Aeson.BetterErrors (Parse, nth, withText, withValue, toAesonParser, perhaps, asText, eachInArray) import qualified Data.Aeson as A import Data.Text (Text) import qualified Data.Text as T import qualified Data.ByteString.Lazy as BS import qualified Data.Text.Encoding as TE import Language.PureScript.Names import Language.PureScript.AST (Associativity(..)) import Language.PureScript.Crash (internalError) -- | Given a list of actions, attempt them all, returning the first success. -- If all the actions fail, 'tryAll' returns the first argument. tryAll :: MonadError e m => m a -> [m a] -> m a tryAll = foldr $ \x y -> catchError x (const y) firstEq :: Text -> Parse Text a -> Parse Text a firstEq str p = nth 0 (withText (eq str)) *> p where eq s s' = if s == s' then Right () else Left "" -- | -- Try the given parsers in sequence. If all fail, fail with the given message, -- and include the JSON in the error. -- tryParse :: Text -> [Parse Text a] -> Parse Text a tryParse msg = tryAll (withValue (Left . (fullMsg <>) . showJSON)) where fullMsg = "Invalid " <> msg <> ": " showJSON :: A.Value -> Text showJSON = TE.decodeUtf8 . BS.toStrict . A.encode -- | -- This type is isomorphic to 'Maybe' 'ModuleName'. It makes code a bit -- easier to read, as the meaning is more explicit. -- data ContainingModule = ThisModule | OtherModule ModuleName deriving (Show, Eq, Ord) instance A.ToJSON ContainingModule where toJSON = A.toJSON . go where go = \case ThisModule -> ["ThisModule"] OtherModule mn -> ["OtherModule", runModuleName mn] instance A.FromJSON ContainingModule where parseJSON = toAesonParser id asContainingModule asContainingModule :: Parse Text ContainingModule asContainingModule = tryParse "containing module" $ current ++ backwardsCompat where current = [ firstEq "ThisModule" (pure ThisModule) , firstEq "OtherModule" (OtherModule <$> nth 1 asModuleName) ] -- For JSON produced by compilers up to 0.10.5. backwardsCompat = [ maybeToContainingModule <$> perhaps asModuleName ] asModuleName = moduleNameFromString <$> asText -- | -- Convert a 'Maybe' 'ModuleName' to a 'ContainingModule', using the obvious -- isomorphism. -- maybeToContainingModule :: Maybe ModuleName -> ContainingModule maybeToContainingModule Nothing = ThisModule maybeToContainingModule (Just mn) = OtherModule mn -- | -- Convert a 'ContainingModule' to a 'Maybe' 'ModuleName', using the obvious -- isomorphism. -- containingModuleToMaybe :: ContainingModule -> Maybe ModuleName containingModuleToMaybe ThisModule = Nothing containingModuleToMaybe (OtherModule mn) = Just mn -- | -- A version of 'fromMaybe' for 'ContainingModule' values. -- fromContainingModule :: ModuleName -> ContainingModule -> ModuleName fromContainingModule def ThisModule = def fromContainingModule _ (OtherModule mn) = mn fromQualified :: Qualified a -> (ContainingModule, a) fromQualified (Qualified mn x) = (maybeToContainingModule mn, x) data Link = NoLink | Link ContainingModule deriving (Show, Eq, Ord) instance A.ToJSON Link where toJSON = \case NoLink -> A.toJSON ["NoLink" :: Text] Link mn -> A.toJSON ["Link", A.toJSON mn] asLink :: Parse Text Link asLink = tryParse "link" [ firstEq "NoLink" (pure NoLink) , firstEq "Link" (Link <$> nth 1 asContainingModule) ] instance A.FromJSON Link where parseJSON = toAesonParser id asLink data Namespace = ValueLevel | TypeLevel | KindLevel deriving (Show, Eq, Ord, Generic) instance NFData Namespace instance A.ToJSON Namespace where toJSON = A.toJSON . show asNamespace :: Parse Text Namespace asNamespace = tryParse "namespace" [ withText $ \case "ValueLevel" -> Right ValueLevel "TypeLevel" -> Right TypeLevel "KindLevel" -> Right KindLevel _ -> Left "" ] instance A.FromJSON Namespace where parseJSON = toAesonParser id asNamespace -- | -- A single element in a rendered code fragment. The intention is to support -- multiple output formats. For example, plain text, or highlighted HTML. -- data RenderedCodeElement = Syntax Text | Keyword Text | Space -- | Any symbol which you might or might not want to link to, in any -- namespace (value, type, or kind). Note that this is not related to the -- kind called Symbol for type-level strings. | Symbol Namespace Text Link deriving (Show, Eq, Ord) instance A.ToJSON RenderedCodeElement where toJSON (Syntax str) = A.toJSON ["syntax", str] toJSON (Keyword str) = A.toJSON ["keyword", str] toJSON Space = A.toJSON ["space" :: Text] toJSON (Symbol ns str link) = A.toJSON ["symbol", A.toJSON ns, A.toJSON str, A.toJSON link] asRenderedCodeElement :: Parse Text RenderedCodeElement asRenderedCodeElement = tryParse "RenderedCodeElement" $ [ a Syntax "syntax" , a Keyword "keyword" , asSpace , asSymbol ] ++ backwardsCompat where a ctor' ctorStr = firstEq ctorStr (ctor' <$> nth 1 asText) asSymbol = firstEq "symbol" (Symbol <$> nth 1 asNamespace <*> nth 2 asText <*> nth 3 asLink) asSpace = firstEq "space" (pure Space) -- These will make some mistakes e.g. treating data constructors as types, -- because the old code did not save information which is necessary to -- distinguish these cases. This is the best we can do. backwardsCompat = [ oldAsIdent , oldAsCtor , oldAsKind ] oldAsIdent = firstEq "ident" (Symbol ValueLevel <$> nth 1 asText <*> nth 2 (Link <$> asContainingModule)) oldAsCtor = firstEq "ctor" (Symbol TypeLevel <$> nth 1 asText <*> nth 2 (Link <$> asContainingModule)) oldAsKind = firstEq "kind" (Symbol KindLevel <$> nth 1 asText <*> pure (Link ThisModule)) -- | -- A type representing a highly simplified version of PureScript code, intended -- for use in output formats like plain text or HTML. -- newtype RenderedCode = RC { unRC :: [RenderedCodeElement] } deriving (Show, Eq, Ord, Semigroup, Monoid) instance A.ToJSON RenderedCode where toJSON (RC elems) = A.toJSON elems asRenderedCode :: Parse Text RenderedCode asRenderedCode = RC <$> eachInArray asRenderedCodeElement -- | -- This function allows conversion of a 'RenderedCode' value into a value of -- some other type (for example, plain text, or HTML). The first argument -- is a function specifying how each individual 'RenderedCodeElement' should be -- rendered. -- outputWith :: Monoid a => (RenderedCodeElement -> a) -> RenderedCode -> a outputWith f = foldMap f . unRC -- | -- A 'RenderedCode' fragment representing a space. -- sp :: RenderedCode sp = RC [Space] -- | -- Wrap a RenderedCode value in parens. parens :: RenderedCode -> RenderedCode parens x = syntax "(" <> x <> syntax ")" -- possible TODO: instead of this function, export RenderedCode values for -- each syntax element, eg syntaxArr (== syntax "->"), syntaxLBrace, -- syntaxRBrace, etc. syntax :: Text -> RenderedCode syntax x = RC [Syntax x] keyword :: Text -> RenderedCode keyword kw = RC [Keyword kw] keywordForall :: RenderedCode keywordForall = keyword "forall" keywordData :: RenderedCode keywordData = keyword "data" keywordNewtype :: RenderedCode keywordNewtype = keyword "newtype" keywordType :: RenderedCode keywordType = keyword "type" keywordClass :: RenderedCode keywordClass = keyword "class" keywordInstance :: RenderedCode keywordInstance = keyword "instance" keywordWhere :: RenderedCode keywordWhere = keyword "where" keywordFixity :: Associativity -> RenderedCode keywordFixity Infixl = keyword "infixl" keywordFixity Infixr = keyword "infixr" keywordFixity Infix = keyword "infix" keywordKind :: RenderedCode keywordKind = keyword "kind" keywordAs :: RenderedCode keywordAs = keyword "as" ident :: Qualified Ident -> RenderedCode ident (fromQualified -> (mn, name)) = RC [Symbol ValueLevel (runIdent name) (Link mn)] dataCtor :: Qualified (ProperName 'ConstructorName) -> RenderedCode dataCtor (fromQualified -> (mn, name)) = RC [Symbol ValueLevel (runProperName name) (Link mn)] typeCtor :: Qualified (ProperName 'TypeName) -> RenderedCode typeCtor (fromQualified -> (mn, name)) = RC [Symbol TypeLevel (runProperName name) (Link mn)] typeOp :: Qualified (OpName 'TypeOpName) -> RenderedCode typeOp (fromQualified -> (mn, name)) = RC [Symbol TypeLevel (runOpName name) (Link mn)] typeVar :: Text -> RenderedCode typeVar x = RC [Symbol TypeLevel x NoLink] kind :: Qualified (ProperName 'KindName) -> RenderedCode kind (fromQualified -> (mn, name)) = RC [Symbol KindLevel (runProperName name) (Link mn)] type FixityAlias = Qualified (Either (ProperName 'TypeName) (Either Ident (ProperName 'ConstructorName))) alias :: FixityAlias -> RenderedCode alias for = prefix <> RC [Symbol ns name (Link mn)] where (ns, name, mn) = unpackFixityAlias for prefix = case ns of TypeLevel -> keywordType <> sp _ -> mempty aliasName :: FixityAlias -> Text -> RenderedCode aliasName for name' = let (ns, _, _) = unpackFixityAlias for unParen = T.tail . T.init name = unParen name' in case ns of ValueLevel -> ident (Qualified Nothing (Ident name)) TypeLevel -> typeCtor (Qualified Nothing (ProperName name)) KindLevel -> internalError "Kind aliases are not supported" -- | Converts a FixityAlias into a different representation which is more -- useful to other functions in this module. unpackFixityAlias :: FixityAlias -> (Namespace, Text, ContainingModule) unpackFixityAlias (fromQualified -> (mn, x)) = case x of -- We add some seemingly superfluous type signatures here just to be extra -- sure we are not mixing up our namespaces. Left (n :: ProperName 'TypeName) -> (TypeLevel, runProperName n, mn) Right (Left n) -> (ValueLevel, runIdent n, mn) Right (Right (n :: ProperName 'ConstructorName)) -> (ValueLevel, runProperName n, mn)