module Hasmin.Types.Declaration (
Declaration(..)
, clean
) where
import Control.Monad.Reader (Reader, ask)
import Control.Arrow (first)
import Control.Monad ((>=>))
import Data.Map.Strict (Map)
import Data.Monoid ((<>))
import Data.Maybe (fromMaybe)
import Data.List (find, delete, minimumBy, (\\))
import Data.Text (Text)
import Data.Text.Lazy.Builder (singleton, fromText)
import qualified Data.Map.Strict as Map
import qualified Data.Text as T
import Text.PrettyPrint.Mainland (Pretty, ppr, strictText, colon, (<+>))
import Hasmin.Config
import Hasmin.Properties
import Hasmin.Types.BgSize
import Hasmin.Types.Class
import Hasmin.Types.Dimension
import Hasmin.Types.Numeric
import Hasmin.Types.PercentageLength
import Hasmin.Types.Position
import Hasmin.Types.TransformFunction
import Hasmin.Types.Value
import Hasmin.Utils
data Declaration = Declaration { propertyName :: Text
, valueList :: Values
, isImportant :: Bool
, hasIEhack :: Bool
} deriving (Eq, Show)
instance Pretty Declaration where
ppr (Declaration p v i h) = strictText p <> colon <+> ppr v <> imp
<> (if h then " \\9" else mempty)
where imp | i = strictText " !important"
| otherwise = mempty
instance ToText Declaration where
toBuilder (Declaration p vs i h) = fromText p <> singleton ':'
<> toBuilder vs <> imp <> (if h then " \\9" else mempty)
where imp | i = "!important"
| otherwise = mempty
instance Minifiable Declaration where
minifyWith d@(Declaration p vs _ _) = do
minifiedValues <- minifyWith vs
conf <- ask
let name = case letterCase conf of
Lowercase -> T.toLower p
Original -> p
newDec = d {propertyName = name, valueList = minifiedValues }
case Map.lookup (T.toCaseFold p) propertyOptimizations of
Just f -> propertyTraits newDec >>= f
Nothing -> propertyTraits newDec
propertyTraits :: Declaration -> Reader Config Declaration
propertyTraits d@(Declaration p _ _ _) = do
conf <- ask
pure $ if shouldUsePropertyTraits conf
then case Map.lookup (T.toCaseFold p) propertiesTraits of
Just (vals, inhs) -> minifyDec d vals inhs
Nothing -> d
else d
propertyOptimizations :: Map Text (Declaration -> Reader Config Declaration)
propertyOptimizations = Map.fromList
[("transform", combineTransformFunctions)
,("-webkit-transform", combineTransformFunctions)
,("-moz-transform", combineTransformFunctions)
,("font-family", optimizeValues optimizeFontFamily)
,("font-weight", fontWeightOptimizer)
,("background-size", nullPercentageToLength)
,("width", nullPercentageToLength)
,("perspective-origin", nullPercentageToLength)
,("-o-perspective-origin", nullPercentageToLength)
,("-moz-perspective-origin", nullPercentageToLength)
,("-webkit-perspective-origin", nullPercentageToLength)
,("background-position", nullPercentageToLength)
,("top", nullPercentageToLength)
,("right", nullPercentageToLength)
,("bottom", nullPercentageToLength)
,("left", nullPercentageToLength)
,("border-color", pure . reduceTRBL)
,("border-width", pure . reduceTRBL)
,("border-style", pure . reduceTRBL)
,("padding", nullPercentageToLength >=> pure . reduceTRBL)
,("padding-top", nullPercentageToLength)
,("padding-right", nullPercentageToLength)
,("padding-bottom", nullPercentageToLength)
,("padding-left", nullPercentageToLength)
,("margin-top", nullPercentageToLength)
,("margin-right", nullPercentageToLength)
,("margin-bottom", nullPercentageToLength)
,("margin-left", nullPercentageToLength)
,("margin", nullPercentageToLength >=> pure . reduceTRBL)
,("grid-row-gap", nullPercentageToLength)
,("grid-column-gap", nullPercentageToLength)
,("line-height", nullPercentageToLength)
,("min-height", nullPercentageToLength)
,("max-width", nullPercentageToLength)
,("min-width", nullPercentageToLength)
,("text-indent", nullPercentageToLength)
,("text-transform", nullPercentageToLength)
,("font-size", nullPercentageToLength)
,("word-spacing", nullPercentageToLength >=> replaceWithZero "normal")
,("vertical-align", nullPercentageToLength >=> replaceWithZero "baseline")
,("transform-origin", optimizeTransformOrigin >=> nullPercentageToLength)
,("-o-transform-origin", optimizeTransformOrigin >=> nullPercentageToLength)
,("-moz-transform-origin", optimizeTransformOrigin >=> nullPercentageToLength)
,("-ms-transform-origin", optimizeTransformOrigin >=> nullPercentageToLength)
,("-webkit-transform-origin", optimizeTransformOrigin >=> nullPercentageToLength)
]
optimizeValues :: (Value -> Reader Config Value)
-> Declaration -> Reader Config Declaration
optimizeValues f d@(Declaration _ vs _ _) = do
newV <- mapValues f vs
pure $ d {valueList = newV }
nullPercentageToLength :: Declaration -> Reader Config Declaration
nullPercentageToLength d = do
conf <- ask
if shouldConvertNullPercentages conf
then optimizeValues f d
else pure d
where f :: Value -> Reader Config Value
f (PositionV p@(Position _ a _ b)) = pure . PositionV $
let stripPercentage Nothing = Nothing
stripPercentage (Just x) = if isZero x
then l0
else Just x
in p { offset1 = stripPercentage a, offset2 = stripPercentage b }
f (PercentageV p) = pure $ zeroPercentageToLength p
where zeroPercentageToLength :: Percentage -> Value
zeroPercentageToLength 0 = DistanceV (Distance 0 Q)
zeroPercentageToLength x = PercentageV x
f (BgSizeV (BgSize x y)) = pure . BgSizeV $ BgSize (zeroPerToLength x) (fmap zeroPerToLength y)
where zeroPerToLength (Left (Left 0)) = Left $ Right (Distance 0 Q)
zeroPerToLength z = z
f x = pure x
replaceWithZero :: Text -> Declaration -> Reader Config Declaration
replaceWithZero s d@(Declaration p (Values v vs) _ _)
| not (null vs) = pure d
| otherwise =
case Map.lookup (T.toCaseFold p) propertiesTraits of
Just (iv, inhs) -> if f iv inhs == mkOther s
then pure $ d { valueList = Values (DistanceV (Distance 0 Q)) [] }
else pure d
Nothing -> pure d
where f (Just (Values x _)) inh
| v == Initial || v == Unset && not inh = x
| otherwise = v
f _ _ = v
fontWeightOptimizer :: Declaration -> Reader Config Declaration
fontWeightOptimizer = optimizeValues f
where f :: Value -> Reader Config Value
f x@(Other t) = do
conf <- ask
pure $ case fontweightSettings conf of
FontWeightMinOn -> replaceForSynonym t
FontWeightMinOff -> x
f x = pure x
replaceForSynonym :: TextV -> Value
replaceForSynonym t
| t == TextV "normal" = NumberV 400
| t == TextV "bold" = NumberV 700
| otherwise = Other t
optimizeTransformOrigin :: Declaration -> Reader Config Declaration
optimizeTransformOrigin d@(Declaration _ v _ _) = do
conf <- ask
pure $ if shouldMinifyTransformOrigin conf
then d { valueList = optimizeTransformOrigin' v}
else d
optimizeTransformOrigin' :: Values -> Values
optimizeTransformOrigin' v =
mkValues $ case valuesToList v of
[x, y, z] -> if isZeroVal z
then transformOrigin2 x y
else transformOrigin3 x y z
[x, y] -> transformOrigin2 x y
[x] -> transformOrigin1 x
x -> x
isZeroVal :: Value -> Bool
isZeroVal (DistanceV (Distance 0 Q)) = True
isZeroVal (NumberV (Number 0)) = True
isZeroVal (PercentageV 0) = True
isZeroVal _ = False
transformOrigin1 :: Value -> [Value]
transformOrigin1 (Other "top") = [Other "top"]
transformOrigin1 (Other "bottom") = [Other "bottom"]
transformOrigin1 (Other "right") = [PercentageV (Percentage 100)]
transformOrigin1 (Other "left") = [DistanceV (Distance 0 Q)]
transformOrigin1 (Other "center") = [PercentageV (Percentage 50)]
transformOrigin1 (PercentageV 0) = [DistanceV (Distance 0 Q)]
transformOrigin1 x = [x]
transformOrigin2 :: Value -> Value -> [Value]
transformOrigin2 x y
| equalsCenter x = firstIsCenter
| equalsCenter y = secondIsCenter
| isYoffsetKeyword x = fmap convertValue [y,x]
| isXoffsetKeyword y = fmap convertValue [y,x]
| otherwise = fmap convertValue [x,y]
where firstIsCenter
| equalsCenter y = [per50]
| isYoffsetKeyword y = [y]
| y == per100 = [Other "bottom"]
| isZeroVal y = [Other "top"]
| isPercentageOrDistance y = [per50, y]
| otherwise = transformOrigin1 y
secondIsCenter
| equalsCenter x = [per50]
| isYoffsetKeyword x || isPercentageOrDistance x = [x]
| otherwise = transformOrigin1 x
isPercentageOrDistance (PercentageV _) = True
isPercentageOrDistance (DistanceV _) = True
isPercentageOrDistance _ = False
equalsCenter a = a == Other "center" || a == per50
isXoffsetKeyword a = a == Other "left" || a == Other "right"
isYoffsetKeyword a = a == Other "top" || a == Other "bottom"
per50 = PercentageV $ Percentage 50
per100 = PercentageV $ Percentage 100
convertValue (Other t) = fromMaybe (Other t) (Map.lookup (getText t) transformOriginKeywords)
convertValue n@(PercentageV p) = if p == 0
then DistanceV (Distance 0 Q)
else n
convertValue i = i
transformOrigin3 :: Value -> Value -> Value -> [Value]
transformOrigin3 x y z
| x == Other "top" || x == Other "bottom"
|| y == Other "left" || y == Other "right" = fmap replaceKeywords [y, x, z]
| otherwise = fmap replaceKeywords [x, y, z]
where replaceKeywords :: Value -> Value
replaceKeywords (Other t) = fromMaybe x (Map.lookup (getText t) transformOriginKeywords)
replaceKeywords e = e
transformOriginKeywords :: Map Text Value
transformOriginKeywords = Map.fromList
[("top", DistanceV (Distance 0 Q))
,("right", PercentageV (Percentage 100))
,("bottom", PercentageV (Percentage 100))
,("left", DistanceV (Distance 0 Q))
,("center", PercentageV (Percentage 50))]
minifyDec :: Declaration -> Maybe Values -> Bool -> Declaration
minifyDec d@(Declaration p vs _ _) mv inherits =
case mv of
Just vals ->
case Map.lookup (T.toCaseFold p) declarationExceptions of
Just f -> f d vals inherits
Nothing -> reduceDeclaration d vals inherits
Nothing ->
if not inherits && vs == initial || inherits && vs == inherit
then d { valueList = unset }
else d
unset :: Values
unset = Values Unset mempty
initial :: Values
initial = Values Initial mempty
inherit :: Values
inherit = Values Inherit mempty
declarationExceptions :: Map Text (Declaration -> Values -> Bool -> Declaration)
declarationExceptions = Map.fromList $ map (first T.toCaseFold)
[("background-size", backgroundSizeReduce)
,("-webkit-background-size", backgroundSizeReduce)
,("font-synthesis", fontSynthesisReduce)
]
combineTransformFunctions :: Declaration -> Reader Config Declaration
combineTransformFunctions d@(Declaration _ vs _ _) = do
combinedFuncs <- combine $ fmap (\(TransformV x) -> x) tfuncs
let newVals = fmap TransformV combinedFuncs ++ (decValues \\ tfuncs)
pure $ d { valueList = mkValues newVals}
where decValues = valuesToList vs
tfuncs = filter isTransformFunction decValues
isTransformFunction (TransformV _) = True
isTransformFunction _ = False
backgroundSizeReduce :: Declaration -> Values -> Bool -> Declaration
backgroundSizeReduce d@(Declaration _ vs _ _) initVals inherits =
case valuesToList vs of
[v1,v2] -> if v2 == mkOther "auto"
then d { valueList = mkValues [v1] }
else d
_ -> d { valueList = shortestEquiv vs initVals inherits }
fontSynthesisReduce :: Declaration -> Values -> Bool -> Declaration
fontSynthesisReduce d@(Declaration _ vs _ _) initVals inherits =
case valuesToList initVals \\ valuesToList vs of
[] -> d {valueList = initial}
_ -> d {valueList = shortestEquiv vs initVals inherits}
reduceDeclaration :: Declaration -> Values -> Bool -> Declaration
reduceDeclaration d@(Declaration _ vs _ _) initVals inherits =
case analyzeValueDifference vs initVals of
Just v -> d {valueList = shortestEquiv v shortestInitialValue inherits}
Nothing -> d {valueList = minVal inherits shortestInitialValue}
where comparator x y = compare (textualLength x) (textualLength y)
shortestInitialValue = mkValues [minimumBy comparator (valuesToList initVals)]
shortestEquiv :: Values -> Values -> Bool -> Values
shortestEquiv vs siv inherits
| inherits && vs == inherit = unset
| not inherits && vs == unset || vs == initial = minVal inherits siv
| otherwise = vs
minVal :: Bool -> Values -> Values
minVal inherits vs
| textualLength globalKeyword <= textualLength vs = globalKeyword
| otherwise = vs
where globalKeyword = mkValues [if not inherits then Unset else Initial]
analyzeValueDifference :: Values -> Values -> Maybe Values
analyzeValueDifference vs initVals =
case valuesDifference of
[] -> Nothing
_ -> Just $ mkValues valuesDifference
where valuesDifference = valuesToList vs \\ valuesToList initVals
clean :: [Declaration] -> [Declaration]
clean [] = []
clean (d:ds) =
let (newD, newDs) = solveClashes ds d pinfo
in case newD of
Just x -> x : clean newDs
Nothing -> clean newDs
where pinfo = fromMaybe (PropertyInfo mempty mempty)
(Map.lookup (propertyName d) shorthandAndLonghandsMap)
solveClashes :: [Declaration] -> Declaration
-> PropertyInfo -> (Maybe Declaration, [Declaration])
solveClashes ds = solveClashes' ds ds
solveClashes' :: [Declaration] -> [Declaration] -> Declaration
-> PropertyInfo -> (Maybe Declaration, [Declaration])
solveClashes' newDs [] dec _ = (Just dec, newDs)
solveClashes' newDs (laterDec:ds) dec pinfo
| hasVendorPrefix dec = (Just dec, newDs)
| hasVendorPrefix laterDec || hasIEhack dec /= hasIEhack laterDec =
solveClashes' newDs ds dec pinfo
| propertyName laterDec `elem` subproperties pinfo =
attemptMerge newDs ds dec laterDec pinfo
| propertyName laterDec `elem` overwrittenBy pinfo =
if isImportant dec && (not . isImportant) laterDec
then solveClashes' newDs ds dec pinfo
else (Nothing, newDs)
| propertyName dec == propertyName laterDec =
if isImportant dec && (not . isImportant) laterDec
then solveClashes' (delete laterDec newDs) ds dec pinfo
else (Nothing, newDs)
| otherwise = solveClashes' newDs ds dec pinfo
hasVendorPrefix :: Declaration -> Bool
hasVendorPrefix (Declaration _ vs _ _) = any isVendorPrefixedValue $ valuesToList vs
isVendorPrefixedValue :: Value -> Bool
isVendorPrefixedValue (Other t) = T.isPrefixOf "-" $ getText t
isVendorPrefixedValue (GradientV t _) = T.isPrefixOf "-" t
isVendorPrefixedValue (GenericFunc t _) = T.isPrefixOf "-" t
isVendorPrefixedValue _ = False
attemptMerge :: [Declaration] -> [Declaration] -> Declaration
-> Declaration -> PropertyInfo
-> (Maybe Declaration, [Declaration])
attemptMerge newDs ds dec laterDec pinfo =
case merge dec laterDec of
Just m -> (Nothing, m : delete dec (delete laterDec newDs))
Nothing -> solveClashes' newDs ds dec pinfo
merge :: Declaration -> Declaration -> Maybe Declaration
merge d1@(Declaration p1 _ _ _) d2@Declaration{} = do
mergeFunction <- Map.lookup p1 propertyMergers
mergeFunction d1 d2
where propertyMergers :: Map Text (Declaration -> Declaration -> Maybe Declaration)
propertyMergers = Map.fromList [("margin", mergeIntoTRBL)
,("padding", mergeIntoTRBL)
,("border-color", mergeIntoTRBL)
,("border-width", mergeIntoTRBL)
,("border-style", mergeIntoTRBL)
--,("background",
--,("border",
--,("border-image",
--,("border-radius"
--,("border-right",
--,("border-top"
--,("column-rule",
--,("columns",
--,("flex-flow",
--,("font",
--,("grid",
--,("grid-area"
--,("grid-column"
--,("grid-gap",
--,("grid-row",
--,("grid-template",
--,("list-style",
--,("outline",
--,("padding",
--,("text-emphasis"
--,("transition",
]
mergeIntoTRBL :: Declaration
-> Declaration
-> Maybe Declaration
mergeIntoTRBL d1@(Declaration _ (Values v1 vs) i1 h1) d2@(Declaration p2 (Values v2 _) i2 h2)
| h1 || h2 = Nothing
| i1 && not i2 = Just $ reduceTRBL d1
| not i1 && i2 = Nothing
| otherwise = do
(_,index) <- find (\(x,_) -> T.isInfixOf x (T.toCaseFold p2)) indexTable
let mkDec ys = d1 {valueList = mkValues $ replaceAt index v2 ys}
retDec ys = let mergedDec = reduceTRBL (mkDec ys)
in if textualLength mergedDec <= originalLength
then Just mergedDec
else Nothing
case trblValues of
[_,_,_,_] -> retDec trblValues
[t,r,b] -> retDec [t,r,b,r]
[t,r] -> retDec [t,r,t,r]
[t] -> retDec [t,t,t,t]
_ -> Nothing
where originalLength = textualLength d1 + textualLength d2 + 1
trblValues = v1 : map snd vs
indexTable = fmap (first T.toCaseFold) [("top", 0)
,("right", 1)
,("bottom", 2)
,("left", 3)]
reduceTRBL :: Declaration -> Declaration
reduceTRBL d@(Declaration _ (Values v1 vs) _ _) =
case v1:map snd vs of
[t,r,b,l] -> reduce4 t r b l
[t,r,b] -> reduce3 t r b
[t,r] -> reduce2 t r
_ -> d
where reduce4 tv rv bv lv
| lv == rv = reduce3 tv rv bv
| otherwise = d
reduce3 tv rv bv
| tv == bv = reduce2 tv rv
| otherwise = d { valueList = mkValues [tv, rv, bv] }
reduce2 tv rv
| tv == rv = d { valueList = mkValues [tv] }
| otherwise = d { valueList = mkValues [tv, rv] }
mapValues :: (Value -> Reader Config Value) -> Values -> Reader Config Values
mapValues f (Values v1 vs) = do
x <- f v1
xs <- (mapM . mapM) f vs
pure $ Values x xs