{-# LANGUAGE DeriveDataTypeable, OverloadedStrings, PatternSynonyms, TemplateHaskellQuotes, TypeFamilies #-}
module Css.Selector.Core (
ToCssSelector(..)
, Selector(..)
, SelectorCombinator(..), SelectorGroup(..)
, SelectorSequence(..)
, combinatorText, combine
, (.>), (.+), (.~)
, SelectorFilter(..), filters, filters', addFilters, (.:)
, Namespace(..), pattern NEmpty
, ElementName(..), TypeSelector(..), pattern Universal, (.|)
, Attrib(..), AttributeCombinator(..), AttributeName(..), AttributeValue
, (.=), (.~=), (.|=), (.^=), (.$=), (.*=)
, attrib, attributeCombinatorText
, Class(..), (...)
, Hash(..), (.#)
, SelectorSpecificity(..), specificity, specificityValue
) where
import Css.Selector.Utils(encodeIdentifier, encodeText, toIdentifier)
import Data.Aeson(Value(String), ToJSON(toJSON))
import Data.Data(Data)
import Data.Default(Default(def))
import Data.Function(on)
import Data.List(sort, unfoldr)
import Data.List.NonEmpty(NonEmpty((:|)))
import qualified Data.List.NonEmpty
import Data.Ord(comparing)
import Data.String(IsString(fromString))
import Data.Text(Text, cons, intercalate, pack, unpack)
import GHC.Exts(IsList(Item, fromList, toList))
import Language.Haskell.TH.Lib(appE, conE)
import Language.Haskell.TH.Syntax(Lift(lift), Exp(AppE, ConE, LitE), Lit(StringL), Name, Pat(ConP, ListP, ViewP), Q)
import Test.QuickCheck.Arbitrary(Arbitrary(arbitrary), arbitraryBoundedEnum)
import Test.QuickCheck.Gen(Gen, frequency, listOf, listOf1, oneof)
import Text.Blaze(ToMarkup(toMarkup), text)
import Text.Blaze.Internal(Markup)
import Text.Julius(Javascript, ToJavascript(toJavascript))
data SelectorSpecificity =
SelectorSpecificity Int Int Int
deriving (Data, Show)
specificityValue :: SelectorSpecificity
-> Int
specificityValue (SelectorSpecificity a b c) = 100*a + 10*b + c
class ToCssSelector a where
toCssSelector :: a
-> Text
toSelectorGroup :: a
-> SelectorGroup
specificity' :: a
-> SelectorSpecificity
toPattern :: a
-> Pat
normalize :: a
-> a
normalize = id
{-# MINIMAL toCssSelector, toSelectorGroup, specificity', toPattern #-}
specificity :: ToCssSelector a => a
-> Int
specificity = specificityValue . specificity'
newtype SelectorGroup = SelectorGroup {
unSelectorGroup :: NonEmpty Selector
} deriving (Data, Eq, Ord, Show)
data Selector =
Selector SelectorSequence
| Combined SelectorSequence SelectorCombinator Selector
deriving (Data, Eq, Ord, Show)
data SelectorCombinator =
Descendant
| Child
| DirectlyPreceded
| Preceded
deriving (Bounded, Data, Enum, Eq, Ord, Read, Show)
combinatorText :: SelectorCombinator
-> Text
combinatorText Descendant = " "
combinatorText Child = " > "
combinatorText DirectlyPreceded = " + "
combinatorText Preceded = " ~ "
combine :: SelectorCombinator
-> Selector
-> Selector
-> Selector
combine c0 x0 ys = go x0
where go (Selector x) = Combined x c0 ys
go (Combined s1 c s2) = Combined s1 c (go s2)
(.>) :: Selector
-> Selector
-> Selector
(.>) = combine Child
(.+) :: Selector
-> Selector
-> Selector
(.+) = combine DirectlyPreceded
(.~) :: Selector
-> Selector
-> Selector
(.~) = combine Preceded
data SelectorSequence =
SimpleSelector TypeSelector
| Filter SelectorSequence SelectorFilter
deriving (Data, Eq, Ord, Show)
addFilters :: SelectorSequence
-> [SelectorFilter]
-> SelectorSequence
addFilters = foldl Filter
(.:) :: SelectorSequence
-> [SelectorFilter]
-> SelectorSequence
(.:) = addFilters
filters' :: SelectorSequence
-> [SelectorFilter]
filters' = unfoldr go
where go (Filter s f) = Just (f, s)
go (SimpleSelector _) = Nothing
filters :: SelectorSequence
-> [SelectorFilter]
filters = reverse . filters'
data SelectorFilter =
SHash Hash
| SClass Class
| SAttrib Attrib
deriving (Data, Eq, Ord, Show)
data Attrib =
Exist AttributeName
| Attrib AttributeName AttributeCombinator AttributeValue
deriving (Data, Eq, Ord, Show)
attrib :: AttributeCombinator
-> AttributeName
-> AttributeValue
-> Attrib
attrib = flip Attrib
(.=) :: AttributeName
-> AttributeValue
-> Attrib
(.=) = attrib Exact
(.~=) :: AttributeName
-> AttributeValue
-> Attrib
(.~=) = attrib Include
(.|=) :: AttributeName
-> AttributeValue
-> Attrib
(.|=) = attrib DashMatch
(.^=) :: AttributeName
-> AttributeValue
-> Attrib
(.^=) = attrib PrefixMatch
(.$=) :: AttributeName
-> AttributeValue
-> Attrib
(.$=) = attrib SuffixMatch
(.*=) :: AttributeName
-> AttributeValue
-> Attrib
(.*=) = attrib SubstringMatch
(.#) :: SelectorSequence
-> Hash
-> SelectorSequence
(.#) = (. SHash) . Filter
(...) :: SelectorSequence
-> Class
-> SelectorSequence
(...) = (. SClass) . Filter
(.|) :: Namespace
-> ElementName
-> TypeSelector
(.|) = TypeSelector
data Namespace =
NAny
| Namespace Text
deriving (Data, Eq, Ord, Show)
pattern NEmpty :: Namespace
pattern NEmpty = Namespace ""
data ElementName =
EAny
| ElementName Text
deriving (Data, Eq, Ord, Show)
data TypeSelector = TypeSelector {
selectorNamespace :: Namespace,
elementName :: ElementName
} deriving (Data, Eq, Ord, Show)
data AttributeName = AttributeName {
attributeNamespace :: Namespace,
attributeName :: Text
} deriving (Data, Eq, Ord, Show)
type AttributeValue = Text
data AttributeCombinator =
Exact
| Include
| DashMatch
| PrefixMatch
| SuffixMatch
| SubstringMatch
deriving (Bounded, Data, Enum, Eq, Ord, Read, Show)
newtype Class = Class {
unClass :: Text
} deriving (Data, Eq, Ord, Show)
newtype Hash = Hash {
unHash :: Text
} deriving (Data, Eq, Ord, Show)
attributeCombinatorText :: AttributeCombinator
-> AttributeValue
attributeCombinatorText Exact = "="
attributeCombinatorText Include = "~="
attributeCombinatorText DashMatch = "|="
attributeCombinatorText PrefixMatch = "^="
attributeCombinatorText SuffixMatch = "$="
attributeCombinatorText SubstringMatch = "*="
pattern Universal :: TypeSelector
pattern Universal = TypeSelector NAny EAny
instance Semigroup SelectorSpecificity where
SelectorSpecificity a1 b1 c1 <> SelectorSpecificity a2 b2 c2 = SelectorSpecificity (a1+a2) (b1+b2) (c1+c2)
instance Semigroup SelectorGroup where
SelectorGroup g1 <> SelectorGroup g2 = SelectorGroup (g1 <> g2)
instance Semigroup Selector where
(<>) = combine def
instance Semigroup Namespace where
(<>) NAny = id
(<>) x = const x
instance Semigroup ElementName where
(<>) EAny = id
(<>) x = const x
instance Monoid SelectorSpecificity where
mempty = SelectorSpecificity 0 0 0
instance Monoid Namespace where
mempty = NAny
instance Monoid ElementName where
mempty = EAny
instance IsString Class where
fromString = toIdentifier Class
instance IsString Hash where
fromString = toIdentifier Hash
instance IsString Namespace where
fromString = toIdentifier Namespace
instance IsString ElementName where
fromString = toIdentifier ElementName
instance IsString AttributeName where
fromString = toIdentifier (AttributeName NAny)
instance IsString Attrib where
fromString = Exist . fromString
instance IsList SelectorGroup where
type Item SelectorGroup = Selector
fromList = SelectorGroup . fromList
toList (SelectorGroup ss) = toList ss
_textToPattern :: Text -> Pat
_textToPattern t = ViewP (AppE (ConE '(==)) (AppE (ConE 'pack) (LitE (StringL (unpack t))))) (_constantP 'True)
_constantP :: Name -> Pat
_constantP = flip ConP []
instance ToCssSelector SelectorGroup where
toCssSelector (SelectorGroup g) = intercalate " , " (map toCssSelector (toList g))
toSelectorGroup = id
specificity' (SelectorGroup g) = foldMap specificity' g
toPattern (SelectorGroup g) = ConP 'SelectorGroup [go g]
where go (x :| xs) = ConP '(:|) [toPattern x, ListP (map toPattern xs)]
normalize (SelectorGroup g) = SelectorGroup (Data.List.NonEmpty.sort (normalize <$> g))
instance ToCssSelector Class where
toCssSelector = cons '.' . encodeIdentifier . unClass
toSelectorGroup = toSelectorGroup . SClass
specificity' = const (SelectorSpecificity 0 1 0)
toPattern (Class c) = ConP 'Class [_textToPattern c]
instance ToCssSelector Attrib where
toCssSelector (Exist name) = "[" <> toCssSelector name <> "]"
toCssSelector (Attrib name op val) = "[" <> toCssSelector name <> attributeCombinatorText op <> encodeText '"' val <> "]"
toSelectorGroup = toSelectorGroup . SAttrib
specificity' = const (SelectorSpecificity 0 1 0)
toPattern (Exist name) = ConP 'Exist [toPattern name]
toPattern (Attrib name op val) = ConP 'Attrib [toPattern name, _constantP (go op), _textToPattern val]
where go Exact = 'Exact
go Include = 'Include
go DashMatch = 'DashMatch
go PrefixMatch = 'PrefixMatch
go SuffixMatch = 'SuffixMatch
go SubstringMatch = 'SubstringMatch
instance ToCssSelector AttributeName where
toCssSelector (AttributeName NAny e) = encodeIdentifier e
toCssSelector (AttributeName n e) = toCssSelector n <> "|" <> encodeIdentifier e
toSelectorGroup = toSelectorGroup . Exist
specificity' = mempty
toPattern (AttributeName n a) = ConP 'AttributeName [toPattern n, _textToPattern a]
instance ToCssSelector Hash where
toCssSelector = cons '#' . encodeIdentifier . unHash
toSelectorGroup = toSelectorGroup . SHash
specificity' = const (SelectorSpecificity 1 0 0)
toPattern (Hash h) = ConP 'Hash [_textToPattern h]
instance ToCssSelector Namespace where
toCssSelector NAny = "*"
toCssSelector (Namespace t) = encodeIdentifier t
toSelectorGroup = toSelectorGroup . flip TypeSelector EAny
specificity' = mempty
toPattern NAny = _constantP 'NAny
toPattern NEmpty = _constantP 'NEmpty
toPattern (Namespace t) = ConP 'Namespace [_textToPattern t]
instance ToCssSelector SelectorSequence where
toCssSelector (SimpleSelector s) = toCssSelector s
toCssSelector (Filter s f) = toCssSelector s <> toCssSelector f
toSelectorGroup = toSelectorGroup . Selector
specificity' (SimpleSelector s) = specificity' s
specificity' (Filter s f) = specificity' s <> specificity' f
toPattern (SimpleSelector s) = ConP 'SimpleSelector [toPattern s]
toPattern (Filter s f) = ConP 'Filter [toPattern s, toPattern f]
normalize = flip go []
where go (Filter s f) = go s . (normalize f:)
go (SimpleSelector s) = addFilters (SimpleSelector (normalize s)) . sort
instance ToCssSelector TypeSelector where
toCssSelector (TypeSelector NAny e) = toCssSelector e
toCssSelector (TypeSelector n e) = toCssSelector n <> "|" <> toCssSelector e
toSelectorGroup = toSelectorGroup . SimpleSelector
specificity' (TypeSelector _ e) = specificity' e
toPattern Universal = _constantP 'Universal
toPattern (TypeSelector n t) = ConP 'TypeSelector [toPattern n, toPattern t]
instance ToCssSelector ElementName where
toCssSelector EAny = "*"
toCssSelector (ElementName e) = encodeIdentifier e
toSelectorGroup = toSelectorGroup . TypeSelector NAny
specificity' EAny = mempty
specificity' (ElementName _) = SelectorSpecificity 0 0 1
toPattern EAny = _constantP 'EAny
toPattern (ElementName e) = ConP 'ElementName [_textToPattern e]
instance ToCssSelector SelectorFilter where
toCssSelector (SHash h) = toCssSelector h
toCssSelector (SClass c) = toCssSelector c
toCssSelector (SAttrib a) = toCssSelector a
toSelectorGroup = toSelectorGroup . Filter (SimpleSelector Universal)
specificity' (SHash h) = specificity' h
specificity' (SClass c) = specificity' c
specificity' (SAttrib a) = specificity' a
toPattern (SHash h) = ConP 'SHash [toPattern h]
toPattern (SClass c) = ConP 'SClass [toPattern c]
toPattern (SAttrib a) = ConP 'SAttrib [toPattern a]
instance ToCssSelector Selector where
toCssSelector (Selector s) = toCssSelector s
toCssSelector (Combined s1 c s2) = toCssSelector s1 <> combinatorText c <> toCssSelector s2
toSelectorGroup = toSelectorGroup . SelectorGroup . pure
specificity' (Selector s) = specificity' s
specificity' (Combined s1 _ s2) = specificity' s1 <> specificity' s2
toPattern (Selector s) = ConP 'Selector [toPattern s]
toPattern (Combined s1 c s2) = ConP 'Combined [toPattern s1, _constantP (go c), toPattern s2]
where go Descendant = 'Descendant
go Child = 'Child
go DirectlyPreceded = 'DirectlyPreceded
go Preceded = 'Preceded
normalize (Selector s) = Selector (normalize s)
normalize (Combined s1 c s2) = Combined (normalize s1) c (normalize s2)
instance Eq SelectorSpecificity where
(==) = on (==) specificityValue
instance Ord SelectorSpecificity where
compare = comparing specificityValue
instance Default SelectorGroup where
def = SelectorGroup (pure def)
instance Default Selector where
def = Selector def
instance Default SelectorSequence where
def = SimpleSelector def
instance Default TypeSelector where
def = Universal
instance Default SelectorSpecificity where
def = mempty
instance Default Namespace where
def = NAny
instance Default ElementName where
def = EAny
instance Default SelectorCombinator where
def = Descendant
instance Default AttributeCombinator where
def = Exact
_apply :: Name -> [Q Exp] -> Q Exp
_apply = foldl appE . conE
instance Lift SelectorGroup where
lift (SelectorGroup sg) = _apply 'SelectorGroup [liftNe sg]
where liftNe (a :| as) = _apply '(:|) [lift a, lift as]
instance Lift Selector
instance Lift SelectorCombinator
instance Lift SelectorSequence
instance Lift SelectorFilter
instance Lift Attrib
_cssToMarkup :: ToCssSelector a => a -> Markup
_cssToMarkup = text . toCssSelector
instance ToMarkup SelectorGroup where
toMarkup = _cssToMarkup
instance ToMarkup Selector where
toMarkup = _cssToMarkup
instance ToMarkup SelectorSequence where
toMarkup = _cssToMarkup
instance ToMarkup SelectorFilter where
toMarkup = _cssToMarkup
instance ToMarkup Attrib where
toMarkup = _cssToMarkup
_cssToJavascript :: ToCssSelector a => a -> Javascript
_cssToJavascript = toJavascript . toCssSelector
_cssToJson :: ToCssSelector a => a -> Value
_cssToJson = String . toCssSelector
instance ToJavascript SelectorGroup where
toJavascript = _cssToJavascript
instance ToJavascript Selector where
toJavascript = _cssToJavascript
instance ToJavascript SelectorSequence where
toJavascript = _cssToJavascript
instance ToJavascript SelectorFilter where
toJavascript = _cssToJavascript
instance ToJavascript Attrib where
toJavascript = _cssToJavascript
instance ToJSON SelectorGroup where
toJSON = _cssToJson
instance ToJSON Selector where
toJSON = _cssToJson
instance ToJSON SelectorSequence where
toJSON = _cssToJson
instance ToJSON SelectorFilter where
toJSON = _cssToJson
instance ToJSON Attrib where
toJSON = _cssToJson
_arbitraryIdent :: Gen Text
_arbitraryIdent = pack <$> listOf1 arbitrary
instance Arbitrary Hash where
arbitrary = Hash <$> _arbitraryIdent
instance Arbitrary Class where
arbitrary = Class <$> _arbitraryIdent
instance Arbitrary Namespace where
arbitrary = frequency [(3, return NAny), (1, Namespace <$> _arbitraryIdent)]
instance Arbitrary ElementName where
arbitrary = frequency [(1, return EAny), (3, ElementName <$> _arbitraryIdent)]
instance Arbitrary TypeSelector where
arbitrary = TypeSelector <$> arbitrary <*> arbitrary
instance Arbitrary SelectorSequence where
arbitrary = addFilters . SimpleSelector <$> arbitrary <*> listOf arbitrary
instance Arbitrary SelectorCombinator where
arbitrary = arbitraryBoundedEnum
instance Arbitrary AttributeCombinator where
arbitrary = arbitraryBoundedEnum
instance Arbitrary SelectorFilter where
arbitrary = oneof [SHash <$> arbitrary, SClass <$> arbitrary, SAttrib <$> arbitrary]
instance Arbitrary AttributeName where
arbitrary = AttributeName <$> arbitrary <*> _arbitraryIdent
instance Arbitrary Attrib where
arbitrary = oneof [Exist <$> arbitrary, Attrib <$> arbitrary <*> arbitrary <*> (pack <$> listOf arbitrary)]
instance Arbitrary SelectorGroup where
arbitrary = SelectorGroup <$> ((:|) <$> arbitrary <*> arbitrary)
instance Arbitrary Selector where
arbitrary = frequency [(3, Selector <$> arbitrary), (1, Combined <$> arbitrary <*> arbitrary <*> arbitrary) ]