prosidyc-0.3.0.0: src/Prosidy/Compile/DSL.hs
{-|
Module : Prosidy.Compile.DSL
Description : An EDSL for declaring 'Prosidy.Compile.Core.Rules'.
Copyright : ©2020 James Alexander Feldman-Crough
License : MPL-2.0
Maintainer : alex@fldcr.com
-}
{-# LANGUAGE DefaultSignatures #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE PatternSynonyms #-}
module Prosidy.Compile.DSL
( content
-- * Series rules
, (&>)
, (&>>)
, folded
, folded1
, collect
, end
, endWith
-- * Metadata rules
, prop
, req
, opt
, lax
-- * Matchers
, Match
, match
, blockTag
, inlineTag
, literalTag
, paragraph
, text
, breakWith
-- * Get wild with actions
, local
, self
, hoist
-- * Convenience classes
, FromSetting(..)
, RegionLike
)
where
import qualified Prosidy
import Prosidy.Types.Series ( pattern Empty
, pattern (:<:)
, pattern (:<<:)
)
import Prosidy.Compile.Core
import Data.Monoid ( Alt(..) )
import Text.Read ( readMaybe )
import Type.Reflection ( Typeable
, typeRep
)
import Data.Text ( Text )
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Text.Lazy
import Numeric.Natural ( Natural )
infixr 3 &>
infixr 1 &>>
-------------------------------------------------------------------------------
-- | Access the inner content of the 'RegionLike' value @t@.
content :: RegionLike t => Rules (Prosidy.Content t) f a -> Rules t f a
content = rule . liftRegionRule . RegionRuleContent
-------------------------------------------------------------------------------
-- | Given a 'Prosidy.Series', perform the rule on the left hand side on the
-- first element of the 'Prosidy.Series', and the rule on the right hand side
-- on all items after the first.
--
-- This can be used to define rules which must be evaluated sequentially.
(&>)
:: Rules t f a
-> Rules (Prosidy.Series t) f (Prosidy.Series a)
-> Rules (Prosidy.Series t) f (Prosidy.Series a)
r &> rs = rule . SeriesRuleNext $ SeriesNERule (:<:) r rs
-- | Like '(&>)', but returns a combined rule which operates on a non-empty
-- series ('Prosidy.SeriesNE').
(&>>)
:: Rules t f a
-> Rules (Prosidy.Series t) f (Prosidy.Series a)
-> Rules (Prosidy.SeriesNE t) f (Prosidy.SeriesNE a)
r &>> rs = rule $ SeriesNERule (:<<:) r rs
-- | Match the end of a 'Prosidy.Series'.
end :: Rules (Prosidy.Series t) f (Prosidy.Series a)
end = endWith Empty
-- | Match the end of a 'Prosidy.Series', returning the provided value.
endWith :: a -> Rules (Prosidy.Series t) f a
endWith = rule . SeriesRuleEmpty
-- | Lift a rule to collect many of that rule in series
collect :: Rules t f a -> Rules (Prosidy.Series t) f (Prosidy.Series a)
collect rules = go
where
go = (rules &> go) <|> end
-- | Lift a rule to operate on a 'Prosidy.Series' by folding the results of
-- evaluation against each element into a single result.
folded :: Monoid a => Rules t f a -> Rules (Prosidy.Series t) f a
folded r = go
where
go = rule (SeriesRuleNext $ SeriesNERule (<>) r go)
<|> rule (SeriesRuleEmpty mempty)
-- | Like 'folded', but operates on a non-empty series.
folded1 :: Monoid a => Rules t f a -> Rules (Prosidy.SeriesNE t) f a
folded1 r = rule $ SeriesNERule (<>) r (folded r)
-------------------------------------------------------------------------------
-- | A class for recursive nodes in a document.
class Prosidy.HasContent t => RegionLike t where
liftRegionRule :: RegionRule (Prosidy.Content t) f a -> RuleFor t f a
instance RegionLike Prosidy.Document where
liftRegionRule = DocumentRule
instance RegionLike (Prosidy.Tag t) where
liftRegionRule = TagRuleRegion
instance RegionLike (Prosidy.Region t) where
liftRegionRule = id
-- | Check if a 'Prosidy.Metadata' property is set on a node.
prop :: RegionLike t => Prosidy.Key -> Rules t f Bool
prop = rule . liftRegionRule . RegionRuleMetadata . MetadataRuleProperty id
-- | Fetch a /required/ 'Prosidy.Metadata' value from a node, parsing it using
-- the provided function.
reqWith
:: forall a t f. RegionLike t => (Text -> Either String a) -> Prosidy.Key -> Rules t f a
reqWith parse =
rule
. liftRegionRule
. RegionRuleMetadata
. MetadataRuleSetting parse Nothing
-- | Fetch a /required/ 'Prosidy.Metadata' setting from a node.
req :: forall a t f. (RegionLike t, FromSetting a) => Prosidy.Key -> Rules t f a
req = reqWith parseSetting
-- | Fetch an /optional/ 'Prosidy.Metadata' value from a node, parsing it using
-- the provided function.
optWith
:: forall a t f. RegionLike t
=> (Text -> Either String a)
-> Prosidy.Key
-> Rules t f (Maybe a)
optWith parse =
rule . liftRegionRule . RegionRuleMetadata . MetadataRuleSetting
(fmap Just . parse)
(Just Nothing)
-- | Fetch an /optional/ 'Prosidy.Metadata' setting from a node.
opt :: forall a t f. (RegionLike t, FromSetting a) => Prosidy.Key -> Rules t f (Maybe a)
opt = optWith parseSetting
-- | Allow unknown properties and settings in this region.
lax :: RegionLike t => Rules t f ()
lax = rule . liftRegionRule . RegionRuleMetadata $ MetadataRuleAllowUnknown ()
-------------------------------------------------------------------------------
-- | A class for values which can be parsed from 'Text'.
class FromSetting a where
parseSetting :: Text -> Either String a
default parseSetting :: (Typeable a, Read a) => Text -> Either String a
parseSetting raw = case readMaybe (Text.unpack raw) of
Just ok -> Right ok
Nothing -> Left $ "Failed to parse " <> show raw <> " as type " <> show (typeRep @a)
instance FromSetting Double
instance FromSetting Float
instance FromSetting Int
instance FromSetting Integer
instance FromSetting Natural
instance FromSetting Word
instance FromSetting String where
parseSetting = Right . Text.unpack
instance FromSetting Text where
parseSetting = Right
{-# INLINE parseSetting #-}
instance FromSetting Text.Lazy.Text where
parseSetting = Right . Text.Lazy.fromStrict
{-# INLINE parseSetting #-}
-------------------------------------------------------------------------------
-- | A type used to declare alternatives in @do@ notation.
type Match t f a = MatchM t f a ()
data MatchM t f a r = MatchM !(Alt (Rules t f) a) !r
instance Semigroup r => Semigroup (MatchM t a f r) where
MatchM r a <> MatchM s b = MatchM (r <> s) (a <> b)
instance Monoid r => Monoid (MatchM t a f r) where
mempty = MatchM mempty mempty
instance Functor (MatchM t f a) where
fmap fn (MatchM r x) = MatchM r (fn x)
instance Applicative (MatchM t f a) where
pure = MatchM mempty
MatchM lhs fn <*> MatchM rhs x = MatchM (lhs <> rhs) (fn x)
instance Monad (MatchM t f a) where
MatchM lhs x >>= f = let MatchM rhs x' = f x in MatchM (lhs <> rhs) x'
-- | Lifts a 'Match' into 'Rules' by trying each defined pattern, from top to
-- bottom, until a match is found.
match :: Match t f a -> Rules t f a
match (MatchM (Alt r) ()) = r
-- | Match a 'Prosidy.BlockTag' with the proided 'Prosidy.Key'.
blockTag
:: Functor f
=> Prosidy.Key
-> Rules Prosidy.BlockRegion f a
-> Match Prosidy.Block f a
blockTag key = matchRule . BlockRuleBlockTag . tagRule key
-- | Match a 'Prosidy.LiteralTag' with the provided 'Prosidy.Key'.
literalTag
:: Functor f
=> Prosidy.Key
-> Rules Prosidy.LiteralRegion f a
-> Match Prosidy.Block f a
literalTag key = matchRule . BlockRuleLiteralTag . tagRule key
-- | Match a 'Prosidy.InlineTag' with the provided 'Prosidy.Key'.
inlineTag
:: Functor f
=> Prosidy.Key
-> Rules Prosidy.InlineRegion f a
-> Match Prosidy.Inline f a
inlineTag key = matchRule . InlineRuleInlineTag . tagRule key
-- | Match a paragraph which is not enclosed in a tag.
paragraph
:: Rules (Prosidy.SeriesNE Prosidy.Inline) f a -> Match Prosidy.Block f a
paragraph = matchRule . BlockRuleParagraph . rule . ParagraphRuleContent
-- | Match textual content, transforming it with the provided function.
text :: (Text -> a) -> Match Prosidy.Inline f a
text = matchRule . InlineRuleFragment . rule . FragmentRuleText
-- | Replace inline breaks with the provided vlaue.
breakWith :: a -> Match Prosidy.Inline f a
breakWith = matchRule . InlineRuleBreak
matchRule :: RuleFor t f a -> Match t f a
matchRule = flip MatchM () . Alt . rule
tagRule
:: Functor f
=> Prosidy.Key
-> Rules (Prosidy.Region t) f a
-> Rules (Prosidy.Tag t) f a
tagRule key r =
rule (TagRuleKey key ()) *> mapRules liftRegionRule Prosidy.tagToRegion id r