{-# LANGUAGE OverloadedStrings #-} module Data.CSS.Preprocessor.Conditions( ConditionalStyles(..), extractImports, resolveImports, resolve ) where import qualified Data.CSS.Preprocessor.Conditions.Expr as Query import Data.CSS.Syntax.StyleSheet import Data.CSS.Syntax.Selector import Data.CSS.Syntax.Tokens(Token(..)) import Data.CSS.Style (PropertyParser(..)) import Data.Text.Internal (Text(..)) import Data.Text (unpack) import Network.URI (URI(..), URIAuth(..), parseURI) import Data.List data ConditionalStyles p = ConditionalStyles { hostURL :: URI, mediaDocument :: String, rules :: [ConditionalRule p], propertyParser :: p } data ConditionalRule p = Priority Int | StyleRule' StyleRule | AtRule Text [Token] | External Query.Expr URI | Internal Query.Expr (ConditionalStyles p) addRule' :: ConditionalStyles p -> ConditionalRule p -> ConditionalStyles p addRule' self rule = self {rules = rule : rules self} hostUrlS :: ConditionalStyles p -> String hostUrlS = show . hostURL parseAtBlock :: StyleSheet t => t -> [Token] -> (t, [Token]) parseAtBlock self (LeftCurlyBracket:toks) = let (block, toks') = scanBlock toks in (parse' self block, toks') parseAtBlock self (_:toks) = parseAtBlock self toks parseAtBlock self [] = (self, []) instance PropertyParser p => StyleSheet (ConditionalStyles p) where setPriority x self = addRule' self $ Priority x addRule self rule = addRule' self $ StyleRule' rule addAtRule self "document" (Whitespace:toks) = addAtRule self "document" toks addAtRule self "document" (Comma:toks) = addAtRule self "document" toks addAtRule self "document" (Url match:toks) | unpack match == hostUrlS self = parseAtBlock self toks | otherwise = addAtRule self "document" toks addAtRule self "document" (Function "url-prefix":String match:RightParen:toks) | unpack match `isPrefixOf` hostUrlS self = parseAtBlock self toks | otherwise = addAtRule self "document" toks addAtRule self "document" (Function "domain":String match:RightParen:toks) | unpack match == domain || ('.':unpack match) `isSuffixOf` domain = parseAtBlock self toks | otherwise = addAtRule self "document" toks where domain | Just auth <- uriAuthority $ hostURL self = uriRegName auth | otherwise = "" addAtRule self "document" (Function "media-document":String match:RightParen:toks) | unpack match == mediaDocument self = parseAtBlock self toks | otherwise = addAtRule self "document" toks -- TODO Support regexp() conditions, requires new dependency addAtRule self "document" tokens = (self, skipAtRule tokens) addAtRule self "media" toks | (cond, LeftCurlyBracket:block) <- Query.parse LeftCurlyBracket toks = let (block', toks') = scanBlock block in (addRule' self $ Internal cond $ parse' self {rules = []} block', toks') addAtRule self "media" tokens = (self, skipAtRule tokens) addAtRule self "import" (Whitespace:toks) = addAtRule self "import" toks addAtRule self "import" (Url src:toks) = parseAtImport self src toks addAtRule self "import" (String src:toks) = parseAtImport self src toks addAtRule self "import" tokens = (self, skipAtRule tokens) addAtRule self "supports" toks = let (cond, toks') = break (== LeftCurlyBracket) toks in if evalSupports (propertyParser self) cond then parseAtBlock self toks' else (self, skipAtRule toks') addAtRule self rule tokens = let (block, rest) = scanBlock tokens in (addRule' self $ AtRule rule block, rest) -------- ---- @import/@media -------- parseAtImport :: PropertyParser p => ConditionalStyles p -> Text -> [Token] -> (ConditionalStyles p, [Token]) parseAtImport self src toks | (cond, Semicolon:toks') <- Query.parse Semicolon toks, Just uri <- parseURI $ unpack src = (addRule' self $ External cond uri, toks') parseAtImport self _ toks = (self, skipAtRule toks) extractImports :: (Text -> Query.Datum) -> (Token -> Query.Datum) -> ConditionalStyles p -> [URI] extractImports vars evalToken self = [uri | External cond uri <- rules self, Query.eval vars evalToken cond] resolveImports :: ConditionalStyles p -> [(URI, ConditionalStyles p)] -> ConditionalStyles p resolveImports self responses = self {rules = map resolveImport $ rules self} where resolveImport (External cond uri) | (body:_) <- [body | (uri', body) <- responses, uri' == uri] = Internal cond body resolveImport x = x resolve :: StyleSheet s => (Text -> Query.Datum) -> (Token -> Query.Datum) -> s -> ConditionalStyles p -> s resolve v t styles self = resolve' v t (reverse $ rules self) styles resolve' :: StyleSheet s => (Text -> Query.Datum) -> (Token -> Query.Datum) -> [ConditionalRule p] -> s -> s resolve' v t (Priority x:rules) styles = resolve' v t rules $ setPriority x styles resolve' v t (StyleRule' rule:rules) styles = resolve' v t rules $ addRule styles rule resolve' v t (AtRule name block:rules) styles = resolve' v t rules $ fst $ addAtRule styles name block resolve' v t (Internal cond block:rules) styles | Query.eval v t cond = resolve' v t rules $ resolve v t styles block resolve' v t (_:rules) styles = resolve' v t rules styles resolve' _ _ [] styles = styles -------- ---- @supports -------- evalSupports :: PropertyParser p => p -> [Token] -> Bool evalSupports self (Whitespace:toks) = evalSupports self toks evalSupports self (Ident "not":toks) = not $ evalSupports self toks evalSupports self (LeftParen:toks) = let (block, toks') = scanBlock toks in evalSupportsOp toks' self $ supportsProperty block self evalSupports self (Function "selector":toks) = let (block, toks') = scanBlock toks in evalSupportsOp toks' self $ supportsSelector block evalSupports _ _ = False evalSupportsOp :: PropertyParser p => [Token] -> p -> Bool -> Bool evalSupportsOp (Whitespace:toks) self right = evalSupportsOp toks self right evalSupportsOp (Ident "and":toks) self right = right && evalSupports self toks evalSupportsOp (Ident "or":toks) self right = right || evalSupports self toks evalSupportsOp [RightParen] _ ret = ret -- scanBlock captures closing paren evalSupportsOp [] _ ret = ret evalSupportsOp _ _ _ = False supportsProperty :: PropertyParser p => [Token] -> p -> Bool supportsProperty (Whitespace:toks) self = supportsProperty toks self supportsProperty toks@(Ident "not":_) self = evalSupports self toks -- Special case fallback supportsProperty (Ident key:toks) self | (Colon:value) <- skipSpace toks = -- "init"'s used to strip trailing RightParen shorthand self key (filter (/= Whitespace) $ init value) /= [] | skipSpace toks `elem` [[RightParen], []] = shorthand self key [Ident "initial"] /= [] | otherwise = False supportsProperty toks self = evalSupports self toks -- Fallback to parenthesized expression. supportsSelector :: [Token] -> Bool supportsSelector toks = let (sels, toks') = parseSelectors toks in sels /= [] && (toks' == [] || toks' == [RightParen])