{-# LANGUAGE OverloadedStrings , FlexibleInstances #-} ----------------------------------------------------------------------------- -- | -- Module : Hasmin.Types.Selector -- Copyright : (c) 2017 Cristian Adrián Ontivero -- License : BSD3 -- Stability : experimental -- Portability : unknown -- ----------------------------------------------------------------------------- module Hasmin.Types.Selector ( Selector(..) , SimpleSelector(..) , CompoundSelector , Combinator(..) , Sign(..) , AnPlusB(..) , Att(..) , specialPseudoElements ) where import Control.Applicative (liftA2) import Control.Monad.Reader (ask) import Data.Text (Text) import qualified Data.Text as T import Data.Text.Lazy.Builder (fromText, singleton, Builder) import Data.Monoid ((<>)) import Data.List.NonEmpty (NonEmpty((:|))) import qualified Data.List.NonEmpty as N import Hasmin.Config import Hasmin.Types.Class import Hasmin.Types.String import Hasmin.Utils {- class Specificity a where specificity :: a -> (Int, Int, Int, Int) addSpecificity :: (Int, Int, Int, Int) -> (Int, Int, Int, Int) -> (Int, Int, Int, Int) addSpecificity (a1,b1,c1,d1) (a2,b2,c2,d2) = (a1 + a2, b1 + b2, c1 + c2, d1 + d2) -} -- | Combinators are: whitespace, "greater-than sign" (U+003E, >), "plus sign" -- (U+002B, +) and "tilde" (U+007E, ~). White space may appear between a -- combinator and the simple selectors around it. Only the characters "space" -- (U+0020), "tab" (U+0009), "line feed" (U+000A), "carriage return" (U+000D), -- and "form feed" (U+000C) can occur in whitespace. Other space-like -- characters, such as "em-space" (U+2003) and "ideographic space" (U+3000), are -- never part of whitespace. data Combinator = Descendant -- ^ ' ' | Child -- ^ '>' | AdjacentSibling -- ^ '+' | GeneralSibling -- ^ '~' deriving (Eq, Show) instance ToText Combinator where toBuilder Descendant = " " toBuilder Child = ">" toBuilder AdjacentSibling = "+" toBuilder GeneralSibling = "~" -- An empty selector, containing no sequence of simple selectors and no -- pseudo-element, is an invalid selector. data Selector = Selector CompoundSelector [(Combinator, CompoundSelector)] deriving (Eq, Show) instance Ord Selector where -- Lexicographical order s1 <= s2 = toText s1 <= toText s2 instance ToText Selector where toBuilder (Selector cs ccss) = toBuilder cs <> mconcat (fmap build ccss) where build (comb, compSel) = toBuilder comb <> toBuilder compSel instance Minifiable Selector where minifyWith (Selector c xs) = do newC <- minifyWith c newCs <- (mapM . mapM) minifyWith xs pure $ Selector newC newCs {- instance Specificity Selector where specificity (Selector (x :| xs) ss) = case x of (Type _ _) -> f (0,0,0,1) xs `addSpecificity` g (0,0,0,0) ss (Universal _) -> f (0,0,0,0) xs `addSpecificity` g (0,0,0,0) ss where f = foldr (addSpecificity . specificity) g = foldr (addSpecificity . specificity . snd) -} -- | Called -- in CSS2.1, but in CSS Syntax Module Level 3. type CompoundSelector = NonEmpty SimpleSelector -- instance ToText a => ToText (NonEmpty a) where instance ToText CompoundSelector where toBuilder ns@(Universal{} :| xs) | length ns > 1 = mconcat $ fmap toBuilder xs toBuilder ns = mconcat $ N.toList (fmap toBuilder ns) instance Minifiable CompoundSelector where minifyWith (a :| xs) = liftA2 (:|) (minifyWith a) (mapM minifyWith xs) {- instance Specificity a => Specificity (NonEmpty a) where specificity = foldr (\x y -> specificity x `addSpecificity` y) (0,0,0,0) -} -- | Certain selectors support namespace prefixes. Namespace prefixes are -- declared with the @namespace rule. A type selector containing a namespace -- prefix that has not been previously declared for namespaced selectors is an -- invalid selector. type Namespace = Text type Element = Text -- e.g.: h1, em, body, ... type Identifier = Text -- Characters in Selectors can be escaped with a backslash according to the same -- escaping rules as CSS. [CSS21]. -- If a universal selector represented by * (i.e. without a namespace prefix) -- is not the only component of a sequence of simple selectors selectors or is -- immediately followed by a pseudo-element, then the * may be omitted and the -- universal selector's presence implied. -- | A simple selector is either a type selector, universal selector, attribute -- selector, class selector, ID selector, or pseudo-class. The first selector -- must be a type or universal selector. When none is specified, the universal -- selector is implied, i.e.: #myId is the same as *#myId -- mempty is used to denote an empty namespace data SimpleSelector = Type Namespace Element -- ^ e.g.: *, ns|*, h1, em, body | Universal Namespace -- ^ '*' | AttributeSel Att | ClassSel Identifier | IdSel Identifier | PseudoElem Identifier | PseudoClass Identifier | Lang (Either Text StringType) -- generic functional pseudo class | FunctionalPseudoClass Identifier Text -- :not() and :matches() fpc | FunctionalPseudoClass1 Identifier [CompoundSelector] -- :not( # ) -- :nth-of-type(), :nth-last-of-type(), :nth-column(), and -- :nth-of-last-column() fpc | FunctionalPseudoClass2 Identifier AnPlusB -- :nth-child(), nth-last-child() | FunctionalPseudoClass3 Identifier AnPlusB [CompoundSelector] deriving (Eq, Show) instance ToText SimpleSelector where toBuilder (Type n e) | T.null n = fromText e | otherwise = fromText n <> singleton '|' <> fromText e toBuilder (Universal n) | T.null n = singleton '*' | otherwise = fromText n <> fromText "|*" toBuilder (AttributeSel att) = singleton '[' <> toBuilder att <> singleton ']' toBuilder (ClassSel t) = singleton '.' <> fromText t toBuilder (IdSel t) = singleton '#' <> fromText t toBuilder (PseudoClass t) = singleton ':' <> fromText t toBuilder (PseudoElem t) | T.toCaseFold t `elem` specialPseudoElements = fromText ":" <> fromText t | otherwise = fromText "::" <> fromText t toBuilder (Lang x) = ":lang" <> singleton '(' <> toBuilder x <> singleton ')' toBuilder (FunctionalPseudoClass t x) = fromText t <> singleton '(' <> fromText x <> singleton ')' toBuilder (FunctionalPseudoClass1 t ss) = singleton ':' <> fromText t <> singleton '(' <> mconcatIntersperse toBuilder (singleton ',') ss <> singleton ')' toBuilder (FunctionalPseudoClass2 t x) = singleton ':' <> fromText t <> singleton '(' <> toBuilder x <> singleton ')' toBuilder (FunctionalPseudoClass3 t a xs) = singleton ':' <> fromText t <> singleton '(' <> toBuilder a <> f xs <> singleton ')' where f [] = mempty f (y:ys) = " of " <> toBuilder y <> mconcat (fmap (\z -> singleton ',' <> toBuilder z) ys) -- Pseudo-elements that support the old pseudo-element syntax of a single -- semicolon, as well as the new one of two semicolons. specialPseudoElements :: [Text] specialPseudoElements = fmap T.toCaseFold ["after", "before", "first-line", "first-letter"] instance Minifiable SimpleSelector where minifyWith a@(AttributeSel att) = do conf <- ask pure $ if shouldRemoveQuotes conf then AttributeSel (removeAttributeQuotes att) else a minifyWith a@(Lang x) = do conf <- ask pure $ if shouldRemoveQuotes conf then case x of Left _ -> a Right s -> Lang (removeQuotes s) else a minifyWith (FunctionalPseudoClass1 i cs) = FunctionalPseudoClass1 i <$> mapM minifyWith cs minifyWith (FunctionalPseudoClass2 i n) = FunctionalPseudoClass2 i <$> minifyWith n minifyWith (FunctionalPseudoClass3 i n cs) = FunctionalPseudoClass3 i <$> minifyWith n <*> pure cs minifyWith x = pure x data Sign = Plus | Minus deriving (Eq, Show) isPositive :: Maybe Sign -> Bool isPositive Nothing = True isPositive (Just Plus) = True isPositive (Just Minus) = False instance ToText Sign where toBuilder Plus = singleton '+' toBuilder Minus = singleton '-' -- We could maybe model the AB constructor with an Either, -- to make sure AB NoValue Nothing isn't possible (which is invalid). -- Also, modelling a BValue would cover all remaining cases, -- for example +6 vs 6, -0 vs 0 vs +0. data AnPlusB = Even | Odd | A (Maybe Sign) (Maybe Int) -- "sign n number", e.g. +3n, -2n, 1n. | B Int -- "sign number", e.g. +1, +2, 3. | AB (Maybe Sign) (Maybe Int) Int -- "sign n number sign number", e.g. 2n+1 deriving (Eq, Show) instance ToText AnPlusB where toBuilder Even = "even" toBuilder Odd = "odd" toBuilder (B b) = toBuilder b toBuilder (A ms mi) = an2Builder ms mi toBuilder (AB ms mi b) = an2Builder ms mi <> bSign b <> toBuilder b where bSign x | x < 0 = mempty | otherwise = singleton '+' -- Used to print "An" values an2Builder :: Maybe Sign -> Maybe Int -> Builder an2Builder ms mi = maybeToBuilder ms <> maybeToBuilder mi <> singleton 'n' where maybeToBuilder :: ToText a => Maybe a -> Builder maybeToBuilder = maybe mempty toBuilder instance Minifiable AnPlusB where minifyWith x = do conf <- ask pure $ if shouldMinifyMicrosyntax conf then minifyAnPlusB x else x minifyAnPlusB :: AnPlusB -> AnPlusB minifyAnPlusB Even = A Nothing (Just 2) minifyAnPlusB (A ms mi) = case mi of Just 0 -> B 0 _ -> uncurry A (minifyAN ms mi) minifyAnPlusB (AB _ (Just 0) b) = B b minifyAnPlusB (AB ms mi b) | isPositive ms && mi == Just 2 = if b == 1 || b < 0 && odd b then Odd else if even b && b <= 0 then minifyAnPlusB Even else AB ms' mi' b | otherwise = if b == 0 then A ms' mi' else AB ms' mi' b where (ms', mi') = minifyAN ms mi minifyAnPlusB x = x minifyAN :: Maybe Sign -> Maybe Int -> (Maybe Sign, Maybe Int) minifyAN (Just Plus) i = minifyAN Nothing i minifyAN s (Just 1) = minifyAN s Nothing minifyAN s i = (s, i) type AttId = Text type AttValue = Either Text StringType data Att = Attribute AttId | AttId :=: AttValue -- ^ \'=\' | AttId :~=: AttValue -- ^ \'~=\' | AttId :|=: AttValue -- ^ \'|=\' | AttId :^=: AttValue -- ^ \'^=\' | AttId :$=: AttValue -- ^ \'$=\' | AttId :*=: AttValue -- ^ \'*=\' deriving (Eq, Show) instance ToText Att where toBuilder (Attribute t) = fromText t toBuilder (attid :=: attval) = fromText attid <> singleton '=' <> toBuilder attval toBuilder (attid :~=: attval) = fromText attid <> fromText "~=" <> toBuilder attval toBuilder (attid :|=: attval) = fromText attid <> fromText "|=" <> toBuilder attval toBuilder (attid :^=: attval) = fromText attid <> fromText "^=" <> toBuilder attval toBuilder (attid :$=: attval) = fromText attid <> fromText "$=" <> toBuilder attval toBuilder (attid :*=: attval) = fromText attid <> fromText "*=" <> toBuilder attval removeAttributeQuotes :: Att -> Att removeAttributeQuotes (attId :=: val) = attId :=: either Left removeQuotes val removeAttributeQuotes (attId :~=: val) = attId :~=: either Left removeQuotes val removeAttributeQuotes (attId :|=: val) = attId :|=: either Left removeQuotes val removeAttributeQuotes (attId :^=: val) = attId :^=: either Left removeQuotes val removeAttributeQuotes (attId :$=: val) = attId :$=: either Left removeQuotes val removeAttributeQuotes (attId :*=: val) = attId :*=: either Left removeQuotes val removeAttributeQuotes a@Attribute{} = a