{-# LANGUAGE OverloadedStrings #-}
module Clay.Render
( Config (..)
, pretty
, compact
, render
, htmlInline
, putCss
, renderWith
, renderSelector
, withBanner
)
where
import Control.Applicative
import Control.Monad.Writer
import Data.Foldable (foldMap)
import Data.List (sort)
import Data.Maybe
import Data.Text (Text, pack)
import Data.Text.Lazy.Builder
import Prelude hiding ((**))
import qualified Data.Text as Text
import qualified Data.Text.Lazy as Lazy
import qualified Data.Text.Lazy.IO as Lazy
import Clay.Common (browsers)
import Clay.Property
import Clay.Selector
import Clay.Stylesheet hiding (Child, query, rule)
import qualified Clay.Stylesheet as Rule
data Config = Config
{ indentation :: Builder
, newline :: Builder
, sep :: Builder
, lbrace :: Builder
, rbrace :: Builder
, finalSemicolon :: Bool
, warn :: Bool
, align :: Bool
, banner :: Bool
, comments :: Bool
}
pretty :: Config
pretty = Config
{ indentation = " "
, newline = "\n"
, sep = " "
, lbrace = "{"
, rbrace = "}"
, finalSemicolon = True
, warn = True
, align = True
, banner = True
, comments = True
}
compact :: Config
compact = Config
{ indentation = ""
, newline = ""
, sep = ""
, lbrace = "{"
, rbrace = "}"
, finalSemicolon = False
, warn = False
, align = False
, banner = False
, comments = False
}
htmlInline :: Config
htmlInline = Config
{ indentation = ""
, newline = ""
, sep = ""
, lbrace = ""
, rbrace = ""
, finalSemicolon = False
, warn = False
, align = False
, banner = False
, comments = False
}
putCss :: Css -> IO ()
putCss = Lazy.putStr . render
render :: Css -> Lazy.Text
render = renderWith pretty []
renderWith :: Config -> [App] -> Css -> Lazy.Text
renderWith cfg top
= renderBanner cfg
. toLazyText
. rules cfg top
. runS
renderSelector :: Selector -> Lazy.Text
renderSelector = toLazyText . selector compact
renderBanner :: Config -> Lazy.Text -> Lazy.Text
renderBanner cfg
| banner cfg = withBanner
| otherwise = id
withBanner :: Lazy.Text -> Lazy.Text
withBanner = (<> "\n/* Generated with Clay, http://fvisser.nl/clay */")
kframe :: Config -> Keyframes -> Builder
kframe cfg (Keyframes ident xs) =
foldMap
( \(browser, _) ->
mconcat [ "@" <> fromText browser <> "keyframes "
, fromText ident
, newline cfg
, lbrace cfg
, newline cfg
, foldMap (frame cfg) xs
, rbrace cfg
, newline cfg
, newline cfg
]
)
(unPrefixed browsers)
frame :: Config -> (Double, [Rule]) -> Builder
frame cfg (p, rs) =
mconcat
[ fromText (pack (show p))
, "% "
, rules cfg [] rs
]
query :: Config -> MediaQuery -> [App] -> [Rule] -> Builder
query cfg q sel rs =
mconcat
[ mediaQuery q
, newline cfg
, lbrace cfg
, newline cfg
, rules cfg sel rs
, rbrace cfg
, newline cfg
]
mediaQuery :: MediaQuery -> Builder
mediaQuery (MediaQuery no ty fs) = mconcat
[ "@media "
, case no of
Nothing -> ""
Just Not -> "not "
Just Only -> "only "
, mediaType ty
, mconcat ((" and " <>) . feature <$> fs)
]
mediaType :: MediaType -> Builder
mediaType (MediaType (Value v)) = fromText (plain v)
feature :: Feature -> Builder
feature (Feature k mv) =
case mv of
Nothing -> fromText k
Just (Value v) -> mconcat
[ "(" , fromText k , ": " , fromText (plain v) , ")" ]
face :: Config -> [Rule] -> Builder
face cfg rs = mconcat
[ "@font-face"
, rules cfg [] rs
]
rules :: Config -> [App] -> [Rule] -> Builder
rules cfg sel rs = mconcat
[ rule cfg sel (mapMaybe property rs)
, newline cfg
, imp cfg `foldMap` mapMaybe imports rs
, kframe cfg `foldMap` mapMaybe kframes rs
, face cfg `foldMap` mapMaybe faces rs
, (\(a, b) -> rules cfg (a : sel) b) `foldMap` mapMaybe nested rs
, (\(a, b) -> query cfg a sel b) `foldMap` mapMaybe queries rs
]
where property (Property m k v) = Just (m, k, v)
property _ = Nothing
nested (Nested a ns ) = Just (a, ns)
nested _ = Nothing
queries (Query q ns ) = Just (q, ns)
queries _ = Nothing
kframes (Keyframe fs ) = Just fs;
kframes _ = Nothing
faces (Face ns ) = Just ns
faces _ = Nothing
imports (Import i ) = Just i
imports _ = Nothing
imp :: Config -> Text -> Builder
imp cfg t =
mconcat
[ "@import url("
, fromText t
, ");"
, newline cfg ]
type KeyVal = ([Modifier], Key (), Value)
rule :: Config -> [App] -> [KeyVal] -> Builder
rule _ _ [] = mempty
rule cfg sel props =
let xs = collect =<< props
in mconcat
[ selector cfg (merger sel)
, newline cfg
, lbrace cfg
, newline cfg
, properties cfg xs
, rbrace cfg
, newline cfg
]
merger :: [App] -> Selector
merger [] = ""
merger (x:xs) =
case x of
Rule.Child s -> case xs of [] -> s; _ -> merger xs |> s
Sub s -> case xs of [] -> s; _ -> merger xs ** s
Root s -> s ** merger xs
Pop i -> merger (drop i (x:xs))
Self f -> case xs of [] -> star `with` f; _ -> merger xs `with` f
data Representation
= Warning Text
| KeyValRep [Modifier] Text Text
deriving (Show)
keys :: [Representation] -> [Text]
keys = mapMaybe f
where
f (KeyValRep _ k _) = Just k
f _ = Nothing
collect :: KeyVal -> [Representation]
collect (ms, Key ky, Value vl) = case (ky, vl) of
( Plain k , Plain v ) -> [prop k v]
( Prefixed ks , Plain v ) -> flip map ks $ \(p, k) -> prop (p <> k) v
( Plain k , Prefixed vs ) -> flip map vs $ \(p, v) -> prop k (p <> v)
( Prefixed ks , Prefixed vs ) -> flip map ks $ \(p, k) -> (Warning (p <> k) `maybe` (prop (p <> k) . mappend p)) (lookup p vs)
where prop k v = KeyValRep ms k v
properties :: Config -> [Representation] -> Builder
properties cfg xs =
let width = 1 + maximum (Text.length <$> keys xs)
ind = indentation cfg
new = newline cfg
finalSemi = if finalSemicolon cfg then ";" else ""
in (<> new) $ (<> finalSemi) $ intercalate (";" <> new) $ flip map xs $ \p ->
case p of
Warning w -> if warn cfg
then ind <> "/* no value for " <> fromText w <> " */" <> new
else mempty
KeyValRep ms k v ->
let pad = if align cfg
then fromText (Text.replicate (width - Text.length k) " ")
else ""
imptant = maybe "" ((" " <>) . fromText) . foldMap _Important $ ms
comm = case (foldMap _Comment ms, comments cfg) of
(Just c, True) -> " /* " <> fromText (unCommentText c) <> " */"
_ -> mempty
in mconcat [ind, fromText k, pad, ":", sep cfg, fromText v, imptant, comm]
selector :: Config -> Selector -> Builder
selector Config { lbrace = "", rbrace = "" } = rec
where rec _ = ""
selector cfg = intercalate ("," <> newline cfg) . rec
where rec (In (SelectorF (Refinement ft) p)) = (<> foldMap predicate (sort ft)) <$>
case p of
Star -> if null ft then ["*"] else [""]
Elem t -> [fromText t]
Child a b -> ins " > " <$> rec a <*> rec b
Deep a b -> ins " " <$> rec a <*> rec b
Adjacent a b -> ins " + " <$> rec a <*> rec b
Combined a b -> rec a ++ rec b
where ins s a b = a <> s <> b
predicate :: Predicate -> Builder
predicate ft = mconcat $
case ft of
Id a -> [ "#" , fromText a ]
Class a -> [ "." , fromText a ]
Attr a -> [ "[" , fromText a, "]" ]
AttrVal a v -> [ "[" , fromText a, "='", fromText v, "']" ]
AttrBegins a v -> [ "[" , fromText a, "^='", fromText v, "']" ]
AttrEnds a v -> [ "[" , fromText a, "$='", fromText v, "']" ]
AttrContains a v -> [ "[" , fromText a, "*='", fromText v, "']" ]
AttrSpace a v -> [ "[" , fromText a, "~='", fromText v, "']" ]
AttrHyph a v -> [ "[" , fromText a, "|='", fromText v, "']" ]
Pseudo a -> [ ":" , fromText a ]
PseudoFunc a p -> [ ":" , fromText a, "(", intercalate "," (map fromText p), ")" ]
PseudoElem a -> [ "::", fromText a ]