purview-0.2.0.0: src/Style.hs
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE TemplateHaskell #-}
-- |
module Style
( style
, handleCSS
, parseLine
, parseCSS
, Brace (..)
)
where
import Language.Haskell.TH
import Language.Haskell.TH.Quote
import Data.Text (pack, unpack, replace)
import Data.Bits
import Data.List
import Component (Attributes ( Style ), Purview ( Attribute ))
-- thanks https://stackoverflow.com/questions/59399050/haskell-making-quasi-quoted-values-strict-evaluated-at-compile-time
{-|
Components styled with this QuasiQuoter will have a class added
to them and the CSS added to the stylesheet. Basic support is
provided for easily styling nested components and for pseudo
selectors.
__Examples:__
Styling a button:
@
blue = [style|
background-color: blue;
|]
blueButton = blue $ button []
@
Styling a list with a pseudo selector to get the right cursor on hover:
@
listStyle = [style|
width: 250px;
li {
padding: 25px;
&:hover {
cursor: pointer;
}
}
|]
list = listStyle $ ul [ li [ text "an item" ] ]
@
-}
style :: QuasiQuoter
style = QuasiQuoter
{ quoteDec = error "quoteDec not implemented"
, quoteType = error "quoteType not implemented"
, quotePat = error "quotePat not implemented"
, quoteExp = style'
}
{-
This is all kind of a mess and could definitely use some love. I don't think
it's quite doing what is wanted, looking at the dumped splices, as ideally
that would be a list of Attributes instead of a call to a function. I ran
into implementing Lift for Purview -> Purview, for the attributes, and this
at least works.
If it catches anyone's eye by all means rewrite it
-}
clean :: String -> String
clean [] = ""
clean ('\n':rest) = clean rest
clean (';':rest) = ';':clean (dropWhile (flip elem [' ', '\n']) rest)
clean (c:rest) = c:clean rest
data Brace = Open | Close | None
deriving (Show, Eq)
-- Takes a chunk of CSS and returns a line, the remainder to parse, and if
-- it contains an opening brace
parseLine' :: (String, String) -> (Brace, String, String)
parseLine' (line, '\n':rest) = parseLine' (line, rest)
parseLine' (line, ';':rest) = (None, line <> [';'], rest)
parseLine' (line, '{':rest) = (Open, line <> ['{'], rest)
parseLine' (line, '}':rest) = (Close, line <> ['{'], rest)
parseLine' (line, c:rest) = parseLine' (line <> [c], rest)
parseLine' (line, "") = (None, line, "")
preParseLine :: String -> String
preParseLine = dropWhile (flip elem [' ', '\n'])
parseLine css =
let cleaned = preParseLine css
in parseLine' ("", cleaned)
parseNewLevel = takeWhile (/= '{')
parseCSS :: [String] -> String -> [(String, String)]
parseCSS _ "" = []
parseCSS path css =
let (brace, line, remaining) = parseLine css
in case brace of
Open -> parseCSS (path <> [parseNewLevel line]) remaining
Close -> parseCSS (take (length path - 1) path) remaining
None ->
if null line
then []
else (concat path, line) : parseCSS path remaining
joinOnClass :: [(String, String)] -> (String, String)
joinOnClass cs@((name, _):_) = (name, concatMap snd cs)
joinOnClass [] = ("", "")
handleCSS :: String -> [(String, String)]
handleCSS css =
fmap joinOnClass $ groupBy (\a b -> fst a == fst b) $ sortOn fst $ parseCSS [] css
-- for handling top level pseudos
handlePseudo hash ('&':newClass) =
hash <> newClass
-- for the nested pseudos
handlePseudo hash newClass =
hash <> " " <> (unpack $ replace " &:" ":" (pack newClass))
combineClasses hash newClass =
if '&' `elem` newClass
then handlePseudo hash newClass
else hash <> " " <> newClass
toAttributes :: String -> String -> (Purview event m -> Purview event m)
toAttributes hashed css =
let ((_, baseCss):rest) = handleCSS css
in foldr
(\(newClass, newCss) acc ->
acc . Attribute (Style (combineClasses hashed newClass, newCss))
)
(Attribute (Style (hashed, baseCss))) rest
style' :: String -> Q Exp
style' css =
-- pretty funny, css needs a leading character (not number)
let
hashed = 'p' : show (hash css)
in [| toAttributes hashed css |]
-- snagged from https://stackoverflow.com/a/9263004/1361890
hash :: String -> Int
hash = foldl' (\h c -> 33 * h `xor` fromEnum c) 5381