{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
module Graphics.SvgTree.CssTypes
( CssSelector( .. )
, CssSelectorRule
, CssRule( .. )
, CssDescriptor( .. )
, CssDeclaration( .. )
, CssElement( .. )
, CssMatcheable( .. )
, CssContext
, Dpi
, Number( .. )
, serializeNumber
, findMatchingDeclarations
, toUserUnit
, mapNumber
, tserialize
) where
#if !MIN_VERSION_base(4,8,0)
import Data.Monoid (mconcat)
#endif
import Data.List (intersperse)
import Data.Monoid ((<>))
import qualified Data.Text as T
import qualified Data.Text.Lazy.Builder as TB
import Text.Printf
import Codec.Picture (PixelRGBA8 (..))
import Graphics.SvgTree.Misc
type Dpi = Int
class TextBuildable a where
tserialize :: a -> TB.Builder
data CssDescriptor
= OfClass T.Text
| OfName T.Text
| OfId T.Text
| OfPseudoClass T.Text
| AnyElem
| WithAttrib T.Text T.Text
deriving (Eq, Show)
instance TextBuildable CssDescriptor where
tserialize d = case d of
OfClass c -> si '.' <> ft c
OfName n -> ft n
OfId i -> si '#' <> ft i
OfPseudoClass c -> si '#' <> ft c
AnyElem -> si '*'
WithAttrib a b -> mconcat [si '[', ft a, si '=', ft b, si ']']
where
ft = TB.fromText
si = TB.singleton
data CssSelector
= Nearby
| DirectChildren
| AllOf [CssDescriptor]
deriving (Eq, Show)
instance TextBuildable CssSelector where
tserialize s = case s of
Nearby -> si '+'
DirectChildren -> si '>'
AllOf lst -> mconcat $ map tserialize lst
where
si = TB.singleton
type CssSelectorRule = [CssSelector]
data CssRule = CssRule
{
cssRuleSelector :: ![CssSelectorRule]
, cssDeclarations :: ![CssDeclaration]
}
deriving (Eq, Show)
instance TextBuildable CssRule where
tserialize (CssRule selectors decl) =
mconcat tselectors
<> ft " {\n"
<> mconcat (fmap tserializeDecl decl)
<> ft "}\n"
where
ft = TB.fromText
tserializeDecl d = ft " " <> tserialize d <> ft ";\n"
tselector =
mconcat . intersperse (ft " ") . fmap tserialize
tselectors =
intersperse (ft ",\n") $ fmap tselector selectors
class CssMatcheable a where
cssIdOf :: a -> Maybe T.Text
cssClassOf :: a -> [T.Text]
cssNameOf :: a -> T.Text
cssAttribOf :: a -> T.Text -> Maybe T.Text
type CssContext a = [[a]]
isDescribedBy :: CssMatcheable a
=> a -> [CssDescriptor] -> Bool
isDescribedBy e = all tryMatch
where
tryMatch (OfClass t) = t `elem` cssClassOf e
tryMatch (OfId i) = cssIdOf e == Just i
tryMatch (OfName n) = cssNameOf e == n
tryMatch (OfPseudoClass _) = False
tryMatch (WithAttrib a v) = cssAttribOf e a == Just v
tryMatch AnyElem = True
isMatching :: CssMatcheable a
=> CssContext a -> [CssSelector] -> Bool
isMatching = go where
go _ [] = True
go [] _ = False
go ((_ : near):upper) (Nearby : rest) = go (near:upper) rest
go ((e:_):upper) (DirectChildren:AllOf descr:rest)
| isDescribedBy e descr = go upper rest
go _ (DirectChildren:_) = False
go ((e:_):upper) selectors@(AllOf descr : rest)
| isDescribedBy e descr = go upper rest
| otherwise = go upper selectors
go (_:upper) selector = go upper selector
findMatchingDeclarations :: CssMatcheable a
=> [CssRule] -> CssContext a -> [CssDeclaration]
findMatchingDeclarations rules context =
concat [cssDeclarations rule
| rule <- rules
, selector <- cssRuleSelector rule
, isMatching context $ reverse selector ]
data CssDeclaration = CssDeclaration
{
_cssDeclarationProperty :: T.Text
, _cssDecarationlValues :: [[CssElement]]
}
deriving (Eq, Show)
instance TextBuildable CssDeclaration where
tserialize (CssDeclaration n elems) =
mconcat $ ft n : ft ": " : intersperse (si ' ') finalElems
where
finalElems = map tserialize (concat elems)
ft = TB.fromText
si = TB.singleton
data Number
= Num Double
| Px Double
| Em Double
| Percent Double
| Pc Double
| Mm Double
| Cm Double
| Point Double
| Inches Double
deriving (Eq, Show)
mapNumber :: (Double -> Double) -> Number -> Number
mapNumber f nu = case nu of
Num n -> Num $ f n
Px n -> Px $ f n
Em n -> Em $ f n
Percent n -> Percent $ f n
Pc n -> Pc $ f n
Mm n -> Mm $ f n
Cm n -> Cm $ f n
Point n -> Point $ f n
Inches n -> Inches $ f n
serializeNumber :: Number -> String
serializeNumber n = case n of
Num c -> ppD c
Px c -> printf "%spx" (ppD c)
Em cc -> printf "%sem" (ppD cc)
Percent p -> printf "%d%%" (floor $ 100 * p :: Int)
Pc p -> printf "%spc" (ppD p)
Mm m -> printf "%smm" (ppD m)
Cm c -> printf "%scm" (ppD c)
Point p -> printf "%spt" (ppD p)
Inches i -> printf "%sin" (ppD i)
instance TextBuildable Number where
tserialize = TB.fromText . T.pack . serializeNumber
data CssElement
= CssIdent !T.Text
| CssString !T.Text
| CssReference !T.Text
| CssNumber !Number
| CssColor !PixelRGBA8
| CssFunction !T.Text ![CssElement]
| CssOpComa
| CssOpSlash
deriving (Eq, Show)
instance TextBuildable CssElement where
tserialize e = case e of
CssIdent n -> ft n
CssString s -> si '"' <> ft s <> si '"'
CssReference r -> si '#' <> ft r
CssNumber n -> tserialize n
CssColor (PixelRGBA8 r g b _) ->
ft . T.pack $ printf "#%02X%02X%02X" r g b
CssFunction t els -> mconcat $ ft t : si '(' : args ++ [si ')']
where args = intersperse (ft ", ") (map tserialize els)
CssOpComa -> si ','
CssOpSlash -> si '/'
where
ft = TB.fromText
si = TB.singleton
toUserUnit :: Dpi -> Number -> Number
toUserUnit dpi = go where
go nu = case nu of
Num _ -> nu
Px p -> go $ Num p
Em _ -> nu
Percent _ -> nu
Pc n -> go . Inches $ (12 * n) / 72
Inches n -> Num $ n * fromIntegral dpi
Mm n -> go . Inches $ n / 25.4
Cm n -> go . Inches $ n / 2.54
Point n -> go . Inches $ n / 72