prosidy-1.6.0.0: src/Prosidy/Types.hs
{-|
Module : Prosidy.Types
Description : A convenience module which reëxports type definitions & helpers.
Copyright : ©2020 James Alexander Feldman-Crough
License : MPL-2.0
Maintainer : alex@fldcr.com
-}
{-# LANGUAGE DeriveAnyClass #-}
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE DeriveFunctor #-}
{-# LANGUAGE DeriveFoldable #-}
{-# LANGUAGE DeriveTraversable #-}
{-# LANGUAGE DerivingStrategies #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE GeneralizedNewtypeDeriving #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE StrictData #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE UndecidableInstances #-}
module Prosidy.Types
( -- * Documents
Document(..)
, documentToRegion
, regionToDocument
-- * Tags
, Tag(..)
, BlockTag
, InlineTag
, LiteralTag
, tagToRegion
, regionToTag
-- * Contextual enumerations
, Block(..)
, Inline(..)
-- * Paragraphs
, Paragraph(..)
-- * Common structures
, Metadata(..)
, Region(..)
-- * Textual fragments
, Fragment(..)
-- * Utility wrappers
, module X
)
where
import Prosidy.Types.Assoc as X
( Assoc(..) )
import Prosidy.Types.Key as X
( Key
, KeyError(..)
, InvalidCharacter
, makeKey
, rawKey
)
import Prosidy.Types.Series as X
( Series(..)
, SeriesNE
)
import Prosidy.Types.Set as X
( Set(..) )
import Prosidy.Source ( Location )
import Data.Text ( Text )
import GHC.Generics ( Generic )
import Control.DeepSeq ( NFData )
import Data.Binary ( Binary )
import Data.Hashable ( Hashable )
import Data.Aeson ( ToJSON(..)
, FromJSON(..)
, withObject
, withText
, (.:)
, (.=)
, object
, pairs
)
import qualified Data.Aeson as Aeson
-------------------------------------------------------------------------------
-- | A sum type enumerating allowed types inside of a block context.
data Block =
BlockLiteral LiteralTag
| BlockParagraph Paragraph
| BlockTag BlockTag
deriving stock (Eq, Show, Generic)
deriving anyclass (Hashable, Binary, NFData)
instance FromJSON Block where
parseJSON = withObject "block" $ \o -> do
ty <- o .: "type"
case ty :: Text of
"tag" -> do
subtype <- o .: "subtype"
case subtype :: Text of
"block" -> BlockTag <$> o .: "value"
"literal" -> BlockLiteral <$> o .: "value"
_ -> fail $ "unknown tag subtype: " <> show subtype
"paragraph" -> BlockParagraph <$> o .: "value"
_ -> fail $ "unknown block type: " <> show ty
instance ToJSON Block where
toEncoding b = pairs . mconcat $ case b of
BlockLiteral t ->
[ "type" .= ("tag" :: Text)
, "subtype" .= ("literal" :: Text)
, "value" .= t
]
BlockParagraph p -> ["type" .= ("paragraph" :: Text), "value" .= p]
BlockTag t ->
[ "type" .= ("tag" :: Text)
, "subtype" .= ("block" :: Text)
, "value" .= t
]
toJSON b = object $ case b of
BlockLiteral t ->
[ "type" .= ("tag" :: Text)
, "subtype" .= ("literal" :: Text)
, "value" .= t
]
BlockParagraph p -> ["type" .= ("paragraph" :: Text), "value" .= p]
BlockTag t ->
[ "type" .= ("tag" :: Text)
, "subtype" .= ("block" :: Text)
, "value" .= t
]
-------------------------------------------------------------------------------
-- | A full Prosidy document.
data Document = Document
{ documentMetadata :: Metadata
, documentContent :: Series Block
}
deriving stock (Eq, Show, Generic)
deriving anyclass (Hashable, NFData, Binary)
instance FromJSON Document where
parseJSON = withObject "Document"
$ \o -> Document <$> o .: "metadata" <*> o .: "content"
instance ToJSON Document where
toEncoding (Document md ct) =
pairs $ mconcat ["metadata" .= md, "content" .= ct]
toJSON (Document md ct) = object ["metadata" .= md, "content" .= ct]
-- | Convert a 'Document' to a 'Region'. The resulting 'Region' will never have
-- a 'Location' attached.
documentToRegion :: Document -> Region (Series Block)
documentToRegion (Document md ct) = Region md ct Nothing
-- | Convert a 'Region' to a 'Document'. Any 'Location' on the 'Region' will be
-- discarded.
regionToDocument :: Region (Series Block) -> Document
regionToDocument (Region md ct _) = Document md ct
-------------------------------------------------------------------------------
-- | Plain text, possibly annotated with a 'Location'.
data Fragment = Fragment
{ fragmentText :: Text
-- ^ Access the underlying 'Text'.
, fragmentLocation :: Maybe Location
-- ^ The location of the 'Text' in the source code.
}
deriving stock (Eq, Show, Generic)
deriving anyclass (Hashable, Binary, NFData)
instance FromJSON Fragment where
parseJSON = withText "Fragment" $ pure . flip Fragment Nothing
instance ToJSON Fragment where
toEncoding = toEncoding . fragmentText
toJSON = toJSON . fragmentText
-------------------------------------------------------------------------------
-- | A sum type enumerating allowed types inside of an inline context.
data Inline =
Break
-- ^ Spacing recorded between lines or on either side of an 'Inline' 'Tag'.
-- Although we could represent this as 'Text', Prosidy defines a special
-- node for this case so that authors in CJK languages (or other languages
-- without explicit spaces between words) may simply ignore these spaces
-- in their output.
| InlineTag InlineTag
-- ^ A 'Tag' which contains only 'Inline' items. These tags begin with the
-- @#@ sigil in source.
| InlineText Fragment
-- ^ A fragment of plain text.
deriving stock (Eq, Show, Generic)
deriving anyclass (Hashable, Binary, NFData)
instance FromJSON Inline where
parseJSON = withObject "Inline" $ \o -> do
ty <- o .: "type"
case ty :: Text of
"break" -> pure Break
"tag" -> InlineTag <$> o .: "value"
"text" -> InlineText <$> o .: "value"
_ -> fail $ "unknown inline type: " <> show ty
instance ToJSON Inline where
toEncoding i = pairs . mconcat $ case i of
Break -> ["type" .= ("break" :: Text), "value" .= Aeson.Null]
InlineTag t ->
[ "type" .= ("tag" :: Text)
, "subtype" .= ("inline" :: Text)
, "value" .= t
]
InlineText t -> ["type" .= ("text" :: Text), "value" .= t]
toJSON i = object $ case i of
Break -> ["type" .= ("break" :: Text)]
InlineTag t ->
[ "type" .= ("tag" :: Text)
, "subtype" .= ("inline" :: Text)
, "value" .= t
]
InlineText t -> ["type" .= ("text" :: Text), "value" .= t]
-------------------------------------------------------------------------------
-- | A set of properties and settings, associated with a 'Region'.
--
-- The namespaces of properties and settings are distinct; a property can share
-- a name with a setting without conflict.
data Metadata = Metadata
{ metadataProperties :: Set Key
-- ^ Properties are a set of 'Key's with no associated value.
, metadataSettings :: Assoc Key Text
-- ^ Settings are 'Key's with an attached value.
}
deriving stock (Eq, Show, Generic)
deriving anyclass (NFData, Binary, Hashable)
instance Monoid Metadata where
mempty = Metadata mempty mempty
instance Semigroup Metadata where
Metadata p1 s1 <> Metadata p2 s2 = Metadata (p1 <> p2) (s1 <> s2)
instance FromJSON Metadata where
parseJSON = withObject "Metadata"
$ \o -> Metadata <$> o .: "properties" <*> o .: "settings"
instance ToJSON Metadata where
toEncoding (Metadata ps ss) =
pairs $ mconcat ["properties" .= ps, "settings" .= ss]
toJSON (Metadata ps ss) = object ["properties" .= ps, "settings" .= ss]
-------------------------------------------------------------------------------
-- | A non-empty collection of 'Inline' items. A 'Paragraph' represents the
-- border between block and inline contexts. All ancestors of a paragraph are
-- block items or a document, and all children are inline items.
data Paragraph = Paragraph
{ paragraphContent :: SeriesNE Inline
, paragraphLocation :: Maybe Location
}
deriving stock (Eq, Show, Generic)
deriving anyclass (Hashable, NFData, Binary)
instance FromJSON Paragraph where
parseJSON = fmap (flip Paragraph Nothing) . parseJSON
instance ToJSON Paragraph where
toEncoding (Paragraph s _) = toEncoding s
toJSON (Paragraph s _) = toJSON s
-------------------------------------------------------------------------------
-- | An untagged structural grouping of items with type @a@. Regions do not
-- occur in parsing.
data Region a = Region
{ regionMetadata :: Metadata
, regionContent :: a
, regionLocation :: Maybe Location
}
deriving stock (Eq, Foldable, Functor, Show, Traversable, Generic)
deriving anyclass (Hashable, NFData, Binary)
instance ToJSON a => ToJSON (Region a) where
toJSON (Region md ct _) = Aeson.object ["metadata" .= md, "content" .= ct]
-------------------------------------------------------------------------------
-- | A 'Region', annotated with a tag name.
data Tag a = Tag
{ tagName :: Key
, tagMetadata :: Metadata
, tagContent :: a
, tagLocation :: Maybe Location
}
deriving stock (Eq, Foldable, Functor, Show, Traversable, Generic)
deriving anyclass (Hashable, NFData, Binary)
instance FromJSON a => FromJSON (Tag a) where
parseJSON = withObject "Tag" $ \o ->
Tag
<$> o
.: "name"
<*> o
.: "metadata"
<*> o
.: "content"
<*> pure Nothing
instance ToJSON a => ToJSON (Tag a) where
toEncoding (Tag nm md ct _) =
pairs $ mconcat ["name" .= nm, "metadata" .= md, "content" .= ct]
toJSON (Tag nm md ct _) =
object ["name" .= nm, "metadata" .= md, "content" .= ct]
-- | A 'Tag' containing zero or more 'Block' items.
-- Specified in Prosidy source with the @#-@ sigil.
type BlockTag = Tag (Series Block)
-- | A 'Tag' containing zero or more 'Inline' items.
-- Specified in Prosidy source with the @#@ sigil.
type InlineTag = Tag (Series Inline)
-- | A 'Tag' containing a single plain-text item.
-- Specified in Prosidy source with the @#=@ sigil.
type LiteralTag = Tag Text
-- | Convert a 'Tag' to a 'Region' by discarding the tag's name.
tagToRegion :: Tag a -> Region a
tagToRegion (Tag _ md ct loc) = Region md ct loc
-- | Convert a 'Region' to a 'Tag' by providing a tag name.
regionToTag :: Key -> Region a -> Tag a
regionToTag name (Region md ct loc) = Tag name md ct loc