diff --git a/LICENSE b/LICENSE
new file mode 100644
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,26 @@
+Copyright 2017 Cristian Adrián Ontivero
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are met:
+
+1. Redistributions of source code must retain the above copyright notice, this
+   list of conditions and the following disclaimer.
+
+2. Redistributions in binary form must reproduce the above copyright notice,
+   this list of conditions and the following disclaimer in the documentation
+   and/or other materials provided with the distribution.
+
+3. Neither the name of the copyright holder nor the names of its contributors
+   may be used to endorse or promote products derived from this software
+   without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
+ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
+WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
+DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
+FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
+DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
+SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
+CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
+OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
diff --git a/Main.hs b/Main.hs
new file mode 100644
--- /dev/null
+++ b/Main.hs
@@ -0,0 +1,119 @@
+{-# LANGUAGE TemplateHaskell #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Main
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Main where
+
+import Codec.Compression.Hopfli
+import Control.Monad.Reader
+import Data.Monoid ((<>))
+import Data.Attoparsec.Text (parseOnly)
+import Data.Text.Lazy.Builder (toLazyText)
+import Options.Applicative hiding (command)
+import Data.Version (showVersion)
+import Development.GitRev (gitHash)
+import Paths_hasmin (version)
+import qualified Data.ByteString as B
+import qualified Data.Text.Encoding as TE
+import qualified Data.Text.IO as TIO
+import qualified Data.Text.Lazy as TL
+import System.Exit (die)
+import Text.PrettyPrint.Mainland (line, punctuate, putDoc, ppr)
+import Hasmin.Config
+import Hasmin.Parser.Internal
+import Hasmin.Types.Class
+import Hasmin.Types.Stylesheet
+
+command :: Parser Commands
+command = Commands <$> switch (long "beautify"
+                          <> short 'b' <> help "Beautify output")
+                  <*> switch (long "zopfli"
+                          <> short 'z' <> help "Compress result using zopfli")
+                  <*> argument str (metavar "FILE")
+
+config :: Parser Config
+config = Config
+  <$> flag ColorMinOn ColorMinOff (long "no-color-min"
+                                <> short 'c'
+                                <> help "Disable <color> minification")
+  <*> flag DimMinOff DimMinOn (long "dimension-min"
+                            <> short 'd'
+                            <> help  "Enable normalization of absolute dimensions")
+  <*> flag GradientMinOn GradientMinOff (long "-no-gradient-min"
+                    <> short 'g'
+                    <> help "Disable <gradient> minification")
+  <*> flag True False (long "no-property-traits"
+                    <> short 't'
+                    <> help "Disable use of property traits for declaration minification")
+  <*> flag True False (long "no-rule-cleaning"
+                    <> short 'a'
+                    <> help "Disable deletion of overwritten properties")
+  <*> flag True False (long "no-timing-function-min"
+                    <> help "Disable <timing-function> minifications")
+  <*> flag True False (long "no-filter-function-min"
+                    <> help "Disable <filter-function> minifications")
+         <*> flag True False (long "no-quotes-removal"
+                           <> short 'q'
+                           <> help "Disable removing quotes whenever possible")
+         <*> flag FontWeightMinOn FontWeightMinOff (long "no-font-weight-minification"
+                           <> help "Disable converting normal to 400 and bold to 700 in font-weight")
+         <*> flag True False (long "no-transform-origin-minification"
+                           <> help "Disable converting left and top to 0%, bottom and right to 100%, and center to 50%")
+         <*> flag True False (long "no-microsyntax-min"
+                           <> short 'm'
+                           <> help "Disable minification of An+B microsyntax")
+         <*> flag True False (long "no-@kfsel-min"
+                           <> short 'k'
+                           <> help "Disable transform function minification")
+         <*> flag True False (long "no-transform-function-min"
+                           <> help "Disable @keyframe selectors minification")
+         <*> switch (long "convert-escaped-characters"
+                    <> help "Convert escaped characters to their UTF-8 equivalent")
+         <*> flag True False (long "no-null-percentage-conversion"
+                           <> help "Disable converting 0% to 0 when possible")
+         <*> flag True False (long "no-empty-block-removal"
+                           <> short 'e'
+                           <> help "Disable empty block removal")
+         <*> flag True False (long "no-duplicate-selector-removal"
+                           <> help "Disable removal of duplicate selectors")
+         <*> flag True False (long "no-quote-normalization"
+                           <> help "Disable trying to convert all quotation marks to either \" or \'")
+         <*> flag Lowercase Original (long "no-lowercasing"
+                           <> short 'l'
+                           <> help "Disable lowercasing everything possible")
+         <*> flag Lexicographical NoSorting (long "no-selector-sorting"
+                           <> help "Disable sorting selectors lexicographically")
+         <*> flag Lexicographical NoSorting (long "no-property-sorting"
+                           <> help "Disable sorting properties lexicographically")
+
+instructions :: ParserInfo Instructions
+instructions = info (helper <*> versionOption <*> ((,) <$> command <*> config))
+    (fullDesc <> header "Hasmin - A Haskell CSS Minifier")
+  where versionOption = infoOption (showVersion version <> " " <> $(gitHash))
+                                   (long "version" <> help "Show version and commit hash")
+
+main :: IO ()
+main = do
+    (comm, conf) <- execParser instructions
+    text         <- TIO.readFile (file comm)
+    case parseOnly stylesheet text of
+      Right r -> process r comm conf
+      Left e  -> die e
+
+process :: [Rule] -> Commands -> Config -> IO ()
+process r comm conf
+    | shouldBeautify comm = printBeautified $ fmap ppr sheet
+    | shouldCompress comm = B.writeFile "output.gz" . compressWith defaultCompressOptions GZIP . TE.encodeUtf8 $ output
+    | otherwise           = TIO.putStr output
+  where sheet  = let ruleList = fmap (\x -> runReader (minifyWith x) conf) r
+                 in if shouldRemoveEmptyBlocks conf
+                       then filter (not . isEmpty) ruleList
+                       else ruleList
+        output = TL.toStrict . toLazyText $ mconcat (fmap toBuilder sheet)
+        printBeautified = putDoc . mconcat . punctuate (line <> line)
diff --git a/README.md b/README.md
new file mode 100644
--- /dev/null
+++ b/README.md
@@ -0,0 +1,50 @@
+Hasmin - A Haskell CSS Minifier
+====
+[![Build Status](https://travis-ci.org/contivero/hasmin.svg?branch=master)](https://travis-ci.org/contivero/hasmin)
+[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
+
+Hasmin is a CSS minifier. To use it as a library, refer to the Hasmin module
+documentation.
+
+Aside from the usual techniques (e.g. whitespace removal, color minification,
+etc.), the idea was to explore new possibilities, by implementing things
+other minifiers weren't doing, or they were, but not taking full advantage of.
+
+Also, the minifier implements some techniques that do nothing for minified
+sizes, but attempt to improve post-compression sizes (at least when using
+DEFLATE, i.e. gzip).
+
+For a list of techniques, see [Minification Techniques](https://github.com/contivero/hasmin/wiki/Minification-Techniques).
+
+## Building
+To compile, just run `stack build`.
+
+## Minifier Usage
+Hasmin expects a path to the CSS file, and outputs the minified result to
+stdout.
+
+Every technique is enabled by default, except for:
+ 1. Escaped character conversions (e.g. converting `\2714` to `✔`, which can be enabled with `--convert-escaped-characters`)
+ 2. Dimension minifications (e.g. converting `12px` to `9pt`, which can be enabled with `--dimension-min`, or just `-d`)
+
+These two are disabled mainly because they are—on average, not
+always—detrimental for DEFLATE compression. When something needs to be
+disabled, use the appropriate flag. Not every technique can be toggled yet, but
+a good amount of them allow it.
+
+Note: there is a problem in Windows when using the
+`--convert-escaped-characters` flag to enable the conversion of escaped
+characters. A workaround is changing the code page, which can be done by running
+`chcp 65001` in the terminal (whether cmd, or cygwin).
+
+## Zopfli Integration
+Hasmin uses bindings to Google's
+[Zopfli library](https://en.wikipedia.org/wiki/Zopfli), allowing the
+possibility to compress the result.
+
+Since the output is a gzip file, it can be used for the web. It tipically
+produces files 3~8% smaller than zlib, at the cost of being around 80 times
+slower, so it is only a good idea if you don't need compression on the fly.
+
+
+Zopfli is released under the Apache License, Version 2.0.
diff --git a/Setup.hs b/Setup.hs
new file mode 100644
--- /dev/null
+++ b/Setup.hs
@@ -0,0 +1,2 @@
+import Distribution.Simple
+main = defaultMain
diff --git a/hasmin.cabal b/hasmin.cabal
new file mode 100644
--- /dev/null
+++ b/hasmin.cabal
@@ -0,0 +1,132 @@
+name:                hasmin
+version:             0.3.0.1
+license:             BSD3
+license-file:        LICENSE
+author:              (c) 2017 Cristian Adrián Ontivero <cristianontivero@gmail.com>
+maintainer:          Cristian Adrián Ontivero <cristianontivero@gmail.com>
+synopsis:            "A CSS Minifier"
+homepage:            https://github.com/contivero/hasmin/
+bug-reports:         https://github.com/contivero/hasmin/issues
+category:            Text
+build-type:          Simple
+extra-source-files:  README.md
+cabal-version:       >=1.10
+description:
+    A CSS minifier which not only aims at reducing the amount of bytes of the
+    output, but also at improving gzip compression. By default, the output is the
+    minified CSS file, but hasmin allows also its compression into gzip using
+    Google's Zopfli library.
+    .
+    To use it: ./hasmin input.css > output.css
+    .
+    By default, most minification techniques are enabled. For a list of
+    available flags, do: ./hasmin --help
+
+source-repository head
+  type: git
+  location: https://github.com/contivero/hasmin.git
+
+executable hasmin
+  default-language:    Haskell2010
+  ghc-options:         -O2 -Wall -fwarn-tabs -fwarn-unused-do-bind
+  main-is:             Main.hs
+  other-modules:       Paths_hasmin
+  build-depends:       base                 >=4.8        && <5.1
+                     , attoparsec           >=0.12       && <0.14
+                     , containers           >=0.5        && <0.6
+                     , optparse-applicative >=0.11       && <0.14
+                     , parsers              >=0.12.3     && <0.13
+                     , mainland-pretty      >=0.4.1      && <0.5
+                     , text                 >=1.2        && <1.3
+                     , hopfli               >=0.2        && <0.3
+                     , bytestring           >=0.10.2.0   && <0.11
+                     , gitrev               >=1.0.0      && <=1.2.0
+                     , matrix               >=0.3.4      && <0.4
+                     , mtl                  >=2.2.1      && <2.3
+                     , numbers              >=3000.2.0.0 && <3000.3
+                     , hasmin
+
+library
+  default-language:    Haskell2010
+  hs-source-dirs:      src
+  ghc-options:         -O2 -Wall -fwarn-tabs -fwarn-unused-do-bind
+  exposed-modules:     Hasmin
+                     , Hasmin.Config
+                     , Hasmin.Types.Class
+                     , Hasmin.Types.Stylesheet
+                     -- exposed because it is needed by tests:
+                     , Hasmin.Parser.Internal
+                     , Hasmin.Parser.Value
+                     , Hasmin.Types.Color
+                     , Hasmin.Types.Declaration
+                     , Hasmin.Types.Dimension
+                     , Hasmin.Types.Numeric
+                     , Hasmin.Types.TransformFunction
+                     , Hasmin.Types.Value
+                     , Hasmin.Utils
+  other-modules:       Hasmin.Parser.Utils
+                     , Hasmin.Properties
+                     , Hasmin.Selector
+                     , Hasmin.Types.BgSize
+                     , Hasmin.Types.FilterFunction
+                     , Hasmin.Types.Gradient
+                     , Hasmin.Types.PercentageLength
+                     , Hasmin.Types.Position
+                     , Hasmin.Types.RepeatStyle
+                     , Hasmin.Types.Shadow
+                     , Hasmin.Types.String
+                     , Hasmin.Types.TimingFunction
+  build-depends:       base            >=4.8        && <5.1
+                     , attoparsec      >=0.12       && <0.14
+                     , bytestring      >=0.10.2.0   && <0.11
+                     , containers      >=0.5        && <0.6
+                     , mainland-pretty >=0.4.1      && <0.5
+                     , matrix          >=0.3.4      && <0.4
+                     , mtl             >=2.2.1      && <2.3
+                     , numbers         >=3000.2.0.0 && <3000.3
+                     , parsers         >=0.12.3     && <0.13
+                     , semigroups      >=0.16       && <0.19
+                     , text            >=1.2        && <1.3
+
+test-suite spec
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      tests
+  main-is:             Spec.hs
+  ghc-options:         -Wall -fno-warn-orphans
+  default-extensions:  OverloadedStrings
+  other-modules:       Hasmin.Parser.InternalSpec
+                     , Hasmin.Parser.ValueSpec
+                     , Hasmin.SelectorSpec
+                     , Hasmin.TestUtils
+                     , Hasmin.Types.ColorSpec
+                     , Hasmin.Types.DeclarationSpec
+                     , Hasmin.Types.DimensionSpec
+                     , Hasmin.Types.FilterFunctionSpec
+                     , Hasmin.Types.GradientSpec
+                     , Hasmin.Types.PositionSpec
+                     , Hasmin.Types.RepeatStyleSpec
+                     , Hasmin.Types.ShadowSpec
+                     , Hasmin.Types.StringSpec
+                     , Hasmin.Types.StylesheetSpec
+                     , Hasmin.Types.TransformFunctionSpec
+  build-depends:       base             >=4.7     && <5.1
+                     , attoparsec       >=0.12    && <0.14
+                     , hspec            >=2.2     && <3.0
+                     , hspec-attoparsec >=0.1.0.0 && <0.2
+                     , mtl              >=2.2.1   && <2.3
+                     , QuickCheck       >=2.8     && <3.0
+                     , text             >=1.2     && <1.3
+                     , hasmin
+
+test-suite doctest
+  default-language:    Haskell2010
+  type:                exitcode-stdio-1.0
+  hs-source-dirs:      tests
+  main-is:             DocTest.hs
+  ghc-options:         -threaded -Wall -fno-warn-orphans
+  build-depends:       base             >=4.8     && <5.1
+                     , doctest          >=0.11    && <0.12
+                     , doctest-discover >=0.1.0.0 && <0.2
+                     , doctest          >=0.10
+                     , hasmin
diff --git a/src/Hasmin.hs b/src/Hasmin.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin.hs
@@ -0,0 +1,71 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Recommended module to use the library.
+--
+-----------------------------------------------------------------------------
+module Hasmin (
+    -- * Using the library
+    -- $use
+      minifyCSS
+    , minifyCSSWith
+    , module Hasmin.Config
+    ) where
+
+import Control.Monad.Reader (runReader)
+import Data.Attoparsec.Text (parseOnly)
+import Data.Text.Lazy.Builder (toLazyText)
+import Data.Text (Text)
+import qualified Data.Text.Lazy as TL
+
+import Hasmin.Parser.Internal
+import Hasmin.Types.Class
+import Hasmin.Config
+
+-- | Minify Text, based on a 'Config'. To just use a default set of
+-- configurations (i.e. 'defaultConfig'), use 'minifyCSS'.
+minifyCSSWith :: Config -> Text -> Either String Text
+minifyCSSWith cfg t = do
+    sheet <- parseOnly stylesheet t
+    pure . TL.toStrict . toLazyText . mconcat $ map f sheet
+  where f x = toBuilder $ runReader (minifyWith x) cfg
+
+-- | Minify Text CSS, using a default set of configurations (with most
+-- minification techniques enabled).
+minifyCSS :: Text -> Either String Text
+minifyCSS = minifyCSSWith defaultConfig
+
+-- $use
+--
+-- This section shows a basic library use case.
+--
+-- Given a style sheet, say:
+--
+-- > sampleSheet :: Text
+-- > sampleSheet = "body { color: #ff0000 } div { margin: 0 0 0 0 }"
+--
+-- Minify it with 'minifyCSS'. In ghci:
+--
+-- > > minifyCSS sampleSheet
+-- > Just "body{color:red}div{margin:0}"
+--
+-- To modify the minification settings, just use another 'Config', e.g.:
+--
+-- > cfg :: Config
+-- > cfg = defaultConfig { colorSettings = ColorMinOff }
+--
+-- Once more in ghci, this time using 'minifyCSSWith':
+--
+-- > > minifyStylesheet cfg sampleSheet
+-- > Just "body{color:#ff0000}div{margin:0}"
+--
+-- The output is once more minified, but this time leaving colors as they were
+-- originally.
+--
+-- For the complete list of possible options, refer to 'Config'.
diff --git a/src/Hasmin/Config.hs b/src/Hasmin/Config.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Config.hs
@@ -0,0 +1,95 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Config
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Config (
+      Config(..)
+    , ColorSettings(..)
+    , DimensionSettings(..)
+    , GradientSettings(..)
+    , FontWeightSettings(..)
+    , LetterCase(..)
+    , SortingMethod(..)
+    , defaultConfig
+    , Instructions
+    , Commands(..)
+    ) where
+
+type Instructions = (Commands, Config)
+
+data Commands = Commands { shouldBeautify :: Bool
+                         , shouldCompress :: Bool
+                         , file :: FilePath
+                         } deriving (Show)
+
+data ColorSettings = ColorMinOff | ColorMinOn
+  deriving (Show, Eq)
+data DimensionSettings = DimMinOff | DimMinOn
+  deriving (Show, Eq)
+data GradientSettings = GradientMinOff | GradientMinOn
+  deriving (Show, Eq)
+data LetterCase = Original  -- ^ Leave letter casing as is.
+                | Lowercase -- ^ Lowercase whatever possible to improve gzip compression.
+  deriving (Show, Eq)
+data SortingMethod = NoSorting | Lexicographical
+  deriving (Show, Eq)
+data FontWeightSettings = FontWeightMinOff | FontWeightMinOn
+  deriving (Show, Eq)
+
+-- TODO: * avoid boolean blindness
+--       * Use a more declarative style for the names, e.g. shouldLowercase vs.
+--       preferredCasing, shouldSortSelectors vs. selectorSorting, etc.
+-- | The configuration used for minifying.
+data Config = Config { colorSettings :: ColorSettings
+                     , dimensionSettings :: DimensionSettings
+                     , gradientSettings :: GradientSettings
+                     , shouldUsePropertyTraits :: Bool
+                     , shouldCleanRules :: Bool
+                     , shouldMinifyTimingFunctions :: Bool
+                     , shouldMinifyFilterFunctions :: Bool
+                     , shouldRemoveQuotes :: Bool
+                     , fontweightSettings :: FontWeightSettings
+                     , shouldMinifyTransformOrigin :: Bool
+                     , shouldMinifyMicrosyntax :: Bool
+                     , shouldMinifyKeyframeSelectors :: Bool
+                     , shouldMinifyTransformFunction :: Bool
+                     , shouldConvertEscaped :: Bool
+                     , shouldConvertNullPercentages :: Bool
+                     , shouldRemoveEmptyBlocks :: Bool
+                     , shouldRemoveDuplicateSelectors :: Bool
+                     , shouldNormalizeQuotes :: Bool
+                     , letterCase :: LetterCase
+                     , selectorSorting :: SortingMethod
+                     , declarationSorting :: SortingMethod
+                     } deriving (Show)
+
+-- | A default config with most settings enabled. Used by the minify function,
+-- mainly for testing purposes.
+defaultConfig :: Config
+defaultConfig = Config { colorSettings                  = ColorMinOn
+                       , dimensionSettings              = DimMinOn
+                       , gradientSettings               = GradientMinOn
+                       , shouldUsePropertyTraits        = True
+                       , shouldCleanRules               = True
+                       , shouldMinifyTimingFunctions    = True
+                       , shouldMinifyFilterFunctions    = True
+                       , shouldRemoveQuotes             = True
+                       , fontweightSettings             = FontWeightMinOn
+                       , shouldMinifyTransformOrigin    = True
+                       , shouldMinifyMicrosyntax        = True
+                       , shouldMinifyKeyframeSelectors  = True
+                       , shouldMinifyTransformFunction  = True
+                       , shouldConvertEscaped           = True
+                       , shouldConvertNullPercentages   = True
+                       , shouldRemoveEmptyBlocks        = True
+                       , shouldRemoveDuplicateSelectors = True
+                       , shouldNormalizeQuotes          = True
+                       , letterCase                     = Lowercase
+                       , selectorSorting                = NoSorting
+                       , declarationSorting             = NoSorting
+                       }
diff --git a/src/Hasmin/Parser/Internal.hs b/src/Hasmin/Parser/Internal.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Parser/Internal.hs
@@ -0,0 +1,499 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Parser.Internal
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Parser.Internal (
+      stylesheet
+    , atRule
+    , declaration
+    , declarations
+    , selector
+    ) where
+
+import Control.Arrow (first)
+import Control.Applicative ((<|>), many)
+import Control.Monad (mzero)
+import Data.Functor (($>))
+import Data.Attoparsec.Combinator (lookAhead, sepBy, endOfInput)
+import Data.Attoparsec.Text (asciiCI, char, many1, manyTill,
+  option, Parser, satisfy, string)
+import Data.List.NonEmpty (NonEmpty( (:|) ))
+import Data.Monoid ((<>))
+import Data.Maybe (fromMaybe)
+import Data.Text (Text)
+import Data.Text.Lazy.Builder as LB
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as Set
+import qualified Data.Attoparsec.Text as A
+import qualified Data.Char as C
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+
+import Hasmin.Parser.Utils
+import Hasmin.Parser.Value
+import Hasmin.Selector
+import Hasmin.Types.Stylesheet
+import Hasmin.Types.Declaration
+
+selector :: Parser Selector
+selector = Selector <$> compoundSelector
+      <*> many ((,) <$> (combinator <* skipComments) <*> compoundSelector)
+
+-- First tries with '>>' (descendant), '>' (child), '+' (adjacent sibling), and
+-- '~' (general sibling) combinators. If those fail, it tries with the
+-- descendant (whitespace) combinator. This is done to allow comments in-between.
+--
+-- | Parser for selector combinators, i.e. ">>" (descendant), '>' (child), '+'
+-- (adjacent sibling), '~' (general sibling), and ' ' (descendant) combinators.
+combinator :: Parser Combinator
+combinator =  (skipComments *> ((string ">>" $> Descendant)
+                            <|> (char '>' $> Child)
+                            <|> (char '+' $> AdjacentSibling)
+                            <|> (char '~' $> GeneralSibling)))
+          <|> (satisfy ws $> Descendant)
+  where ws c = c == ' ' || c == '\t' || c == '\n' || c == '\r' || c == '\f'
+
+compoundSelector :: Parser CompoundSelector
+compoundSelector =
+    (:|) <$> (typeSelector <|> universal)
+                           <*> many p
+                       <|> ((Universal mempty :|) <$> many1 p)
+  where p = idSel <|> classSel <|> attributeSel <|> pseudo
+
+idSel :: Parser SimpleSelector
+idSel = do
+    _ <- char '#'
+    name <- mconcat <$> many1 nmchar
+    pure $ IdSel (TL.toStrict (toLazyText name))
+
+-- class: '.' IDENT
+classSel :: Parser SimpleSelector
+classSel = char '.' *> (ClassSel <$> ident)
+
+{-
+attrib
+  : '[' S* [ namespace_prefix ]? IDENT S*
+        [ [ PREFIXMATCH |
+            SUFFIXMATCH |
+            SUBSTRINGMATCH |
+            '=' |
+            INCLUDES |
+            DASHMATCH ] S* [ IDENT | STRING ] S*
+        ]? ']'
+  ;
+-}
+-- FIXME namespace prefixes aren't allowed inside attribute selectors,
+-- but they should be.
+attributeSel :: Parser SimpleSelector
+attributeSel = do
+    _     <- char '[' *> skipComments
+    attId <- ident <* skipComments
+    g     <- option Attribute attValue
+    _     <- char ']'
+    pure $ AttributeSel (g attId)
+  where attValue = do
+          f <- ((string "^=" $> (:^=:)) <|>
+                (string "$=" $> (:$=:)) <|>
+                (string "*=" $> (:*=:)) <|>
+                (string "="  $> (:=:))  <|>
+                (string "~=" $> (:~=:)) <|>
+                (string "|=" $> (:|=:))) <* skipComments
+          attval <- ((Left <$> ident) <|> (Right <$> stringtype)) <* skipComments
+          pure (`f` attval)
+
+{-
+-- string1: \"([^\n\r\f\\"]|\\{nl}|{escape})*\"
+string1 = char '\"' *> many (o1 <|> o2 <|> escape)
+  where o1 = satisfy (c -> c /= '\"' && c /= '\\' && c /= '\n')
+        o2 = char '\\' *> newline $> mempty
+-}
+
+-- type_selector: [namespace_prefix]? element_name
+typeSelector :: Parser SimpleSelector
+typeSelector = Type <$> opt namespacePrefix <*> ident
+
+-- universal: [ namespace_prefix ]? '*'
+universal :: Parser SimpleSelector
+universal = Universal <$> opt namespacePrefix <* char '*'
+
+-- namespace_prefix: [ IDENT | '*' ]? '|'
+namespacePrefix :: Parser Text
+namespacePrefix = opt (ident <|> string "*") <* char '|'
+
+{- '::' starts a pseudo-element, ':' a pseudo-class
+   Exceptions: :first-line, :first-letter, :before and :after.
+   Note that pseudo-elements are restricted to one per selector and
+   occur only in the last simple_selector_sequence.
+
+   <pseudo-class-selector> = ':' <ident-token> |
+                             ':' <function-token> <any-value> ')'
+   <pseudo-element-selector> = ':' <pseudo-class-selector>
+-}
+-- pseudo: ':' ':'? [ IDENT | functional_pseudo ]
+pseudo :: Parser SimpleSelector
+pseudo = char ':' *> (pseudoElementSelector <|> pseudoClassSelector)
+  where pseudoClassSelector = do
+            i <- ident
+            c <- A.peekChar
+            case c of
+              Just '(' -> char '(' *> case Map.lookup (T.toCaseFold i) fpcMap of
+                            Just p  -> functionParser p
+                            Nothing -> functionParser (FunctionalPseudoClass i <$> A.takeWhile (/= ')'))
+              _        -> pure $ PseudoClass i
+        pseudoElementSelector = do
+            parsedColon <- option False (char ':' $> True)
+            if parsedColon
+               then PseudoElem <$> ident
+               else ident >>= handleSpecialCase
+          where handleSpecialCase :: Text -> Parser SimpleSelector
+                handleSpecialCase t = if T.toCaseFold t `elem` specialPseudoElements
+                                         then pure $ PseudoElem t
+                                         else mzero
+
+-- \<An+B> microsyntax parser.
+anplusb :: Parser AnPlusB
+anplusb = (asciiCI "even" $> Even)
+      <|> (asciiCI "odd" $> Odd)
+      <|> do
+    s <- option Nothing (Just <$> parseSign)
+    x <- option mempty digits
+    case x of
+      [] -> ciN *> (AB (Nwith s Nothing) <$> (skipComments *> option Nothing (Just <$> bValue)))
+      _  -> do n <- option False (ciN $> True)
+               let a = read x :: Int
+               if n
+                  then AB (Nwith s (Just a)) <$> (skipComments *> option Nothing (Just <$> bValue))
+                  else pure $ AB NoValue (Just $ getSign s * a)
+  where ciN       = satisfy (\c -> c == 'N' || c == 'n')
+        parseSign = (char '-' $> Minus) <|> (char '+' $> Plus)
+        getSign (Just Minus) = -1
+        getSign _            = 1
+        bValue    = do
+            readPlus <- (char '-' $> False) <|> (char '+' $> True)
+            d        <- skipComments *> digits
+            if readPlus
+               then pure $ read d
+               else pure $ read ('-':d)
+
+-- Functional pseudo classes parsers map
+fpcMap :: Map Text (Parser SimpleSelector)
+fpcMap = Map.fromList $ fmap (first T.toCaseFold)
+    [buildTuple "nth-of-type"      (\x -> FunctionalPseudoClass2 x <$> anplusb)
+    ,buildTuple "nth-last-of-type" (\x -> FunctionalPseudoClass2 x <$> anplusb)
+    ,buildTuple "nth-column"       (\x -> FunctionalPseudoClass2 x <$> anplusb)
+    ,buildTuple "nth-last-column"  (\x -> FunctionalPseudoClass2 x <$> anplusb)
+    ,buildTuple "not"              (\x -> FunctionalPseudoClass1 x <$> compoundSelectorList)
+    ,buildTuple "matches"          (\x -> FunctionalPseudoClass1 x <$> compoundSelectorList)
+    ,buildTuple "nth-child"        (anbAndSelectors . FunctionalPseudoClass3)
+    ,buildTuple "nth-last-child"   (anbAndSelectors . FunctionalPseudoClass3)
+    --
+    -- :drop( [ active || valid || invalid ]? )
+    -- The :drop() functional pseudo-class is identical to :drop
+    -- ,("drop", anplusb)
+    --
+    -- It accepts a comma-separated list of one or more language ranges as its
+    -- argument. Each language range in :lang() must be a valid CSS <ident> or
+    -- <string>.
+    -- ,("lang", anplusb)
+    --
+    -- ,("dir", text)
+    -- ,("has", relative selectors)
+    ]
+  where buildTuple t c = (t, c t)
+        compoundSelectorList = (:) <$> compoundSelector <*> many (comma *> compoundSelector)
+        anbAndSelectors constructor = do
+            a <- anplusb <* skipComments
+            o <- option [] (asciiCI "of" *> skipComments *> compoundSelectorList)
+            pure $ constructor a o
+
+-- | Parse a list of comma-separated selectors, ignoring whitespace and
+-- comments.
+selectors :: Parser [Selector]
+selectors = lexeme selector `sepBy` char ','
+
+declaration :: Parser Declaration
+declaration = do
+    p  <- property <* colon
+    v  <- values p <|> valuesFallback
+    i  <- important <* skipComments
+    ie <- iehack <* skipComments
+    pure $ Declaration p v i ie
+
+-- | Parser for property names. Usually, 'ident' would be enough, but this
+-- parser adds support for IE hacks (e.g. *width), which deviate from the CSS
+-- grammar.
+property :: Parser Text
+property = mappend <$> opt ie7orLessHack <*> ident
+  where ie7orLessHack = T.singleton <$> satisfy (`Set.member` ie7orLessHacks)
+        ie7orLessHacks = Set.fromList ("!$&*()=%+@,./`[]#~?:<>|" :: String)
+
+-- | Used to parse the "!important" at the end of declarations, ignoring spaces
+-- and comments after the '!'.
+important :: Parser Bool
+important = option False (char '!' *> skipComments *> asciiCI "important" $> True)
+
+iehack :: Parser Bool
+iehack = option False (string "\\9" $> True)
+
+-- | Parses a list of declarations, ignoring spaces, comments, and empty
+-- declarations (e.g. ; ;)
+declarations :: Parser [Declaration]
+declarations = many (declaration <* handleSemicolons)
+  where handleSemicolons = many (string ";" *> skipComments)
+
+-- | Parser for CSS at-rules (e.g. \@keyframes, \@media)
+atRule :: Parser Rule
+atRule = do
+    _ <- char '@'
+    ruleType <- ident
+    fromMaybe (atBlock ruleType) (Map.lookup ruleType m)
+  where m = Map.fromList [("charset",           atCharset)
+                         ,("import",            atImport)
+                         ,("namespace",         atNamespace)
+                         ,("media",             atMedia)
+                         -- ,("supports",          atSupports)
+                         -- ,("document",          atDocument)
+                         -- ,("page",              atPage)
+                         ,("font-face",         skipComments *> atBlock "font-face")
+                         ,("keyframes",         atKeyframe mempty )
+                         ,("-webkit-keyframes", atKeyframe "-webkit-")
+                         ,("-moz-keyframes",    atKeyframe "-moz-")
+                         ,("-o-keyframes",      atKeyframe "-o-")
+                         -- ,("viewport",              atViewport)
+                         -- ,("counter-style",         atCounterStyle)
+                         -- ,("font-feature-value",    atFontFeatureValue)
+                         ]
+-- @import [ <string> | <url> ] [<media-query-list>]?;
+atImport :: Parser Rule
+atImport = do
+    esu <- skipComments *> stringOrUrl
+    mql <- option [] mediaQueryList
+    _   <- skipComments <* char ';'
+    pure $ AtImport esu mql
+
+atCharset :: Parser Rule
+atCharset = do
+    st <- skipComments *> stringtype <* skipComments <* char ';'
+    pure $ AtCharset st
+
+-- @namespace <namespace-prefix>? [ <string> | <uri> ];
+-- where
+-- <namespace-prefix> = IDENT
+atNamespace :: Parser Rule
+atNamespace = do
+    i   <- skipComments *> option mempty ident
+    ret <- if T.null i
+              then (AtNamespace i . Left) <$> stringtype
+              else decideBasedOn i
+    _ <- skipComments <* char ';'
+    pure ret
+  where decideBasedOn x =
+            let urltext = T.toCaseFold "url"
+            in if T.toCaseFold x == urltext
+                  then do c <- A.peekChar
+                          case c of
+                            Just '(' -> AtNamespace mempty <$> (char '(' *> (Right <$> url))
+                            _        -> AtNamespace x <$> (skipComments *> stringOrUrl)
+                  else AtNamespace x <$> (skipComments *> stringOrUrl)
+
+atKeyframe :: Text -> Parser Rule
+atKeyframe t = do
+    _    <- skipComments
+    name <- ident <* skipComments <* char '{'
+    bs   <- many (keyframeBlock <* skipComments)
+    _    <- char '}'
+    pure $ AtKeyframes t name bs
+
+keyframeBlock :: Parser KeyframeBlock
+keyframeBlock = do
+    sel  <- skipComments *> kfsList <* skipComments
+    ds   <- char '{' *> skipComments *> declarations <* char '}'
+    pure $ KeyframeBlock sel ds
+  where from = asciiCI "from" $> From
+        to   = asciiCI "to" $> To
+        keyframeSelector = from <|> to <|> (KFPercentage <$> percentage)
+        kfsList = (:) <$> keyframeSelector <*> many (comma *> keyframeSelector)
+
+atMedia :: Parser Rule
+atMedia = do
+  m <- satisfy C.isSpace *> mediaQueryList
+  _ <- char '{' <* skipComments
+  r <- manyTill (rule <* skipComments) (lookAhead (char '}'))
+  _ <- char '}'
+  pure $ AtMedia m r
+
+-- TODO clean code
+-- the "manyTill .. lookAhead" was added because if we only used "rules", it
+-- doesn't know when to stop, and breaks the parser
+atBlock :: Text -> Parser Rule
+atBlock i = do
+  t <- mappend i <$> A.takeWhile (/= '{') <* char '{'
+  r <- skipComments *> ((AtBlockWithDec t <$> declarations) <|> (AtBlockWithRules t <$> manyTill (rule <* skipComments) (lookAhead (char '}'))))
+  _ <- char '}'
+  pure r
+
+-- | Parses a CSS style rule, e.g. @body { padding: 0; }@
+styleRule :: Parser Rule
+styleRule = do
+    sels <- selectors <* char '{' <* skipComments
+    decs <- declarations <* char '}'
+    pure $ StyleRule sels decs
+
+-- | Parser for a CSS rule, which can be either an at-rule (e.g. \@charset), or a style
+-- rule.
+rule :: Parser Rule
+rule = atRule <|> styleRule
+
+-- | Parser for CSS rules (both style rules, and at-rules), which can be
+-- separated by whitespace or comments.
+rules :: Parser [Rule]
+rules = manyTill (rule <* skipComments) endOfInput
+
+stylesheet :: Parser [Rule]
+stylesheet = do
+  charset <- option [] ((:[]) <$> atCharset <* skipComments)
+  imports <- many (atImport <* skipComments)
+  namespaces <- many (atNamespace <* skipComments)
+  _ <- skipComments -- if there is no charset, import, or namespace at rule we need this here.
+  rest <- rules
+  pure $ charset <> imports <> namespaces <> rest
+
+-- data AtRule = AtMedia (Maybe [MediaQuery])
+
+-- | Media Feature values. Per the
+-- <https://www.w3.org/TR/css3-mediaqueries/#values original spec (6.1)>,
+-- \<resolution\> only accepts dpi and dpcm units, dppx was added later.
+-- However, the w3c validator considers it valid, so we make no exceptions.
+{-
+data MediaFeatureValue = MFV_Length Length
+                       | MFV_Ratio Ratio
+                       | MFV_Number Number
+                       | MFV_Resolution Resolution
+                       | MFV_Other Text
+-}
+
+-- | Specs:
+--
+-- https://drafts.csswg.org/mediaqueries-3/#syntax
+-- https://www.w3.org/TR/css3-mediaqueries/
+-- https://www.w3.org/TR/CSS21/grammar.html
+--
+-- Implementation based on mozilla's pseudo BNF:
+-- https://developer.mozilla.org/en-US/docs/Web/CSS/Media_Queries/Using_media_queries
+mediaQueryList :: Parser [MediaQuery]
+mediaQueryList = lexeme ((:) <$> mediaQuery <*> many (char ',' *> skipComments *> mediaQuery))
+
+mediaQuery :: Parser MediaQuery
+mediaQuery = mediaQuery1 <|> mediaQuery2
+  where mediaQuery1 = MediaQuery1 <$> optionalNotOrOnly <*> mediaType <*> andExpressions
+        mediaQuery2 = MediaQuery2 <$> ((:) <$> expression <*> andExpressions)
+        mediaType = lexeme ident
+        andExpressions = many (h *> expression)
+        h = skipComments *> asciiCI "and" *> satisfy C.isSpace *> skipComments
+        optionalNotOrOnly = option mempty (asciiCI "not" <|> asciiCI "only")
+
+expression :: Parser Expression
+expression = char '(' *> skipComments *> (expr <|> expFallback)
+  where expr = do
+             e <- ident <* skipComments
+             v <- option Nothing (char ':' *> lexeme (Just <$> value))
+             _ <- char ')'
+             pure $ Expression e v
+        expFallback = InvalidExpression <$> A.takeWhile (/= ')') <* char ')'
+
+
+
+-- Note: The code below pertains to CSS Media Queries Level 4.
+-- Since it is still too new and nobody implements it (afaik),
+-- I leave it here for future reference, when the need to cater for it
+-- arrives.
+{-
+mediaCondition = asciiCI "not"
+              <|> asciiCI "and"
+              <|> asciiCI "or"
+              <|> mediaInParens
+
+mediaInParens = char '(' *> skipComments *> mediaCondition <* skipComments <* char ')'
+
+mediaFeature :: Parser MediaFeature
+mediaFeature = mfPlain <|> mfRange <|> mfBoolean
+  where mfBoolean = ident
+
+mfRange = ident
+
+data MediaFeature = MFPlain Text Value
+                  | MFBoolean Text
+                  | MFRange Range
+
+data Range = Range1 Text RangeOp Value
+           | Range2 Value RangeOp Text
+           | Range3 Value RangeOp Text RangeOp Value
+
+data RangeOp = LTOP | GTOP | EQOP | GEQOP | LEQOP
+
+-- = <mf-name> [ '<' | '>' ]? '='? <mf-value>
+--            | <mf-value> [ '<' | '>' ]? '='? <mf-name>
+--            | <mf-value> '<' '='? <mf-name> '<' '='? <mf-value>
+--            | <mf-value> '>' '='? <mf-name> '>' '='? <mf-value>
+
+mfPlain = ident *> skipComments *> char ':' *> skipComments *> mfValue
+
+mfValue :: Parser Value
+mfValue = number <|> dimension <|> ident <|> ratio
+
+-- TODO check if both integers are positive (required by the spec)
+ratio :: Parser Value
+ratio = do
+    n <- digits
+    _ <- skipComments *> char '/' <* skipComments
+    m <- digits
+    pure $ Other (mconcat [n, "/", m])
+
+
+
+-- expr
+--  : term [ operator? term ]*
+--  ;
+--
+--term
+-- : unary_operator?
+--    [ NUMBER S* | PERCENTAGE S* | LENGTH S* | EMS S* | EXS S* | ANGLE S* |
+--      TIME S* | FREQ S* ]
+--  | STRING S* | IDENT S* | URI S* | hexcolor | function
+--  ;
+--
+
+data MediaFeatureType = Range | Discrete
+
+t =
+  [("width",               Range)
+  ,("height",              Range)
+  ,("aspect-ratio",        Range)
+  ,("orientation",         Discrete)
+  ,("resolution",          Range)
+  ,("scan",                Discrete)
+  ,("grid",                Discrete)
+  ,("update",              Discrete)
+  ,("overflow-block",      Discrete)
+  ,("overflow-inline",     Discrete)
+  ,("color",               Range)
+  ,("color-index",         Range)
+  ,("monochrome",          Range)
+  ,("color-gamut",         Discrete)
+  ,("pointer",             Discrete)
+  ,("hover",               Discrete)
+  ,("any-pointer",         Discrete)
+  ,("any-hover",           Discrete)
+  ,("scripting",           Discrete)
+  ,("device-width",        Range)
+  ,("device-height",       Range)
+  ,("device-aspect-ratio", Range)
+  ]
+-}
diff --git a/src/Hasmin/Parser/Utils.hs b/src/Hasmin/Parser/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Parser/Utils.hs
@@ -0,0 +1,125 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Parser.Internal
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Parser.Utils (
+    ident
+    , fontfamilyname
+    , nonquotedurl
+    , skipComments
+    , lexeme
+    , functionParser
+    , comma
+    , colon
+    , opt
+    , nmchar
+    ) where
+
+import Control.Applicative ((<|>), many)
+import Control.Monad (void, mzero)
+import Data.Attoparsec.Text (char,
+  option, Parser, satisfy, skipSpace, string, takeWhile1, (<?>))
+import Data.Monoid ((<>))
+import Data.Text (Text)
+import Data.Text.Lazy.Builder as LB
+import qualified Data.Attoparsec.Text as A
+import qualified Data.Char as C
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+
+-- | Skip whatever comments and whitespaces are found.
+skipComments :: Parser ()
+skipComments = void $ many (skipSpace *> comment) <* skipSpace
+
+comment :: Parser Text
+comment = mappend <$> string "/*" <*> (string "*/" <|> untilAsterisk)
+  where untilAsterisk = mappend <$> A.takeWhile (/= '*') <*> checkAsterisk
+        checkAsterisk = mappend <$> string "*" <*> (string "/" <|> untilAsterisk)
+
+comma :: Parser Char
+comma = lexeme $ char ','
+
+colon :: Parser Char
+colon = lexeme $ char ':'
+
+lexeme :: Parser a -> Parser a
+lexeme p = skipComments *> p <* skipComments
+
+opt :: Monoid m => Parser m -> Parser m
+opt = option mempty
+
+nonquotedurl :: Parser Text
+nonquotedurl = do
+    t <- many (escape <|> (LB.singleton <$> satisfy validChar))
+    pure $ TL.toStrict (toLazyText (mconcat t))
+  where validChar x = x /= '\"' && x /= '\'' && x /= '(' && x /= ')'
+                   && x /= '\\' && notWhitespace x && notNonprintable x
+        notWhitespace x = x /= '\n' &&  x /= '\t' && x /= ' '
+        notNonprintable x = not (C.chr 0 <= x && x <= C.chr 8)
+                         && x /= '\t'
+                         && not ('\SO' <= x && x <= C.chr 31)
+                         && x /= '\DEL'
+
+fontfamilyname :: Parser Text
+fontfamilyname = do
+    i  <- ident
+    is <- many (skipComments *> ident)
+    if T.toLower i `elem` invalidNames
+       then mzero
+       else pure $ i <> mconcat (map (" "<>) is)
+  where invalidNames = ["serif", "sans-serif", "monospace", "cursive",
+                        "fantasy", "inherit", "initial", "unset", "default"]
+
+-- ident: -?{nmstart}{nmchar}*
+ident :: Parser Text
+ident = do
+    dash <- option mempty (LB.singleton <$> char '-')
+    ns   <- nmstart
+    nc   <- mconcat <$> many nmchar
+    pure $ TL.toStrict (toLazyText (dash <> ns <> nc))
+
+-- nmstart: [_a-z]|{nonascii}|{escape}
+nmstart :: Parser Builder
+nmstart = LB.singleton <$> satisfy (\c -> C.isAlpha c || (not . C.isAscii) c || c == '_')
+       <|> escape
+       <?> "not an nmstart token: [_a-z]|{nonascii}|{escape}"
+
+-- nmchar: [_a-z0-9-]|{nonascii}|{escape}
+nmchar :: Parser Builder
+nmchar = LB.singleton <$> satisfy cond <|> escape
+  where cond x = C.isAlphaNum x || x == '_' || x == '-'
+              || (not . C.isAscii) x
+
+-- TODO combine with unicode to make it more efficient
+-- escape: {unicode}|\\[^\n\r\f0-9a-f]
+escape :: Parser Builder
+escape =  unicode
+      <|> (mappend <$> (LB.singleton <$> char '\\') <*> (LB.singleton <$> satisfy cond))
+      <?> "not an escape token: {unicode}|\\\\[^\\n\\r\\f0-9a-f]"
+  where cond c = c /= '\n'
+              && c /= '\r'
+              && c /= '\f'
+              && (not . C.isHexDigit) c
+
+-- unicode        \\[0-9a-f]{1,6}(\r\n|[ \n\r\t\f])?
+unicode :: Parser Builder
+unicode = do
+    backslash <- char '\\'
+    hexChars  <- takeWhile1 C.isHexDigit
+    _         <- opt (string "\r\n" <|> (T.singleton <$> satisfy ws))
+    if T.length hexChars <= 6
+       then pure $ LB.singleton backslash <> LB.fromText hexChars
+       else fail "unicode escaped character with length greater than 6"
+  where ws x = x == ' ' || x == '\n' || x == '\r' || x == '\t' || x == '\f'
+
+-- | Assumes the identifier and the left parenthesis have been parsed
+-- Parses p, ignoring comments before and after it, and consumes the final
+-- right parenthesis
+functionParser :: Parser a -> Parser a
+functionParser p = lexeme p <* char ')'
diff --git a/src/Hasmin/Parser/Value.hs b/src/Hasmin/Parser/Value.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Parser/Value.hs
@@ -0,0 +1,1184 @@
+{-# LANGUAGE OverloadedStrings, TupleSections #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Parser.Internal
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- Parsers for CSS values.
+--
+-----------------------------------------------------------------------------
+module Hasmin.Parser.Value (
+      values
+    , percentage
+    , value
+    , valuesFallback
+    , stringOrUrl
+    , url
+    , stringtype
+    , digits
+    , textualvalue
+    , stringvalue
+    , shadowList
+    , repeatStyle
+    , position
+    , color
+    , number
+    , fontStyle
+    ) where
+
+import Control.Applicative ((<|>), many, liftA2, liftA3)
+import Control.Arrow (first, (&&&))
+import Control.Monad (mzero)
+import Data.Functor (($>))
+import Data.Attoparsec.Text (asciiCI, char, choice, count, many1,
+  option, Parser, satisfy, skipSpace, string, digit)
+import Data.Map.Strict (Map)
+import Data.Monoid ((<>))
+import Data.Maybe (fromMaybe, isNothing)
+import Data.Text (Text)
+import Data.Word (Word8)
+import Data.Char (isAscii)
+import Text.Parser.Permutation ((<|?>), (<$$>), (<$?>), (<||>), permute)
+import qualified Data.Set as Set
+import Numeric (readSigned, readFloat)
+import qualified Data.Attoparsec.Text as A
+import qualified Data.Char as C
+import qualified Data.Map.Strict as Map
+import qualified Data.Text as T
+
+import Hasmin.Parser.Utils
+import Hasmin.Types.BgSize
+import Hasmin.Types.Class
+import Hasmin.Types.Color
+import Hasmin.Types.Dimension
+import Hasmin.Types.FilterFunction
+import Hasmin.Types.Gradient
+import Hasmin.Types.Numeric
+import Hasmin.Types.PercentageLength
+import Hasmin.Types.Position
+import Hasmin.Types.RepeatStyle
+import Hasmin.Types.Shadow
+import Hasmin.Types.String
+import Hasmin.Types.TimingFunction
+import Hasmin.Types.TransformFunction
+import Hasmin.Types.Value
+
+values :: Text -> Parser Values
+values p = case Map.lookup (T.toLower p) propertyValueParsersMap of
+             Just x  -> x <* skipComments -- mappend <$> (x <|> ((:[]) <$> csswideKeyword))
+             Nothing -> valuesFallback
+
+number :: Parser Number
+number = Number <$> rational
+
+-- ---------------------------------------------------------------------------
+-- Color Parsers
+-- ---------------------------------------------------------------------------
+
+-- Assumes "rgb(" has already been read
+rgb :: Parser Color
+rgb = functionParser (rgbInt <|> rgbPer)
+  where rgbInt = mkRGBInt <$> word8 <* comma <*> word8 <* comma <*> word8
+        rgbPer = mkRGBPer <$> percentage <* comma
+                          <*> percentage <* comma <*> percentage
+
+-- Assumes "rgba(" has already been read
+rgba :: Parser Color
+rgba = functionParser (rgbaInt <|> rgbaPer)
+  where rgbaInt = mkRGBAInt <$> word8 <* comma <*> word8 <* comma
+                            <*> word8 <* comma <*> alphavalue
+        rgbaPer = mkRGBAPer <$> percentage <* comma <*> percentage <* comma
+                            <*> percentage <* comma <*> alphavalue
+
+-- Assumes "hsl(" has already been read
+hsl :: Parser Color
+hsl = functionParser p
+  where p = mkHSL <$> int <* comma <*> percentage <* comma <*> percentage
+
+-- Assumes "hsla(" has already been read
+hsla :: Parser Color
+hsla = functionParser p
+  where p = mkHSLA <$> int <* comma <*> percentage <* comma
+                   <*> percentage <* comma <*> alphavalue
+
+alphavalue :: Parser Alphavalue
+alphavalue = mkAlphavalue <$> rational
+
+hexvalue :: Parser Value
+hexvalue = ColorV <$> hex
+
+-- TODO improve this parser
+hex :: Parser Color
+hex = char '#' *> (constructHex <$> choice ps)
+  where hexadecimal = satisfy C.isHexDigit
+        ps = [count 8 hexadecimal, count 6 hexadecimal,
+              count 4 hexadecimal, count 3 hexadecimal]
+        constructHex [r,g,b] = mkHex3 r g b
+        constructHex [r,g,b,a] = mkHex4 r g b a
+        constructHex [r1,r2,g1,g2,b1,b2] = mkHex6 [r1,r2] [g1,g2] [b1,b2]
+        constructHex [r1,r2,g1,g2,b1,b2,a1,a2] = mkHex8 [r1,r2] [g1,g2] [b1,b2] [a1,a2]
+        constructHex _ = error "invalid list size for a hex color"
+
+-- ---------------------------------------------------------------------------
+-- Dimensions Parsers
+-- ---------------------------------------------------------------------------
+
+-- A map relating dimension units and the percentage symbol,
+-- to functions that construct that value. Meant to unify all the numerical
+-- parsing in a single parse for generality without losing much efficiency.
+-- See numericalvalue.
+numericalConstructorsMap :: Map Text (Number -> Value)
+numericalConstructorsMap   = Map.fromList $ fmap (first T.toCaseFold) l
+  where durationFunc u v   = DurationV (Duration v u)
+        frequencyFunc u v  = FrequencyV (Frequency v u)
+        resolutionFunc u v = ResolutionV (Resolution v u)
+        l = [("s",    durationFunc S)
+            ,("ms",   durationFunc Ms)
+            ,("hz",   frequencyFunc Hz)
+            ,("khz",  frequencyFunc Khz)
+            ,("dpi",  resolutionFunc Dpi)
+            ,("dpcm", resolutionFunc Dpcm)
+            ,("dppx", resolutionFunc Dppx)
+            ,("%", \x -> PercentageV (Percentage $ toRational x))
+            ] ++ (fmap . fmap) (DistanceV .) distanceConstructorsList
+              ++ (fmap . fmap) (AngleV .) angleConstructorsList
+
+-- Unified numerical parser.
+-- Parses <number>, dimensions (i.e. <length>, <angle>, ...), and <percentage>
+numericalvalue :: Parser Value
+numericalvalue = do
+    n    <- number
+    rest <- opt (string "%" <|> A.takeWhile1 C.isAlpha)
+    if T.null rest  -- if true, then it was just a <number> value
+       then pure $ NumberV n
+       else case Map.lookup (T.toCaseFold rest) numericalConstructorsMap of
+              Just f  -> pure $ f n
+              Nothing -> mzero -- TODO see if we should return an "Other" value
+
+-- Create a numerical parser based on a Map.
+-- See for instance, the "angle" parser
+dimensionParser :: Map Text (Number -> a) -> a -> Parser a
+dimensionParser m unitlessValue = do
+    n <- number
+    u <- opt (A.takeWhile1 C.isAlpha)
+    if T.null u
+       then if n == 0
+               then pure unitlessValue -- <angle> 0, without units
+               else mzero -- Non-zero <number>, fail
+       else case Map.lookup (T.toCaseFold u) m of
+              Just f  -> pure $ f n
+              Nothing -> mzero -- parsed units aren't angle units, fail
+
+distance :: Parser Distance
+distance = dimensionParser distanceConstructorsMap (Distance 0 Q)
+  where distanceConstructorsMap = Map.fromList distanceConstructorsList
+
+angle :: Parser Angle
+angle = dimensionParser angleConstructorsMap (Angle 0 Deg)
+  where angleConstructorsMap = Map.fromList angleConstructorsList
+
+duration :: Parser Duration
+duration = do
+    n <- number
+    u <- opt (A.takeWhile1 C.isAlpha)
+    if T.null u
+       then mzero
+       else case Map.lookup (T.toCaseFold u) durationConstructorsMap of
+              Just f  -> pure $ f n
+              Nothing -> mzero -- parsed units aren't angle units, fail
+  where durationConstructorsMap = Map.fromList $
+            fmap (toText &&& flip Duration) [S, Ms]
+
+angleConstructorsList :: [(Text, Number -> Angle)]
+angleConstructorsList = fmap (toText &&& flip Angle)
+    [Deg, Grad, Rad, Turn]
+
+distanceConstructorsList :: [(Text, Number -> Distance)]
+distanceConstructorsList = fmap (toText &&& flip Distance)
+    [EM, EX, CH, VH, VW, VMIN, VMAX, REM, Q, CM, MM, IN, PC, PT, PX]
+
+percentage :: Parser Percentage
+percentage = Percentage <$> rational <* char '%'
+
+-- ---------------------------------------------------------------------------
+-- Primitives
+-- ---------------------------------------------------------------------------
+
+-- | \<integer\> data type parser, but into a String instead of an Int, for other
+-- parsers to use (e.g.: see the parsers int, or rational)
+int' :: Parser String
+int' = do
+  sign <- option '+' (char '-' <|> char '+')
+  d    <- digits
+  case sign of
+    '+' -> pure d
+    '-' -> pure (sign:d)
+    _   -> error "int': parsed a number starting with other than [+|-]"
+
+-- | Parser for \<integer\>: [+|-][0-9]+
+int :: Parser Int
+int = read <$> int'
+
+digits :: Parser String
+digits = many1 digit
+
+word8 :: Parser Word8
+word8 = read <$> digits
+
+-- Note that many properties that allow an integer or real number as a value
+-- actually restrict the value to some range, often to a non-negative value.
+--
+-- | Real numbers parser. \<number\>: <'int' integer> | [0-9]*.[0-9]+
+rational :: Parser Rational
+rational = do
+    sign <- option [] (wrapMinus <$> (char '-' <|> char '+'))
+    dgts <- ((++) <$> digits <*> option "" fractionalPart)
+           <|> ("0"++) <$> fractionalPart -- append a zero for read not to fail
+    e <- option [] expo
+    pure $ fst . head $ readSigned readFloat (sign ++ dgts ++ e)
+  where fractionalPart = (:) <$> char '.' <*> digits
+        expo = (:) <$> satisfy (\c -> c == 'e' || c == 'E') <*> int'
+        wrapMinus x = if x == '-' -- we use this since read fails with leading '+'
+                         then [x]
+                         else []
+
+fontStyle :: Parser Value
+fontStyle = do
+    k <- ident
+    if Set.member k keywords
+       then pure $ mkOther k
+       else mzero
+  where keywords = Set.fromList ["normal", "italic", "oblique"]
+
+{-
+fontWeight :: Parser Value
+fontWeight = do
+    k <- ident
+    if Set.member k keywords
+       then pure $ mkOther k
+       else mzero
+  where keywords = Set.fromList ["normal", "bold", "lighter", "bolder"]
+        validNumbers = Set.fromList [100, 200, 300, 400, 500, 600, 700, 800, 900]
+-}
+
+fontSize :: Parser Value
+fontSize = fontSizeKeyword
+        <|> (DistanceV <$> distance)
+        <|> (PercentageV <$> percentage)
+  where fontSizeKeyword = do
+            v1 <- ident
+            if Set.member v1 keywords
+               then pure $ mkOther v1
+               else mzero
+        keywords = Set.fromList ["medium", "xx-small", "x-small", "small"
+                                ,"large", "x-large", "xx-large", "smaller"
+                                ,"larger"]
+
+{- [ [ <‘font-style’> || <font-variant-css21> || <‘font-weight’> ||
+ - <‘font-stretch’> ]? <‘font-size’> [ / <‘line-height’> ]? <‘font-family’> ] |
+ - caption | icon | menu | message-box | small-caption | status-bar
+where <font-variant-css21> = [normal | small-caps]
+
+-}
+
+-- TODO clean this parser!!
+positionvalue :: Parser Value
+positionvalue = PositionV <$> position
+
+position :: Parser Position
+position = pos4 <|> pos2 <|> pos1
+
+pos1 :: Parser Position
+pos1 =  (asciiCI "left" $> (f $ Just PosLeft))
+    <|> (asciiCI "center" $> (f $ Just PosCenter))
+    <|> (asciiCI "right" $> (f $ Just PosRight))
+    <|> (asciiCI "top" $> (f $ Just PosTop))
+    <|> (asciiCI "bottom" $> (f $ Just PosBottom))
+    <|> ((\a -> Position Nothing a Nothing Nothing) <$> (Just <$> percentageLength))
+  where f x = Position x Nothing Nothing Nothing
+
+pos2 :: Parser Position
+pos2 = firstx <|> firsty
+  where firstx = do
+            a <- (asciiCI "left" $> (Position (Just PosLeft) Nothing))
+                 <|> (asciiCI "center" $> (Position (Just PosCenter) Nothing))
+                 <|> (asciiCI "right" $> (Position (Just PosRight) Nothing))
+                 <|> ((Position Nothing . Just) <$> percentageLength)
+            skipComments *> ((asciiCI "top" $> (a (Just PosTop) Nothing))
+                 <|> (asciiCI "center" $> (a (Just PosCenter) Nothing))
+                 <|> (asciiCI "bottom" $> (a (Just PosBottom) Nothing))
+                 <|> ((a Nothing . Just) <$> percentageLength))
+        firsty = do
+            a <- (asciiCI "top" $> (Position (Just PosTop) Nothing))
+                 <|> (asciiCI "center" $> (Position (Just PosCenter) Nothing))
+                 <|> (asciiCI "bottom" $> (Position (Just PosBottom) Nothing))
+                 <|> ((Position Nothing . Just) <$> percentageLength)
+            skipComments *> ((asciiCI "left" $> (a (Just PosLeft) Nothing))
+                 <|> (asciiCI "center" $> (a (Just PosCenter) Nothing))
+                 <|> (asciiCI "right" $> (a (Just PosRight) Nothing))
+                 <|> ((a Nothing . Just) <$> percentageLength))
+
+pos4 :: Parser Position
+pos4 = firstx <|> firsty
+  where posTop    = asciiCI "top" $> (Position (Just PosTop))
+        posRight  = asciiCI "right" $> (Position (Just PosRight))
+        posBottom = asciiCI "bottom" $> (Position (Just PosBottom))
+        posLeft   = asciiCI "left" $> (Position (Just PosLeft))
+        firstx    = do
+            x <- (asciiCI "center" $> (Position (Just PosCenter) Nothing))
+                 <|> ((posLeft <|> posRight) <*> (skipComments *> option Nothing (Just <$> percentageLength)))
+            _ <- skipComments
+            (asciiCI "center" $> (x (Just PosCenter) Nothing))
+                <|> (((asciiCI "top" $> (x $ Just PosTop)) <|> (asciiCI "bottom" $> (x (Just PosBottom))))
+                    <*> (skipComments *> option Nothing (Just <$> percentageLength)))
+        firsty = do
+            x <- (asciiCI "center" $> (Position (Just PosCenter) Nothing))
+                 <|> ((posTop <|> posBottom) <*> (skipComments *> option Nothing (Just <$> percentageLength)))
+            _ <- skipComments
+            (asciiCI "center" $> (x (Just PosCenter) Nothing))
+                <|> (((asciiCI "left" $> (x $ Just PosLeft)) <|> (asciiCI "right" $> (x (Just PosRight))))
+                    <*> (skipComments *> option Nothing (Just <$> percentageLength)))
+
+{-
+transformOrigin :: Parser Values
+transformOrigin = twoVal <|> oneVal
+  where oneVal = (singleValue numericalvalue) <|> offsetKeyword
+        offsetKeyword = do
+          v1 <- ident
+          if v1 == "top" || v1 == "right" || v1 == "bottom" || v1 == "left" || v1 == "center"
+             then pure $ mkValues [mkOther v1]
+             else mzero
+        twoVal = do
+          (v1, v2) <- ((,) <$> yAxis <*> (skipComments *> (xAxis <|> numericalvalue)))
+                    <|> ((,) <$> xAxis <*> (skipComments *> (yAxis <|> numericalvalue)))
+                    <|> ((,) <$> numericalvalue <*> (skipComments *> (yAxis <|> xAxis)))
+          option (mkValues [v1,v2]) ((\x -> mkValues [v1,v2, DistanceV x]) <$> distance)
+        yAxis = do
+          v1 <- ident
+          if v1 == "top" || v1 == "bottom" || v1 == "center"
+             then pure $ mkOther v1
+             else mzero
+        xAxis = do
+          v1 <- ident
+          if v1 == "right" || v1 == "left" || v1 == "center"
+             then pure $ mkOther v1
+             else mzero
+-}
+
+bgSize :: Parser BgSize
+bgSize = twovaluesyntax <|> contain <|> cover
+  where cover   = asciiCI "cover" $> Cover
+        contain = asciiCI "contain" $> Contain
+        twovaluesyntax = do
+            v1 <- (Left <$> percentageLength) <|> (Right <$> auto)
+            _  <- skipComments
+            v2 <- option Nothing (Just <$> ((Left <$> percentageLength) <|> (Right <$> auto)))
+            pure $ BgSize v1 v2
+
+bgAttachment :: Parser TextV
+bgAttachment = matchKeywords ["scroll", "fixed", "local"]
+
+box :: Parser TextV
+box = matchKeywords ["border-box", "padding-box", "content-box"]
+
+-- [ <bg-layer> , ]* <final-bg-layer>
+background :: Parser Values
+background = do
+    xs <- many (bgLayer <* char ',' <* skipComments)
+    x  <- finalBgLayer
+    pure $ if null xs
+              then Values x []
+              else Values (head xs) (fmap (Comma,) $ tail xs ++ [x])
+
+-- <final-bg-layer> = <bg-image> || <position> [ / <bg-size> ]? || <repeat-style> || <attachment> || <box> || <box> || <'background-color'>
+finalBgLayer :: Parser Value
+finalBgLayer = do
+    layer <- permute (mkFinalBgLayer <$?> (Nothing, Just <$> image <* skipComments)
+                                     <|?> (Nothing, Just <$> positionAndBgSize <* skipComments)
+                                     <|?> (Nothing, Just <$> repeatStyle <* skipComments)
+                                     <|?> (Nothing, Just <$> bgAttachment <* skipComments)
+                                     <|?> (Nothing, Just <$> box <* skipComments)
+                                     <|?> (Nothing, Just <$> box <* skipComments)
+                                     <|?> (Nothing, Just <$> color <* skipComments))
+    if finalBgLayerIsEmpty layer
+       then mzero
+       else pure layer
+  where finalBgLayerIsEmpty (FinalBgLayer Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing) = True
+        finalBgLayerIsEmpty _                                                                              = False
+        -- parameters e and f are being swapped to deal with the permutation
+        -- changing the original order of the parsed values.
+        mkFinalBgLayer a Nothing c d e f g = FinalBgLayer a Nothing Nothing c d f e g
+        mkFinalBgLayer a (Just (p,s)) c d e f g = FinalBgLayer a (Just p) s c d f e g
+
+-- <bg-layer> =       <bg-image> || <position> [ / <bg-size> ]? || <repeat-style> || <attachment> || <box>{1,2}
+bgLayer :: Parser Value
+bgLayer = do
+    layer <- permute (mkBgLayer <$?> (Nothing, Just <$> image <* skipComments)
+                                <|?> (Nothing, Just <$> positionAndBgSize <* skipComments)
+                                <|?> (Nothing, Just <$> repeatStyle <* skipComments)
+                                <|?> (Nothing, Just <$> bgAttachment <* skipComments)
+                                <|?> (Nothing, Just <$> box2 <* skipComments))
+    if bgLayerIsEmpty layer
+       then mzero
+       else pure layer
+  where bgLayerIsEmpty (BgLayer Nothing Nothing Nothing Nothing Nothing Nothing Nothing) = True
+        bgLayerIsEmpty _                                                                 = False
+        mkBgLayer a Nothing c d Nothing           = BgLayer a Nothing Nothing c d Nothing Nothing
+        mkBgLayer a (Just (p,s)) c d Nothing      = BgLayer a (Just p) s c d Nothing Nothing
+        mkBgLayer a Nothing c d (Just (i,j))      = BgLayer a Nothing Nothing c d (Just i) j
+        mkBgLayer a (Just (p,s)) c d (Just (i,j)) = BgLayer a (Just p) s c d (Just i) j
+        box2 :: Parser (TextV, Maybe TextV)
+        box2 = do
+            x <- box <* skipComments
+            y <- option Nothing (Just <$> box)
+            pure (x,y)
+
+-- used for the background property, which takes among other things:
+-- <position> [ / <bg-size> ]?
+positionAndBgSize :: Parser (Position, Maybe BgSize)
+positionAndBgSize = do
+    x <- position <* skipComments
+    y <- option Nothing (Just <$> (char '/' *> skipComments *> bgSize))
+    pure (x,y)
+
+matchKeywords :: [Text] -> Parser TextV
+matchKeywords listOfKeywords = do
+    i <- ident
+    if T.toCaseFold i `elem` Set.fromList listOfKeywords
+       then pure $ TextV i
+       else mzero
+
+-- <image> = <url> | <image()> | <image-set()> | <element()> | <cross-fade()> | <gradient>
+image :: Parser Value
+image = do
+    i <- ident
+    let lowercased = T.toLower i
+    if lowercased == "none"
+       then pure $ mkOther "none"
+       else do _ <- char '('
+               if Set.member lowercased possibilities
+                  then fromMaybe mzero (Map.lookup lowercased functionsMap)
+                  else mzero
+  where possibilities = Set.fromList $ map T.toCaseFold
+            ["url", "element", "linear-gradient", "radial-gradient"]
+
+transition :: Parser Values
+transition = parseCommaSeparated singleTransition
+
+-- [ none | <single-transition-property> ] || <time> || <single-transition-timing-function> || <time>
+singleTransition :: Parser Value
+singleTransition = do
+    st <- permute (mkSingleTransition <$?> (Nothing, Just <$> singleTransitionProperty <* skipComments)
+                                      <|?> (Nothing, Just <$> duration <* skipComments)
+                                      <|?> (Nothing, Just <$> timingFunction <* skipComments)
+                                      <|?> (Nothing, Just <$> duration <* skipComments))
+    if singleTransitionIsEmpty st
+       then mzero
+       else pure st
+  where singleTransitionIsEmpty (SingleTransition Nothing Nothing Nothing Nothing) = True
+        singleTransitionIsEmpty _                                                  = False
+        -- Needed because permute is swapping the order of the time parameters.
+        mkSingleTransition a b c d = SingleTransition a d c b
+        singleTransitionProperty = do
+            i <- ident
+            let lowercased = T.toLower i
+            if Set.member lowercased excludedKeywords
+               then mzero
+               else pure $ TextV i
+        excludedKeywords = Set.fromList ["initial", "inherit", "unset", "default", "none"]
+
+timingFunction :: Parser TimingFunction
+timingFunction = do
+    i <- ident
+    let lowercased = T.toLower i
+    case Map.lookup lowercased timingFunctionKeywords of
+      Just x -> x
+      Nothing -> char '(' *> (if lowercased == "steps"
+                                 then steps
+                                 else if lowercased == "cubic-bezier"
+                                         then cubicbezier
+                                         else mzero)
+  where timingFunctionKeywords = Map.fromList [("ease",        pure Ease)
+                                              ,("ease-in",     pure EaseIn)
+                                              ,("ease-in-out", pure EaseInOut)
+                                              ,("ease-out",    pure EaseOut)
+                                              ,("linear",      pure Linear)
+                                              ,("step-end",    pure StepEnd)
+                                              ,("step-start",  pure StepStart)]
+
+backgroundSize :: Parser Values
+backgroundSize = parseCommaSeparated (BgSizeV <$> bgSize)
+
+auto :: Parser Auto
+auto = asciiCI "auto" $> Auto
+
+propertyValueParsersMap :: Map Text (Parser Values)
+propertyValueParsersMap = Map.fromList
+    [--("color",           (:[]) <$> colorvalue)
+    ("display",                     singleValue textualvalue)
+    ,("font",                       singleValue font)
+    ,("font-size",                  singleValue fontSize)
+    ,("font-style",                 singleValue fontStyle)
+    ,("font-weight",                singleValue numberOrText)
+    ,("font-family",                fontFamilyValues)
+    ,("background",                 background)
+    ,("background-repeat",          parseCommaSeparated (RepeatStyleV <$> repeatStyle))
+    ,("background-size",            backgroundSize)
+    ,("box-shadow",                 shadowList)
+    ,("-o-box-shadow",              shadowList)
+    ,("-moz-box-shadow",            shadowList)
+    ,("-webkit-box-shadow",         shadowList)
+    ,("text-shadow",                textShadow)
+
+    ,("animation",                  animation)
+    ,("-o-animation",               animation)
+    ,("-ms-animation",              animation)
+    ,("-moz-animation",             animation)
+    ,("-webkit-animation",          animation)
+
+    ,("transition",                 transition)
+    ,("-o-transition",              transition)
+    ,("-ms-transition",             transition)
+    ,("-moz-transition",            transition)
+    ,("-webkit-transition",         transition)
+
+    ,("height",                     singleValue numberOrText) -- <length>|<percentage>|auto
+    ,("margin-bottom",              singleValue numberOrText)
+    ,("margin-left",                singleValue numberOrText)
+    ,("background-position",        singleValue positionvalue)
+    ,("perspective-origin",         singleValue positionvalue)
+    ,("-o-perspective-origin",      singleValue positionvalue)
+    ,("-moz-perspective-origin",    singleValue positionvalue)
+    ,("-webkit-perspective-origin", singleValue positionvalue)
+    ,("mask-position",              positionList)
+    ,("-webkit-mask-position",      positionList)
+    -- ,("transform-origin",           transformOrigin)
+    -- ,("-ms-transform-origin",       transformOrigin)
+    -- ,("-webkit-transform-origin",   transformOrigin)
+    -- ,("font",
+    -- ,("margin-right",   numberOrText)
+    -- ,("margin-top",     numberOrText)
+    -- ,("opacity",        someNumericalValue)
+    -- ,("overflow",       someText)
+    -- ,("padding-bottom", someNumericalValue)
+    -- ,("padding-left",   someNumericalValue)
+    -- ,("padding-right",  someNumericalValue)
+    -- ,("padding-top",    someNumericalValue)
+    -- ,("position",       someText)
+    -- ,("text-align",     someText)
+    -- ,("width",          numberOrText)
+    ]
+  where numberOrText = numericalvalue <|> textualvalue
+
+-- helper wrapper
+singleValue :: Parser Value -> Parser Values
+singleValue = (flip Values [] <$>)
+
+value :: Parser Value
+value =  textualvalue
+     <|> numericalvalue
+     <|> hexvalue
+     <|> (StringV <$> stringtype)
+     <|> invalidvalue
+
+-- | Parses until the end of the declaration, i.e. ';' or '}'
+-- Used to deal with invalid input, or some of the IE specific things
+invalidvalue :: Parser Value
+invalidvalue = mkOther <$> A.takeWhile1 (\c -> c /= '\\' && c /= ';' && c /= '}' && c /= '!')
+
+-- [ [ <'font-style'> || <font-variant-css21> || <'font-weight'> ||
+-- <'font-stretch'> ]? <'font-size'> [ / <'line-height'> ]? <'font-family'> ] |
+font :: Parser Value
+font = systemFonts <|> do
+    (fsty, fvar, fwgt, fstr) <- parse4
+    (fsz, lh)                <- fontSizeAndLineHeight
+    ff                       <- ((:) <$> fontfamily <* skipComments) <*> many (char ',' *> lexeme fontfamily)
+    pure $ FontV fsty fvar fwgt fstr fsz lh ff
+  where systemFonts = Other <$> parseIdents ["caption", "icon", "menu", "message-box", "small-caption", "status-bar"]
+        fontSizeAndLineHeight = do
+            fsz <- fontSize <* skipComments
+            lh  <- option Nothing (Just <$> (char '/' *> lexeme lineHeight))
+            pure (fsz, lh)
+        lineHeight = let validNum = do n <- numericalvalue
+                                       case n of
+                                         NumberV _     -> pure n
+                                         PercentageV _ -> pure n
+                                         DistanceV _   -> pure n
+                                         _             -> mzero
+                     in (Other <$> parseIdents ["normal"]) <|> validNum
+        fontWeightNumber :: Parser Value
+        fontWeightNumber = do
+            n <- number
+            if Set.notMember n (Set.fromList [100, 200, 300, 400, 500, 600, 700, 800, 900])
+               then mzero
+               else do c <- A.peekChar
+                       case c of
+                         Nothing -> mzero
+                         Just x  -> if isAscii x || x == '%'
+                                       then mzero
+                                       else pure $ NumberV n
+        parseFirstFour j = (storeProperty' j <$> fontWeightNumber <* skipComments) <|> do
+            i <- ident
+            case Map.lookup (T.toLower i) m of
+              Just x  -> case x of
+                           FontWeight -> pure $ storeProperty' j (mkOther i)
+                           _          -> pure $ storeProperty  j x (TextV i)
+              Nothing -> mzero
+          where m  = Map.fromList $ zip ["ultra-condensed", "extra-condensed", "condensed", "semi-condensed", "semi-expanded", "expanded", "extra-expanded", "ultra-expanded"] (repeat FontStretch)
+                                 ++ zip ["small-caps"] (repeat FontVariant)
+                                 ++ zip ["italic", "oblique"] (repeat FontStyle)
+                                 ++ zip ["bold", "bolder", "lighter"] (repeat FontWeight)
+                                 ++ [("normal", Ambiguous)]
+        parse4 :: Parser (Maybe TextV, Maybe TextV, Maybe Value, Maybe TextV)
+        parse4 = do
+            let initialized = (Nothing, Nothing, Nothing, Nothing, 0)
+            w <- option initialized (parseFirstFour initialized <* skipComments)
+            x <- option w (parseFirstFour w <* skipComments)
+            y <- option x (parseFirstFour x <* skipComments)
+            (a,b,c,d,e) <- option y (parseFirstFour y <* skipComments)
+            pure $ fillTuple (a,b,c,d) e
+        fillTuple (a,b,c,d) 0 = (a,b,c,d)
+        fillTuple (a,b,c,d) x
+            | isNothing a = fillTuple (Just nrml,b,c,d) (x-1)
+            | isNothing b = fillTuple (a, Just nrml,c,d) (x-1)
+            | isNothing c = fillTuple (a,b, Just $ Other nrml,d) (x-1)
+            | isNothing d = fillTuple (a,b,c, Just nrml) (x-1)
+            | otherwise   = (a,b,c,d)
+          where nrml = TextV "normal"
+
+data FontProperty = FontStyle | FontVariant | FontWeight | FontStretch | Ambiguous
+  deriving (Eq, Show)
+
+storeProperty :: (Maybe TextV, Maybe TextV, Maybe Value, Maybe TextV, Int) -> FontProperty -> TextV
+              -> (Maybe TextV, Maybe TextV, Maybe Value, Maybe TextV, Int)
+storeProperty (a,b,c,d,i) y x = replace y
+  where replace FontStyle   = (Just x,b,c,d,i)
+        replace FontVariant = (a, Just x,c,d,i)
+        replace FontStretch = (a,b,c, Just x,i)
+        replace Ambiguous   = (a,b,c,d,i + 1)
+        replace _           = (a,b,c,d,i)
+
+storeProperty' :: (Maybe TextV, Maybe TextV, Maybe Value, Maybe TextV, Int) -> Value
+               -> (Maybe TextV, Maybe TextV, Maybe Value, Maybe TextV, Int)
+storeProperty' (a,b,_,d,i) x = (a,b, Just x,d,i)
+
+
+-- font-style: normal
+-- font-variant: normal
+-- font-weight: normal
+-- font-stretch: normal
+-- font-size: medium
+-- line-height: normal
+-- font-family: depends on user agent
+
+fontFamilyValues :: Parser Values
+fontFamilyValues = singleValue csswideKeyword <|> do
+    v <- fontfamily
+    vs <- many ((,) <$> separator <*> fontfamily)
+    pure $ Values v vs
+
+fontfamily :: Parser Value
+fontfamily = (StringV <$> stringtype) <|> (mkOther <$> unquotedFontFamily)
+
+local :: Parser Value
+local = functionParser $
+    Local <$> ((Left <$> unquotedFontFamily) <|> (Right <$> stringtype))
+
+unquotedFontFamily :: Parser Text
+unquotedFontFamily = do
+    v  <- ident
+    vs <- many (skipComments *> ident)
+    pure $ v <> mconcat (map (T.singleton ' ' <>) vs)
+
+
+textualvalue :: Parser Value
+textualvalue = do
+    i <- ident
+    if i == "\\9" -- iehack
+       then mzero
+       else do c <- A.peekChar
+               case c of
+                 Just '(' -> functionParsers i
+                 Just ':' -> mzero -- invalid
+                 _        -> textualParsers i
+
+textualParsers :: Text -> Parser Value
+textualParsers i = let t = T.toCaseFold i
+                   in fromMaybe (pure $ mkOther i) (Map.lookup t textualParsersMap)
+  where textualParsersMap = Map.union csswideKeywordsMap namedColorsParsersMap
+
+csswideKeyword :: Parser Value
+csswideKeyword = do
+    i <- ident <* skipComments
+    let lowercased = T.toLower i
+    case Map.lookup lowercased csswideKeywordsMap of
+      Nothing -> mzero
+      Just x -> do c <- A.peekChar
+                   case c of
+                     Nothing -> x
+                     Just y -> if y `elem` ['!', ';', '}']
+                                  then x
+                                  else mzero
+
+csswideKeywordsMap :: Map Text (Parser Value)
+csswideKeywordsMap  = Map.fromList $ map (first T.toCaseFold)
+                                 [("initial", pure Initial)
+                                 ,("inherit", pure Inherit)
+                                 ,("unset",   pure Unset)]
+
+stringvalue :: Parser Value
+stringvalue = StringV <$> stringtype
+
+functionParsers :: Text -> Parser Value
+functionParsers i = char '(' *>
+    case Map.lookup (T.toCaseFold i) functionsMap of
+      Just x -> x <|> genericFunc i
+      Nothing -> genericFunc i
+                 <|> (mkOther <$> (f i "(" <$> someText <*> string ")"))
+  where f x y z w = x <> y <> z <> w
+        someText = A.takeWhile (/= ')')
+
+genericFunc :: Text -> Parser Value
+genericFunc i = (GenericFunc i <$> valuesInParens) <* char ')'
+  where valuesInParens = Values <$> v <*> many ((,) <$> separator <*> v) <* skipComments
+        v =  textualvalue
+         <|> numericalvalue
+         <|> hexvalue
+         <|> (StringV <$> stringtype)
+
+stringOrUrl :: Parser (Either StringType Url)
+stringOrUrl = (Left <$> stringtype) <|> (Right <$> someUrl)
+  where someUrl :: Parser Url
+        someUrl = asciiCI "url" *> char '(' *> url
+
+-- <repeat-style> parser, for background-repeat.
+repeatStyle :: Parser RepeatStyle
+repeatStyle = do
+  i <- ident
+  let lowercased = T.toLower i
+  case Map.lookup lowercased singleKeywords of
+    Nothing -> case Map.lookup lowercased keywordPairs of
+                 Nothing -> mzero
+                 Just y -> do j <- option Nothing secondKeyword
+                              pure $ RSPair y j
+    Just x -> pure x
+  where secondKeyword = do
+            z <- skipComments *> ident
+            case Map.lookup (T.toLower z) keywordPairs of
+              Nothing -> mzero
+              Just a -> pure $ Just a
+        singleKeywords = Map.fromList [("repeat-x", RepeatX), ("repeat-y", RepeatY)]
+        keywordPairs = Map.fromList [("repeat",    RsRepeat)
+                                    ,("no-repeat", RsNoRepeat)
+                                    ,("space",     RsSpace)
+                                    ,("round",     RsRound)]
+
+functionsMap :: Map Text (Parser Value)
+functionsMap = Map.fromList $ fmap (first T.toCaseFold)
+          [("rgb",                     ColorV <$> rgb)
+          ,("rgba",                    ColorV <$> rgba)
+          ,("hsl",                     ColorV <$> hsl)
+          ,("hsla",                    ColorV <$> hsla)
+          ,("url",                     UrlV <$> url)
+          ,("format",                  format)
+          ,("local",                   local)
+          -- <gradient> parsers
+          ,("linear-gradient",         GradientV "linear-gradient" <$> lineargradient)
+          ,("-o-linear-gradient",      GradientV "-o-linear-gradient" <$> lineargradient)
+          ,("-ms-linear-gradient",     GradientV "-ms-linear-gradient" <$> lineargradient)
+          ,("-moz-linear-gradient",    GradientV "-moz-linear-gradient" <$> lineargradient)
+          ,("-webkit-linear-gradient", GradientV "-webkit-linear-gradient" <$> lineargradient)
+          ,("radial-gradient",         GradientV "radial-gradient" <$> radialgradient)
+          ,("-o-radial-gradient",      GradientV "-o-radial-gradient" <$> radialgradient)
+          ,("-moz-radial-gradient",    GradientV "-moz-radial-gradient" <$> radialgradient)
+          ,("-webkit-radial-gradient", GradientV "-webkit-radial-gradient" <$> radialgradient)
+          --,("repeating-linear-gradient", ? ) -- TODO
+          --,("repeating-radial-gradient", ? ) -- TODO
+          -- <shape>
+          ,("rect",                    rect)
+          -- <transform-function>
+          ,("matrix",                  TransformV <$> matrix)
+          ,("matrix3d",                TransformV <$> matrix3d)
+          ,("rotate",                  (TransformV . Rotate) <$> functionParser angle)
+          ,("rotate3d",                TransformV <$> rotate3d)
+          ,("rotateX",                 (TransformV . Rotate) <$> functionParser angle)
+          ,("rotateY",                 (TransformV . Rotate) <$> functionParser angle)
+          ,("rotateZ",                 (TransformV . Rotate) <$> functionParser angle)
+          ,("scale",                   TransformV <$> scale)
+          ,("scale3d",                 TransformV <$> scale3d)
+          ,("scaleX",                  (TransformV . ScaleY) <$> functionParser number)
+          ,("scaleY",                  (TransformV . ScaleY) <$> functionParser number)
+          ,("scaleZ",                  (TransformV . ScaleZ) <$> functionParser number)
+          ,("skew",                    TransformV <$> skew)
+          ,("skewX",                   (TransformV . SkewX) <$> functionParser angle)
+          ,("skewY",                   (TransformV . SkewY) <$> functionParser angle)
+          ,("translate",               TransformV <$> translate)
+          ,("translate3d",             TransformV <$> translate3d)
+          ,("translateX",              (TransformV . TranslateX) <$> functionParser percentageLength)
+          ,("translateY",              (TransformV . TranslateY) <$> functionParser percentageLength)
+          ,("translateZ",              (TransformV . TranslateZ) <$> functionParser distance)
+          ,("perspective",             (TransformV . Perspective) <$> functionParser distance)
+          -- <timing-function>
+          ,("cubic-bezier",            TimingFuncV <$> cubicbezier)
+          ,("steps",                   TimingFuncV <$> steps)
+          -- <filter-function>
+          ,("blur",                    (FilterV . Blur) <$> functionParser distance)
+          ,("contrast",                (FilterV . Contrast) <$> functionParser numberPercentage)
+          ,("grayscale",               (FilterV . Grayscale) <$> functionParser numberPercentage)
+          ,("invert",                  (FilterV . Invert) <$> functionParser numberPercentage)
+          ,("opacity",                 (FilterV . Opacity) <$> functionParser numberPercentage)
+          ,("saturate",                (FilterV . Saturate) <$> functionParser numberPercentage)
+          ,("sepia",                   (FilterV . Sepia) <$> functionParser numberPercentage)
+          ,("brightness",              (FilterV . Brightness) <$> functionParser numberPercentage)
+          ,("drop-shadow",             FilterV <$> dropShadow)
+          ,("hue-rotate",              (FilterV . HueRotate) <$> functionParser angle)
+          ,("element", genericFunc "element")
+          --
+          -- <basic-shape>
+          -- circle() = circle( [<shape-radius>]? [at <position>]? )
+          -- polygon( [<fill-rule>,]? [<shape-arg> <shape-arg>]# )
+          -- inset()
+          -- ellipse( [<shape-radius>{2}]? [at <position>]? )
+          --
+          -- <image> https://drafts.csswg.org/css-images
+          -- Note: <gradient> is a type of <image> !
+          -- cross-fade()
+          -- image()
+          -- image-set()
+          -- image-set()
+          -- element()
+          --
+          --
+          --,("stylistic", functionParser ident) -- IDENT
+          --,("styleset", ? ) -- ident#
+          --,("swash", ? ) -- ident
+          --,("annotation", ? ) -- ident
+          --,("attr", ? ) --  <attr-name> <type-or-unit>? [, <attr-fallback> ]?
+          --,("calc", ? )  -- <calc-sum> , experimental
+          --,("character-variant", ? ) -- ident#
+          --,("element", ? )  -- id selector, experimental
+          --,("local", ? )
+          --,("ornaments", ? ) -- ident
+          --,("symbols", ? )  --  <symbols-type>? [ <string> | <image> ]+
+          --,("var", ? )  -- experimental: <custom-property-name> [, <declaration-value> ]?
+-- minmax()
+          ]
+
+dropShadow :: Parser FilterFunction
+dropShadow = functionParser $ do
+    l1 <- distance <* skipComments
+    l2 <- distance <* skipComments
+    l3 <- option Nothing ((Just <$> distance) <* skipComments)
+    c  <- option Nothing (Just <$> color)
+    pure $ DropShadow l1 l2 l3 c
+
+textShadow :: Parser Values
+textShadow = do
+    v <- shadowText <* skipComments
+    vs <- many (liftA2 (,) commaSeparator shadowText) <* skipComments
+    pure $ Values v vs
+
+shadowText :: Parser Value
+shadowText = permute (mkShadowText <$$> (lns <* skipComments)
+                                   <|?> (Nothing , Just <$> color <* skipComments))
+  where mkShadowText (x,y,b) = ShadowText x y b
+        lns = do
+            l1 <- distance <* skipComments
+            l2 <- distance <* skipComments
+            l3 <- option Nothing ((Just <$> distance) <* skipComments)
+            pure (l1,l2,l3)
+
+shadowList :: Parser Values
+shadowList = parseCommaSeparated (ShadowV <$> shadow)
+
+positionList :: Parser Values
+positionList = parseCommaSeparated positionvalue
+
+parseCommaSeparated :: Parser Value -> Parser Values
+parseCommaSeparated p = do
+    v <- p <* skipComments
+    vs <- many ((,) <$> commaSeparator <*> p) <* skipComments
+    c <- A.peekChar
+    case c of
+      Just x  -> if x `elem` ['!', ';', '}']
+                    then pure $ Values v vs
+                    else mzero
+      Nothing -> pure $ Values v vs
+
+
+shadow :: Parser Shadow
+shadow = permute (mkShadow <$?> (False, asciiCI "inset" $> True <* skipComments)
+                           <||> fourLengths
+                           <|?> (Nothing , Just <$> color <* skipComments))
+  where mkShadow i (l1,l2,l3,l4) = Shadow i l1 l2 l3 l4
+        fourLengths = do
+            l1 <- distance <* skipComments
+            l2 <- distance <* skipComments
+            l3 <- option Nothing ((Just <$> distance) <* skipComments)
+            l4 <- option Nothing ((Just <$> distance) <* skipComments)
+            pure (l1,l2,l3,l4)
+
+radialgradient :: Parser Gradient
+radialgradient = functionParser $ do
+    (def, c) <- option (True, RadialGradient Nothing Nothing) ((False,) <$> endingShapeAndSize <* skipComments)
+    p  <- option Nothing (asciiCI "at" *> skipComments *> (Just <$> position))
+    _  <- if def && isNothing p
+             then pure '*' -- do nothing
+             else comma
+    cs <- colorStopList
+    pure $ c p cs
+  where circle = asciiCI "circle" $> Just Circle <* skipComments
+        ellipse = asciiCI "ellipse" $> Just Ellipse <* skipComments
+        endingShapeAndSize = r1 <|> r2 <|> r3
+          where r1 = permute (RadialGradient <$?> (Nothing, ellipse) <||> (Just <$> (PL <$> percentageLength <* skipComments <*> percentageLength <* skipComments)))
+                r2 = permute (RadialGradient <$?> (Nothing, circle) <||> ((Just . SL) <$> distance <* skipComments))
+                r3 = permute (RadialGradient <$?> (Nothing, circle <|> ellipse) <||> extentKeyword)
+                   <|> permute (RadialGradient <$$> (circle <|> ellipse) <|?> (Nothing, extentKeyword))
+                extentKeyword = do
+                    i <- ident
+                    _ <- skipComments
+                    case Map.lookup i extentKeywords of
+                      Just x -> pure (Just x)
+                      Nothing -> mzero
+                extentKeywords :: Map Text Size
+                extentKeywords = Map.fromList [("closest-corner",  ClosestCorner)
+                                              ,("closest-side",    ClosestSide)
+                                              ,("farthest-corner", FarthestCorner)
+                                              ,("farthest-side",   FarthestSide)]
+
+-- | Assumes "linear-gradient(", or one of its prefixed equivalents, has been parsed.
+-- : [<angle>|to <side-or-corner> ,]? <color-stop> [, <color-stop>]+
+lineargradient :: Parser Gradient
+lineargradient = functionParser (lg <|> oldLg)
+  where lg = do
+            x <- option Nothing angleOrSide
+            c <- colorStopList
+            pure $ LinearGradient x c
+        oldLg = do
+            x <- option Nothing ((ga <|> ((Just . Right) <$> sideOrCorner)) <* comma)
+            c <- colorStopList
+            pure $ OldLinearGradient x c
+        angleOrSide = (ga <|> gs) <* comma
+        ga = (Just . Left) <$> angle
+        gs = asciiCI "to" *> skipComments *> ((Just . Right) <$> sideOrCorner)
+
+-- <side-or-corner> = [left | right] || [top | bottom]
+sideOrCorner :: Parser (Side, Maybe Side)
+sideOrCorner = orderOne <|> orderTwo
+  where orderOne = (,) <$> leftright <* skipComments
+                       <*> option Nothing (Just <$> topbottom)
+        orderTwo = (,) <$> topbottom <* skipComments
+                       <*> option Nothing (Just <$> leftright)
+
+leftright :: Parser Side
+leftright =  (asciiCI "left" $> LeftSide)
+         <|> (asciiCI "right" $> RightSide)
+
+topbottom :: Parser Side
+topbottom =  (asciiCI "top" $> TopSide)
+         <|> (asciiCI "bottom" $> BottomSide)
+
+colorStopList :: Parser [ColorStop]
+colorStopList = do
+    c1 <- colorStop
+    _  <- char ',' <* skipComments
+    c2 <- colorStop
+    cs <- many (char ',' *> skipComments *> colorStop)
+    pure $ c1:c2:cs
+
+colorStop :: Parser ColorStop
+colorStop = ColorStop <$> color <* skipComments
+        <*> option Nothing (Just <$> percentageLength <* skipComments)
+
+color :: Parser Color
+color = hex <|> othercolor
+  where othercolor = do
+            t <- textualvalue
+            case t of
+              ColorV c -> pure c
+              _        -> mzero
+
+-- TODO make parser specifically for it instead of reusing numericalvalue
+percentageLength :: Parser PercentageLength
+percentageLength = do
+    n <- numericalvalue
+    case n of
+      PercentageV p -> pure $ Left p
+      NumberV 0     -> pure $ Right (Distance 0 PX)
+      DistanceV d   -> pure $ Right d
+      _             -> mzero
+
+numberPercentage :: Parser (Either Number Percentage)
+numberPercentage = do
+    n <- numericalvalue
+    case n of
+      NumberV x     -> pure $ Left x
+      PercentageV p -> pure $ Right p
+      _             -> mzero
+
+-- | Assumes "rect(" has been already parsed
+rect :: Parser Value
+rect = functionParser $ do
+    length1 <- distance <* comma
+    length2 <- distance <* comma
+    length3 <- distance <* comma
+    length4 <- distance
+    pure $ Rect length1 length2 length3 length4
+
+-- | Assumes "translate(" has been already parsed
+translate :: Parser TransformFunction
+translate = functionParser $ do
+    pl  <- percentageLength <* skipComments
+    mpl <- option Nothing (char ',' *> skipComments *> (Just <$> percentageLength))
+    pure $ Translate pl mpl
+
+-- | Parser of scale() function. Assumes "scale(" has been already parsed
+scale :: Parser TransformFunction
+scale = functionParser $ do
+    n  <- number <* skipComments
+    mn <- option Nothing (char ',' *> skipComments *> (Just <$> number))
+    pure $ Scale n mn
+
+scale3d :: Parser TransformFunction
+scale3d = functionParser $ liftA3 Scale3d n n number
+  where n = number <* comma
+
+skew :: Parser TransformFunction
+skew = functionParser $ do
+    a  <- angle <* skipComments
+    ma <- option Nothing (char ',' *> skipComments *> (Just <$> angle))
+    pure $ Skew a ma
+
+translate3d :: Parser TransformFunction
+translate3d = functionParser $ do
+    x <- percentageLength <* comma
+    y <- percentageLength <* comma
+    z <- distance
+    pure $ Translate3d x y z
+
+matrix :: Parser TransformFunction
+matrix = functionParser $ do
+    n  <-  number
+    ns <-  count 5 (comma *> number)
+    pure $ mkMat (n:ns)
+
+matrix3d :: Parser TransformFunction
+matrix3d = functionParser $ do
+    n  <-  number
+    ns <-  count 15 (comma *> number)
+    pure $ mkMat3d (n:ns)
+
+rotate3d :: Parser TransformFunction
+rotate3d = functionParser $ do
+    x  <-  number <* comma
+    y  <-  number <* comma
+    z  <-  number <* comma
+    a  <-  angle
+    pure $ Rotate3d x y z a
+
+cubicbezier :: Parser TimingFunction
+cubicbezier = functionParser $ do
+    p0 <- number <* comma
+    p1 <- number <* comma
+    p2 <- number <* comma
+    p3 <- number
+    pure $ CubicBezier p0 p1 p2 p3
+
+steps :: Parser TimingFunction
+steps = functionParser $ do
+    i <- int
+    s <- option Nothing (comma *> (Just <$> startOrEnd))
+    pure $ Steps i s
+  where startOrEnd = (asciiCI "end" $> End)
+                 <|> (asciiCI "start" $> Start)
+
+-- We use skipSpace instead of skipComments, since comments aren't valid inside
+-- the url-token. From the spec:
+-- COMMENT tokens cannot occur within other tokens: thus, "url(/*x*/pic.png)"
+-- denotes the URI "/*x*/pic.png", not "pic.png".
+-- Assumes "url(" has been already parsed
+url :: Parser Url
+url = Url <$> (skipSpace *> someUri <* skipSpace <* char ')')
+  where someUri = (Right <$> stringtype) <|> (Left <$> nonQuotedUri)
+        nonQuotedUri = A.takeWhile1 (/= ')') -- TODO maybe parse ident?
+
+-- format(<string>#)
+-- Assumes "format(" has been already parsed
+format :: Parser Value
+format = Format <$> functionParser p
+  where p = (:) <$> stringtype <*> many (comma *> stringtype)
+
+namedColorsParsersMap :: Map Text (Parser Value)
+namedColorsParsersMap = Map.fromList $ foldr f [] keywordColors
+  where f x xs = let a = fst x
+                 in (a, pure $ ColorV (Named a)) : xs
+
+-- | For cases when CSS hacks are used, e.g.:
+-- margin-top: 1px \9;
+valuesFallback :: Parser Values
+valuesFallback = Values <$> value <*> many ((,) <$> separator <*> value) <* skipComments
+
+separator :: Parser Separator
+separator = lexeme $ (char ',' $> Comma)
+                 <|> (char '/' $> Slash)
+                 <|> pure Space
+
+commaSeparator :: Parser Separator
+commaSeparator = lexeme (char ',' $> Comma)
+
+-- <string> data type parser
+stringtype :: Parser StringType
+stringtype = doubleQuotesString <|> singleQuotesString
+
+doubleQuotesString :: Parser StringType
+doubleQuotesString =  char '\"' *> (DoubleQuotes <$> untilDoubleQuotes)
+  where untilDoubleQuotes = mappend <$> A.takeWhile (\c -> c /= '\\' && c /= '\"') <*> checkCharacter
+        checkCharacter = (string "\"" $> mempty)
+                      <|> (T.cons <$> char '\\' <*> untilDoubleQuotes)
+
+singleQuotesString :: Parser StringType
+singleQuotesString = char '\'' *> (SingleQuotes <$> untilSingleQuotes)
+  where untilSingleQuotes = mappend <$> A.takeWhile (\c -> c /= '\\' && c /= '\'') <*> checkCharacter
+        checkCharacter = (string "\'" $> mempty)
+                      <|> (T.cons <$> char '\\' <*> untilSingleQuotes)
+
+-- <single-animation>#
+animation :: Parser Values
+animation = parseCommaSeparated singleAnimation
+
+-- <single-animation> = <time> || <timing-function> || <time> || <animation-iteration-count> || <animation-direction> || <animation-fill-mode> || <animation-play-state> || [ none | <keyframes-name> ]
+singleAnimation :: Parser Value
+singleAnimation = do
+    sa <- permute (mkSingleAnimation <$?> (Nothing, Just <$> keyframesName <* skipComments)
+                                     <|?> (Nothing, Just <$> iterationCount <* skipComments)
+                                     <|?> (Nothing, Just <$> duration <* skipComments)
+                                     <|?> (Nothing, Just <$> duration <* skipComments)
+                                     <|?> (Nothing, Just <$> timingFunction <* skipComments)
+                                     <|?> (Nothing, Just <$> animationDirection <* skipComments)
+                                     <|?> (Nothing, Just <$> animationFillMode <* skipComments)
+                                     <|?> (Nothing, Just <$> animationPlayState <* skipComments))
+    if saIsEmpty sa
+       then mzero
+       else pure sa
+  where -- mkSingleAnimation is used to order properly the overlapping values parsed by permute.
+        mkSingleAnimation kf ic t1 t2 tf c d e = SingleAnimation t2 tf t1 ic c d e kf
+        saIsEmpty (SingleAnimation Nothing Nothing Nothing Nothing Nothing Nothing Nothing Nothing) = True
+        saIsEmpty _                                                                                 = False
+        iterationCount = (mkOther <$> asciiCI "infinite") <|> (NumberV <$> number)
+        animationDirection = parseIdents ["normal", "reverse", "alternate", "alternate-reverse"]
+        animationFillMode  = parseIdents ["none", "forwards", "backwards", "both"]
+        animationPlayState = parseIdents ["running", "paused"]
+        keyframesName = stringvalue <|> (mkOther <$> ident)
+
+parseIdents :: [Text] -> Parser TextV
+parseIdents ls = do
+    i <- ident
+    if Set.member (T.toLower i) s
+       then pure $ TextV i
+       else mzero
+  where s = Set.fromList ls
+
+-- <single-animation-fill-mode> = none | forwards | backwards | both
diff --git a/src/Hasmin/Properties.hs b/src/Hasmin/Properties.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Properties.hs
@@ -0,0 +1,501 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Properties
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Properties (
+     PropertyInfo(..)
+     , shorthandAndLonghandsMap
+     , propertiesTraits
+    ) where
+
+import Data.Attoparsec.Text (parseOnly)
+import Data.Text (Text)
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import Hasmin.Types.Value
+import Hasmin.Parser.Value
+
+-- TODO see if this can be extended to contain all the information from a
+-- property (such as initial value). Otherwise, delete it.
+data PropertyInfo =
+     PropertyInfo { overwrittenBy :: [Text] -- shorthands that overwrite it
+                  , subproperties :: [Text] -- longhands that can be merged into it
+                  } deriving (Show)
+
+shorthandAndLonghandsMap :: Map Text PropertyInfo
+shorthandAndLonghandsMap = Map.fromList properties
+
+properties :: [(Text,PropertyInfo)]
+properties =
+  [("animation", PropertyInfo mempty ["animation-name","animation-duration","animation-timing-function","animation-delay","animation-iteration-count","animation-direction","animation-fill-mode","animation-play-state"])
+  ,("animation-delay",           PropertyInfo ["animation"] mempty)
+  ,("animation-direction",       PropertyInfo ["animation"] mempty)
+  ,("animation-duration",        PropertyInfo ["animation"] mempty)
+  ,("animation-fill-mode",       PropertyInfo ["animation"] mempty)
+  ,("animation-iteration-count", PropertyInfo ["animation"] mempty)
+  ,("animation-name",            PropertyInfo ["animation"] mempty)
+  ,("animation-play-state",      PropertyInfo ["animation"] mempty)
+  ,("animation-timing-function", PropertyInfo ["animation"] mempty)
+  ,("background",            PropertyInfo mempty ["background-image", "background-position", "background-size", "background-repeat", "background-origin", "background-clip", "background-attachment", "background-color"])
+  ,("background-attachment", PropertyInfo ["background"] mempty)
+  ,("background-clip",       PropertyInfo ["background"] mempty)
+  ,("background-color",      PropertyInfo ["background"] mempty)
+  ,("background-image",      PropertyInfo ["background"] mempty)
+  ,("background-origin",     PropertyInfo ["background"] mempty)
+  ,("background-position",   PropertyInfo ["background"] ["background-position-x", "background-position-y"])
+  ,("background-position-x", PropertyInfo ["background", "background-position"] mempty)
+  ,("background-position-y", PropertyInfo ["background", "background-position"] mempty)
+  ,("background-repeat",     PropertyInfo ["background"] mempty)
+  ,("background-size",       PropertyInfo ["background"] mempty)
+  ,("border",              PropertyInfo mempty ["border-width", "border-style", "border-color"])
+  ,("border-bottom",       PropertyInfo ["border"] ["border-bottom-color", "border-bottom-style", "border-bottom-width"])
+  ,("border-bottom-color", PropertyInfo ["border", "border-color", "border-bottom"] mempty)
+  ,("border-bottom-left-radius",  PropertyInfo ["border-radius"] mempty)
+  ,("border-bottom-right-radius", PropertyInfo ["border-radius"] mempty)
+  ,("border-bottom-style", PropertyInfo ["border", "border-style", "border-bottom"] mempty)
+  ,("border-bottom-width", PropertyInfo ["border", "border-width", "border-bottom"] mempty)
+  ,("border-color",        PropertyInfo ["border"] ["border-top-color", "border-right-color", "border-bottom-color", "border-left-color"])
+  ,("border-image",        PropertyInfo ["border"] ["border-image-source", "border-image-slice", "border-image-width", "border-image-outset", "border-image-repeat"])
+  ,("border-image-outset", PropertyInfo ["border", "border-image"] mempty)
+  ,("border-image-repeat", PropertyInfo ["border", "border-image"] mempty)
+  ,("border-image-slice",  PropertyInfo ["border", "border-image"] mempty)
+  ,("border-image-source", PropertyInfo ["border", "border-image"] mempty)
+  ,("border-image-width",  PropertyInfo ["border", "border-image"] mempty)
+  ,("border-left",       PropertyInfo ["border"] ["border-left-color", "border-left-style", "border-left-width"])
+  ,("border-left-color", PropertyInfo ["border", "border-color", "border-left"] mempty)
+  ,("border-left-style", PropertyInfo ["border", "border-style", "border-left"] mempty)
+  ,("border-left-width", PropertyInfo ["border", "border-width", "border-left"] mempty)
+  ,("border-radius", PropertyInfo mempty ["border-top-left-radius","border-top-right-radius","border-bottom-right-radius","border-bottom-left-radius"])
+  ,("border-right",       PropertyInfo ["border"] ["border-right-color", "border-right-style", "border-right-width"])
+  ,("border-right-color", PropertyInfo ["border", "border-color", "border-right"] mempty)
+  ,("border-right-style", PropertyInfo ["border", "border-style", "border-right"] mempty)
+  ,("border-right-width", PropertyInfo ["border", "border-width", "border-right"] mempty)
+  ,("border-style", PropertyInfo ["border"] ["border-top-style", "border-right-style", "border-bottom-style", "border-left-style"])
+  ,("border-top",       PropertyInfo ["border"] ["border-top-color", "border-top-style", "border-top-width"])
+  ,("border-top-color", PropertyInfo ["border", "border-color", "border-top"] mempty)
+  ,("border-top-left-radius",     PropertyInfo ["border-radius"] mempty)
+  ,("border-top-right-radius",    PropertyInfo ["border-radius"] mempty)
+  ,("border-top-style", PropertyInfo ["border", "border-style", "border-top"] mempty)
+  ,("border-top-width", PropertyInfo ["border", "border-width", "border-top"] mempty)
+  ,("border-width",        PropertyInfo ["border"] ["border-top-width", "border-right-width", "border-bottom-width", "border-left-width"])
+  ,("column-count", PropertyInfo ["columns"] mempty)
+  ,("column-rule", PropertyInfo mempty ["column-rule-color", "column-rule-style", "column-rule-width"])
+  ,("column-rule-color", PropertyInfo ["column-rule"] mempty)
+  ,("column-rule-style", PropertyInfo ["column-rule"] mempty)
+  ,("column-rule-width", PropertyInfo ["column-rule"] mempty)
+  ,("columns", PropertyInfo mempty ["column-width", "column-count"])
+  ,("column-width", PropertyInfo ["columns"] mempty)
+  ,("flex",        PropertyInfo mempty ["flex-grow", "flex-shrink", "flex-basis"])
+  ,("flex-basis",  PropertyInfo ["flex"] mempty)
+  ,("flex-direction", PropertyInfo ["flex-flow"] mempty)
+  ,("flex-flow",      PropertyInfo mempty ["flex-direction", "flex-wrap"])
+  ,("flex-grow",   PropertyInfo ["flex"] mempty)
+  ,("flex-shrink", PropertyInfo ["flex"] mempty)
+  ,("flex-wrap",      PropertyInfo ["flex-flow"] mempty)
+  ,("font", PropertyInfo mempty ["font-style", "font-variant", "font-weight", "font-stretch", "font-size", "line-height", "font-family"])
+  ,("font-family",            PropertyInfo ["font"] mempty)
+  ,("font-kerning",           PropertyInfo ["font"] mempty)
+  ,("font-language-override", PropertyInfo ["font"] mempty)
+  ,("font-size",              PropertyInfo ["font"] mempty)
+  ,("font-size-adjust",       PropertyInfo ["font"] mempty)
+  ,("font-stretch",           PropertyInfo ["font"] mempty)
+  ,("font-style",             PropertyInfo ["font"] mempty)
+  ,("font-variant",           PropertyInfo ["font"] mempty)
+  ,("font-weight",            PropertyInfo ["font"] mempty)
+  ,("grid", PropertyInfo mempty ["grid-template-rows", "grid-template-columns", "grid-template-areas", "grid-auto-rows", "grid-auto-columns", "grid-auto-flow", "grid-column-gap", "grid-row-gap"])
+  ,("grid-area",     PropertyInfo mempty ["grid-row-start", "grid-row-end", "grid-column-start", "grid-column-end"])
+  ,("grid-column",   PropertyInfo mempty ["grid-column-start", "grid-column-end"])
+  ,("grid-column-end", PropertyInfo ["grid-column", "grid-area"] mempty)
+  ,("grid-column-end", PropertyInfo ["grid-column", "grid-area"] mempty)
+  ,("grid-column-gap", PropertyInfo ["grid-gap"] mempty)
+  ,("grid-gap",      PropertyInfo mempty ["grid-row-gap", "grid-column-gap"])
+  ,("grid-row",      PropertyInfo mempty ["grid-row-start", "grid-row-end"])
+  ,("grid-row-end", PropertyInfo ["grid-row", "grid-area"] mempty)
+  ,("grid-row-gap",    PropertyInfo ["grid-gap"] mempty)
+  ,("grid-row-start", PropertyInfo ["grid-row", "grid-area"] mempty)
+  ,("grid-template", PropertyInfo mempty ["grid-template-columns", "grid-template-rows", "grid-template-areas"])
+  ,("grid-template-areas",   PropertyInfo ["grid-template"] mempty)
+  ,("grid-template-columns", PropertyInfo ["grid-template"] mempty)
+  ,("grid-template-rows",    PropertyInfo ["grid-template"] mempty)
+  ,("line-height",            PropertyInfo ["font"] mempty)
+  ,("list-style",          PropertyInfo mempty ["list-style-type", "list-style-position", "list-style-image"])
+  ,("list-style-image",    PropertyInfo ["list-style"] mempty)
+  ,("list-style-position", PropertyInfo ["list-style"] mempty)
+  ,("list-style-type",     PropertyInfo ["list-style"] mempty)
+  ,("margin",        PropertyInfo mempty ["margin-top", "margin-right", "margin-bottom", "margin-left"])
+  ,("margin-bottom", PropertyInfo ["margin"] mempty)
+  ,("margin-left",   PropertyInfo ["margin"] mempty)
+  ,("margin-right",  PropertyInfo ["margin"] mempty)
+  ,("margin-top",    PropertyInfo ["margin"] mempty)
+  ,("mask",        PropertyInfo mempty ["mask-clip", "mask-origin", "mask-border"])
+  ,("mask-border", PropertyInfo ["mask"] ["mask-border-source", "mask-border-slice", "mask-border-width", "mask-border-outset", "mask-border-repeat"])
+  ,("mask-border-outset", PropertyInfo ["mask", "mask-border"] mempty)
+  ,("mask-border-repeat", PropertyInfo ["mask", "mask-border"] mempty)
+  ,("mask-border-slice",  PropertyInfo ["mask", "mask-border"] mempty)
+  ,("mask-border-source", PropertyInfo ["mask", "mask-border"] mempty)
+  ,("mask-border-width",  PropertyInfo ["mask", "mask-border"] mempty)
+  ,("mask-clip",      PropertyInfo ["mask"] mempty)
+  ,("mask-composite", PropertyInfo ["mask"] mempty)
+  ,("mask-image",     PropertyInfo ["mask"] mempty)
+  ,("mask-mode",      PropertyInfo ["mask"] mempty)
+  ,("mask-origin",    PropertyInfo ["mask"] mempty)
+  ,("mask-position",  PropertyInfo ["mask"] mempty)
+  ,("mask-repeat",    PropertyInfo ["mask"] mempty)
+  ,("mask-size",      PropertyInfo ["mask"] mempty)
+  ,("outline",       PropertyInfo mempty ["outline-width", "outline-style", "outline-color"])
+  ,("outline-color", PropertyInfo ["outline"] mempty)
+  ,("outline-style", PropertyInfo ["outline"] mempty)
+  ,("outline-width", PropertyInfo ["outline"] mempty)
+  ,("padding",        PropertyInfo mempty ["padding-top", "padding-right", "padding-bottom", "padding-left"])
+  ,("padding-bottom", PropertyInfo ["padding"] mempty)
+  ,("padding-left",   PropertyInfo ["padding"] mempty)
+  ,("padding-right",  PropertyInfo ["padding"] mempty)
+  ,("padding-top",    PropertyInfo ["padding"] mempty)
+  ,("text-decoration",       PropertyInfo mempty ["text-decoration-color", "text-decoration-style", "text-decoration-line"])
+  ,("text-decoration-color", PropertyInfo ["text-decoration"] mempty)
+  ,("text-decoration-line",  PropertyInfo ["text-decoration"] mempty)
+  ,("text-decoration-style", PropertyInfo ["text-decoration"] mempty)
+  ,("text-emphasis",       PropertyInfo mempty ["text-emphasis-color", "text-emphasis-style"])
+  ,("text-emphasis-color", PropertyInfo ["text-emphasis"] mempty)
+  ,("text-emphasis-style", PropertyInfo ["text-emphasis"] mempty)
+  ,("transition", PropertyInfo mempty ["transition-property", "transition-duration", "transition-timing-function", "transition-delay"])
+  ,("transition-delay",           PropertyInfo ["transition"] mempty)
+  ,("transition-duration",        PropertyInfo ["transition"] mempty)
+  ,("transition-property",        PropertyInfo ["transition"] mempty)
+  ,("transition-timing-function", PropertyInfo ["transition"] mempty)
+  ]
+
+-- mempty is used when there is no initial value, or for properties that need
+-- special treatment, such as many of the shorthands
+propertiesTraits :: Map Text (Maybe Values, Bool)
+propertiesTraits = Map.fromList $ replaceTextWithValues
+  [("align-content",              ("stretch",                  False))
+  ,("align-items",                ("stretch",                  False))
+  ,("align-self",                 ("auto",                     False))
+  ,("all",                        (mempty,                     False)) -- No practical initial value
+  ,("animation",                  (mempty,                     False)) -- shorthand
+  ,("animation-delay",            ("0s",                       False)) -- experimental
+  ,("animation-direction",        ("normal",                   False)) -- experimental
+  ,("animation-duration",         ("0s",                       False)) -- experimental
+  ,("animation-fill-mode",        ("none",                     False)) -- experimental
+  ,("animation-iteration-count",  ("1",                        False)) -- experimental
+  ,("animation-name",             ("none",                     False)) -- experimental
+  ,("animation-play-state",       ("running",                  False)) -- experimental
+  ,("animation-timing-function",  ("ease",                     False)) -- experimental
+  ,("backface-visibility",        ("visible",                  False))
+  ,("-webkit-backface-visibility", ("visible",                 False))
+  ,("backdrop-filter",            ("none",                     False)) -- experimental
+  ,("-webkit-backdrop-filter",    ("none",                     False)) -- experimental
+  ,("background",                 (mempty {-shorthand-},       False))
+  ,("background-attachment",      ("scroll",                   False))
+  ,("background-blend-mode",      ("normal",                   False))
+  ,("background-clip",            ("border-box",               False))
+  ,("-webkit-background-clip",    ("border-box",               False))
+  ,("background-color",           ("transparent",              False))
+  ,("background-image",           ("none",                     False))
+  ,("background-origin",          ("padding-box",              False))
+  ,("background-position",        (mempty,                     False)) --shorthand
+  ,("background-position-x",      ("left",                     False))
+  ,("background-position-inline", (mempty {-not applicable-},  False))
+  ,("background-position-block",  (mempty {-not applicable-},  False))
+  ,("background-position-y",      ("top",                      False))
+  ,("background-repeat",          ("repeat",                   False))
+  ,("background-size",            ("auto",                     False))
+  ,("block-size",                 ("auto",                     False)) -- experimental
+  ,("border",                     ("medium none currentcolor", False)) -- shorthand!
+  -- border-block-* would go here
+  ,("border-bottom",              ("medium none currentcolor", False)) -- shorthand!
+  ,("border-bottom-color",        ("currentcolor",             False))
+  ,("border-bottom-left-radius",  ("0",                        False))
+  ,("border-bottom-right-radius", ("0",                        False))
+  ,("border-bottom-style",        ("none",                     False))
+  ,("border-bottom-width",        ("medium",                   False))
+  ,("border-collapse",            ("separate",                 True))
+  ,("border-color",               (mempty,                     False)) -- shorthand!
+  ,("border-image",               (mempty,                     False)) -- shorthand
+  ,("border-image-outset",        ("0",                        False))
+  ,("border-image-repeat",        ("stretch",                  False))
+  ,("border-image-slice",         ("100%",                     False))
+  ,("border-image-source",        ("none",                     False))
+  ,("border-image-width",         ("1",                        False))
+  -- border-inline-* would go here
+  ,("border-left",                ("medium none currentcolor", False)) -- shorthand!
+  ,("border-left-color",          ("currentcolor",             False))
+  ,("border-left-style",          ("none",                     False))
+  ,("border-left-width",          ("medium",                   False))
+  -- ,("border-radius" -- shorthand
+  ,("border-right",               ("medium none currentcolor", False)) -- shorthand!
+  ,("border-right-color",         ("currentcolor",             False))
+  ,("border-right-style",         ("none",                     False))
+  ,("border-right-width",         ("medium",                   False))
+  ,("border-spacing",             ("0",                        True))
+  -- ,("border-style" -- shorthand
+  ,("border-top",                 ("medium none currentcolor", False)) -- shorthand
+  ,("border-top-color",           ("currentcolor",             False))
+  ,("border-top-style",           ("none",                     False))
+  ,("border-top-width",           ("medium",                   False))
+  ,("border-top-left-radius",     ("0",                        False))
+  ,("border-top-right-radius",    ("0",                        False))
+  -- ,("border-width" -- shorthand
+  ,("bottom",                     ("auto",                     False))
+  ,("box-decoration-break",       ("slice",                    False)) -- experimental
+  ,("-webkit-box-decoration-break", ("slice",                  False)) -- experimental
+  ,("-o-box-decoration-break",    ("slice",                    False)) -- experimental
+  ,("box-shadow",                 ("none",                     False))
+  ,("-webkit-box-shadow",         ("none",                     False))
+  ,("box-sizing",                 ("content-box",              False)) -- experimental
+  ,("-webkit-box-sizing",         ("content-box",              False)) -- experimental
+  ,("-moz-box-sizing",            ("content-box",              False)) -- experimental
+  ,("break-after",                ("auto",                     False))
+  ,("break-before",               ("auto",                     False))
+  ,("break-inside",               ("auto",                     False))
+  ,("caption-side",               ("top",                      True))
+  ,("clear",                      ("none",                     False))
+  ,("clip",                       ("auto",                     False)) -- deprecated!
+  ,("color",                      (mempty {-UA dependent-},    True))
+  ,("column-count",               ("auto",                     False))
+  ,("column-fill",                ("balance",                  False))
+  ,("column-gap",                 ("normal",                   False))
+  ,("column-rule",                ("medium none currentcolor", False)) -- shorthand
+  ,("column-rule-color",          ("currentcolor",             False))
+  ,("column-rule-style",          ("none",                     False))
+  ,("column-rule-width",          ("medium",                   False))
+  ,("column-span",                ("none",                     False))
+  ,("column-width",               ("auto",                     False))
+  -- ,("columns", -- shorthand
+  ,("content",                    ("normal",                   False))
+  ,("counter-increment",          ("none",                     False))
+  ,("counter-reset",              ("none",                     False))
+  ,("cursor",                     ("auto",                     True))
+  ,("direction",                  ("ltr",                      True))
+  ,("display",                    ("inline",                   False))
+  ,("empty-cells",                ("show",                     True))
+  ,("filter",                     ("none",                     False)) -- experimental
+  ,("-webkit-filter",             ("none",                     False)) -- experimental
+  -- ,("flex",                    ("0",                     False)) -- shorthand
+  ,("flex-basis",                 ("auto",                     False))
+  ,("flex-direction",             ("row",                      False))
+  ,("flex-flow",                  ("row nowrap",               False)) -- shorthand
+  ,("flex-grow",                  ("0",                        False))
+  ,("flex-shrink",                ("1",                        False))
+  ,("flex-wrap",                  ("nowrap",                   False))
+  ,("float",                      ("none",                     False))
+  ,("float",                      ("none",                     False))
+  ,("font",                       (mempty {-shorthand-},       True))
+  ,("font-family",                (mempty {-UA dependent-},    True))
+  ,("font-feature-settings",      ("normal",      True))
+  ,("font-kerning",               ("auto",        True))
+  ,("font-language-override",     ("normal",      True))
+  ,("font-size",                  ("medium",      True))
+  ,("font-size-adjust",           ("none",        True))
+  ,("font-stretch",               ("normal",      True))
+  ,("font-style",                 ("normal",      True))
+  ,("font-synthesis",             ("weight style",      True))
+  ,("font-variant",               ("normal",      True))
+  ,("font-variant-alternates",    ("normal",      True))
+  ,("font-variant-caps",          ("normal",      True))
+  ,("font-variant-east-asian",    ("normal",      True))
+  ,("font-variant-ligatures",     ("normal",      True))
+  ,("font-variant-numeric",       ("normal",      True))
+  ,("font-variant-position",      ("normal",      True))
+  ,("font-weight",                ("normal",      True))
+  -- grid -- shorthand, experimental
+  -- grid-area -- shorthand, experimental
+  ,("grid-auto-columns",          ("auto", False)) -- experimental
+  ,("-ms-grid-auto-columns",      ("auto", False)) -- experimental
+  ,("-webkit-grid-auto-columns",  ("auto", False)) -- experimental
+  ,("grid-auto-flow",             ("row", False)) -- experimental
+  ,("-webkit-grid-auto-flow",     ("row", False)) -- experimental
+  ,("grid-auto-rows",             ("auto", False)) -- experimental
+  ,("-ms-grid-auto-rows",         ("auto", False)) -- experimental
+  ,("-webkit-grid-auto-rows",     ("auto", False)) -- experimental
+  -- grid-column -- shorthand, experimental
+  ,("grid-column-end",            ("auto", False)) -- experimental
+  ,("-ms-grid-column-end",        ("auto", False)) -- experimental
+  ,("-webkit-grid-column-end",    ("auto", False)) -- experimental
+  ,("grid-column-gap",            ("0", False)) -- experimental
+  ,("-ms-grid-column-gap",        ("0", False)) -- experimental
+  ,("grid-column-start",          ("auto", False)) -- experimental
+  ,("-ms-grid-column-start",      ("auto", False)) -- experimental
+  ,("-webkit-grid-column-start",  ("auto", False)) -- experimental
+  -- ,("grid-gap", -- shorthand, experimental
+  -- ,("grid-row", -- shorthand, experimental
+  ,("grid-row-end",               ("auto", False)) -- experimental
+  ,("-ms-grid-row-end",           ("auto", False)) -- experimental
+  ,("-webkit-grid-row-end",       ("auto", False)) -- experimental
+  ,("grid-row-gap",               ("0", False)) -- experimental
+  ,("-webkit-grid-row-gap",       ("0", False)) -- experimental
+  ,("grid-row-start",             ("auto", False)) -- experimental
+  ,("-ms-grid-row-start",         ("auto", False)) -- experimental
+  ,("-webkit-grid-row-start",     ("auto", False)) -- experimental
+  -- ,("grid-template", -- shorthand, experimental
+  ,("grid-template-areas",        ("none", False)) -- experimental
+  ,("-webkit-grid-template-areas", ("none", False)) -- experimental
+  ,("grid-template-columns",      ("none", False)) -- experimental
+  ,("-ms-grid-template-columns",  ("none", False)) -- experimental
+  ,("-webkit-grid-template-columns", ("none", False)) -- experimental
+  ,("grid-template-rows",         ("none", False)) -- experimental
+  ,("-ms-grid-template-rows",     ("none", False)) -- experimental
+  ,("-webkit-grid-template-rows", ("none", False)) -- experimental
+  ,("height",                     ("auto",      False))
+  ,("hyphens",                    ("manual",      False))
+  -- ,("image-orientation" --experimental
+  -- ,("image-rendering" --experimental
+  -- ,("inline-size", -- experimental
+  ,("isolation",                  ("auto",      False))
+  ,("justify-content",            ("flex-start",      False))
+  ,("left",                       ("auto",      False))
+  ,("letter-spacing",             ("normal",      True))
+  ,("line-break",                 ("auto",      False))
+  ,("line-height",                ("normal",      True))
+  ,("list-style",                 ("none disc outside", True)) -- none must go first, for entropy reasons.
+  ,("list-style-image",           ("none", True))
+  ,("list-style-position",        ("outside", True))
+  ,("list-style-type",            ("disc", True))
+  ,("margin",                     (mempty {-shorthand-}, False)) -- shorthand
+  -- ,("margin-block-* -- experimental
+  ,("margin-bottom",                      ("0", False))
+  -- ,("margin-inline-*" --experimental
+  ,("margin-left",                        ("0", False))
+  ,("margin-right",                       ("0", False))
+  ,("margin-top",                         ("0", False))
+  -- ,("mask", -- shorthand
+  ,("mask-clip",                          ("border-box", False)) -- experimental
+  ,("-webkit-mask-clip",                  ("border-box", False)) -- experimental
+  ,("mask-composite",                     ("add", False)) -- experimental
+  ,("mask-image",                         ("none", False)) -- experimental
+  ,("-webkit-mask-image",                 ("none", False)) -- experimental
+  -- ,("mask-mode",                 ("match-source", False)) -- experimental, no support
+  ,("mask-origin",                        ("border-box", False)) -- experimental
+  ,("-webkit-mask-origin",                ("border-box", False)) -- experimental
+  ,("mask-position",                      ("center", False)) -- experimental
+  ,("-webkit-mask-position",              ("center", False)) -- experimental
+  ,("mask-repeat",                        ("no-repeat", False)) -- experimental
+  ,("-webkit-mask-repeat",                ("no-repeat", False)) -- experimental
+  ,("mask-size",                          ("auto", False)) -- experimental
+  ,("mask-type",                          ("luminance", False)) -- experimental
+  -- ,("max-block-size",         ("0", False)) -- experimental
+  ,("max-height",                         ("none", False))
+  -- ,("max-inline-size",        ("0", False)) -- experimental
+  ,("max-width",                          ("none", False))
+  -- ,("min-block-size",         ("0", False)) -- experimental
+  ,("min-height",                         ("0", False))
+  -- ,("min-inline-size",        ("0", False)) -- experimental
+  ,("min-width",                          ("0", False))
+  ,("mix-blend-mode",                     ("normal", False))
+  ,("object-fit",                         ("fill", True))
+  ,("object-position",                    ("50% 50%", True))
+  -- ,("offset-*",                        ("auto", False)) -- experimental
+  ,("opacity",                            ("1", False))
+  ,("order",                              ("0", False))
+  ,("orphans",                            ("2", True))
+  ,("outline",                            ("medium none invert", False)) -- shorthand
+  ,("outline-color",                      ("invert" , False))
+  ,("outline-offset",                     ("0", False))
+  ,("outline-style",                      ("none", False))
+  ,("outline-width",                      ("medium", False))
+  ,("overflow",                           ("visible", False))
+  ,("overflow-wrap",                      ("normal", True)) -- also called word-wrap
+  ,("overflow-x",                         ("visible", False)) -- experimental
+  ,("overflow-y",                         ("visible", False)) -- experimental
+  ,("padding",                            (mempty, False)) -- shorthand
+  -- ,("padding-block*",          ("0", False)) -- experimental
+  ,("padding-bottom",                     ("0", False))
+  -- ,("padding-inline-*",                ("0", False)) -- experimental
+  ,("padding-left",                       ("0", False))
+  ,("padding-right",                      ("0", False))
+  ,("padding-top",                        ("0", False))
+  ,("page-break-after",                   ("auto", False))
+  ,("page-break-before",                  ("auto", False))
+  ,("page-break-inside",                  ("auto", False))
+  ,("perspective",                        ("none", False)) -- experimental
+  ,("perspective-origin",                 ("50% 50%", False)) -- experimental
+  ,("pointer-events",                     ("auto", True))
+  ,("position",                           ("static", False))
+  ,("quotes",                             (mempty {-UA dependent-}, True))
+  ,("resize",                             ("none", False))
+  ,("right",                              ("auto", False))
+  ,("right",                              ("auto", False))
+  -- ,("ruby-*", -- experimental
+  ,("scroll-behavior",                    ("auto", False))
+  -- ,("scroll-snap-coordinate", ("none", False)) -- experimental
+  -- ,("scroll-snap-destination",("0 0", False)) -- experimental, actually 0px 0px
+  -- ,("scroll-snap-type", -- experimental
+  ,("shape-image-threshold",              ("0", False))
+  ,("shape-margin",                       ("0", False))
+  ,("shape-outside",                      ("none", False))
+  ,("tab-size",                           ("8", True)) -- experimental
+  ,("-moz-tab-size",                      ("8", True)) -- experimental
+  ,("-o-tab-size",                        ("8", True)) -- experimental
+  ,("table-layout",                       ("auto", False))
+  ,("text-align",                         ("start", False))
+  ,("text-align-last",                    ("auto", True)) -- experimental
+  ,("text-combine-upright",               ("none", True)) -- experimental
+  ,("-webkit-text-combine-upright",       ("none", True)) -- experimental
+  ,("text-decoration",                    ("currentcolor solid none", False)) -- shorthand
+  ,("text-decoration-color",              ("currentcolor", False))
+  ,("text-decoration-line",               ("none", False))
+  ,("text-decoration-style",              ("solid", False))
+  ,("text-emphasis",                      ("none currentcolor", False)) -- shorthand
+  ,("text-emphasis-color",                ("currentcolor", False))
+  ,("text-emphasis-position",             (mempty {-"over right"-}, False))
+  ,("text-emphasis-style",                ("none", False))
+  ,("text-indent",                        ("0", True))
+  ,("text-orientation",                   ("mixed", True)) -- experimental
+  ,("-webkit-text-orientation",           ("mixed", True)) -- experimental
+  ,("text-overflow",                      ("clip", False))
+  ,("text-rendering",                     ("auto", True))
+  ,("text-shadow",                        ("none", True))
+  ,("text-transform",                     ("none", True))
+  ,("text-underline-position",            ("auto", True))
+  ,("top",                                ("auto", False))
+  ,("touch-action",                       ("auto", False))
+  ,("transform",                          ("none", False)) -- experimental!
+  ,("-webkit-transform",                  ("none", False)) -- experimental!
+  ,("transform-box",                      ("border-box", False)) -- experimental!
+  ,("transform-origin",                   (mempty {-"50% 50% 0"-}, False)) -- experimental!
+  ,("-webkit-transform-origin",           (mempty {-"50% 50% 0"-}, False)) -- experimental!
+  ,("transform-style",                    ("flat", False)) -- experimental!
+  ,("-webkit-transform-style",            ("flat", False)) -- experimental!
+  ,("transition",                         (mempty, False))-- shorthand and experimental
+  ,("transition-delay",                   ("0s", False)) -- experimental
+  ,("transition-duration",                ("0s", False)) -- experimental
+  ,("-webkit-transition-duration",        ("0s", False)) -- experimental
+  ,("-o-transition-duration",             ("0s", False)) -- experimental
+  ,("-webkit-transition-delay",           ("0s", False)) -- experimental
+  ,("transition-property",                ("all", False)) -- experimental
+  ,("-webkit-transition-property",        ("all", False)) -- experimental
+  ,("-o-transition-property",             ("all", False)) -- experimental
+  ,("transition-timing-function",         ("ease", False)) -- experimental
+  ,("-webkit-transition-timing-function", ("ease", False)) -- experimental
+  ,("unicode-bidi",                       ("normal", False))
+  ,("user-select",                        ("none", False)) -- experimental
+  ,("-moz-user-select",                   ("none", False)) -- experimental
+  ,("-webkit-user-select",                ("none", False)) -- experimental
+  ,("-ms-user-select",                    ("none", False)) -- experimental
+  ,("vertical-align",                     ("baseline", False))
+  ,("white-space",                        ("normal", True))
+  ,("widows",                             ("2", True))
+  ,("width",                              ("auto", False))
+  ,("will-change",                        ("auto", False))
+  ,("word-break",                         ("normal", True))
+  ,("word-spacing",                       ("normal", True))
+  ,("writing-mode",                       ("horizontal-tb", True)) -- experimental
+  ,("-ms-writing-mode",                   ("horizontal-tb", True)) -- experimental
+  ,("-webkit-writing-mode",               ("horizontal-tb", True)) -- experimental
+  ,("z-index",                            ("auto", False))
+  ]
+
+-- Parses text and replaces it with an array of values, to makes the
+-- propertiesTraits table more readable and maintainable in general.
+replaceTextWithValues :: [(Text, (Text, Bool))] -> [(Text, (Maybe Values, Bool))]
+replaceTextWithValues = foldr (\(p,(t,i)) xs -> (p, (getValues p t,i)) : xs) []
+  where getValues p s = case parseOnly (values p) s of
+                          Right initialValues -> Just initialValues
+                          Left _              -> Nothing
diff --git a/src/Hasmin/Selector.hs b/src/Hasmin/Selector.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Selector.hs
@@ -0,0 +1,339 @@
+{-# LANGUAGE OverloadedStrings, FlexibleInstances #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Selector
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Selector (
+      Selector(..)
+    , SimpleSelector(..)
+    , CompoundSelector
+    , Combinator(..)
+    , Sign(..)
+    , AnPlusB(..)
+    , AValue(..)
+    , Att(..)
+    , specialPseudoElements
+    ) where
+
+import Control.Applicative (liftA2)
+import Control.Monad.Reader (ask)
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Lazy.Builder (fromText, singleton, Builder)
+import Data.Monoid ((<>))
+import Data.List.NonEmpty (NonEmpty((:|)))
+import qualified Data.List.NonEmpty as N
+import Text.PrettyPrint.Mainland (Pretty, char, ppr, strictText)
+
+import Hasmin.Config
+import Hasmin.Types.Class
+import Hasmin.Types.String
+import Hasmin.Utils
+
+{-
+class Specificity a where
+  specificity :: a -> (Int, Int, Int, Int)
+
+addSpecificity :: (Int, Int, Int, Int)
+               -> (Int, Int, Int, Int)
+               -> (Int, Int, Int, Int)
+addSpecificity (a1,b1,c1,d1) (a2,b2,c2,d2) = (a1 + a2, b1 + b2, c1 + c2, d1 + d2)
+-}
+
+-- | Combinators are: whitespace, "greater-than sign" (U+003E, >), "plus sign"
+-- (U+002B, +) and "tilde" (U+007E, ~). White space may appear between a
+-- combinator and the simple selectors around it. Only the characters "space"
+-- (U+0020), "tab" (U+0009), "line feed" (U+000A), "carriage return" (U+000D),
+-- and "form feed" (U+000C) can occur in whitespace. Other space-like
+-- characters, such as "em-space" (U+2003) and "ideographic space" (U+3000), are
+-- never part of whitespace.
+data Combinator = Descendant      -- ^ ' '
+                | Child           -- ^ '>'
+                | AdjacentSibling -- ^ '+'
+                | GeneralSibling  -- ^ '~'
+  deriving (Eq, Show)
+
+instance ToText Combinator where
+  toBuilder Descendant      = " "
+  toBuilder Child           = ">"
+  toBuilder AdjacentSibling = "+"
+  toBuilder GeneralSibling  = "~"
+instance Pretty Combinator where
+  ppr = strictText . toText
+
+-- An empty selector, containing no sequence of simple selectors and no
+-- pseudo-element, is an invalid selector.
+data Selector = Selector CompoundSelector [(Combinator, CompoundSelector)]
+  deriving (Eq, Show)
+
+instance Ord Selector where
+  -- Lexicographical order
+  s1 <= s2 = toText s1 <= toText s2
+
+instance Pretty Selector where
+  ppr (Selector cs ccss) = ppr cs
+                        <> mconcat (fmap toDocument ccss)
+    where toDocument (comb, compSel) = ppr comb <> ppr compSel
+instance ToText Selector where
+  toBuilder (Selector cs ccss) = toBuilder cs
+                              <> mconcat (fmap build ccss)
+    where build (comb, compSel) = toBuilder comb <> toBuilder compSel
+instance Minifiable Selector where
+  minifyWith (Selector c xs) = do
+      newC  <- minifyWith c
+      newCs <- (mapM . mapM) minifyWith xs
+      pure $ Selector newC newCs
+
+{-
+instance Specificity Selector where
+  specificity (Selector (x :| xs) ss) =
+      case x of
+        (Type _ _) -> f (0,0,0,1) xs `addSpecificity` g (0,0,0,0) ss
+        (Universal _) -> f (0,0,0,0) xs `addSpecificity` g (0,0,0,0) ss
+    where f = foldr (addSpecificity . specificity)
+          g = foldr (addSpecificity . specificity . snd)
+-}
+
+-- | Called <https://www.w3.org/TR/css3-selectors/#grammar simple_sequence_selector>
+-- in CSS2.1, but <https://drafts.csswg.org/selectors-4/#typedef-compound-selector
+-- compound-selector> in CSS Syntax Module Level 3.
+type CompoundSelector = NonEmpty SimpleSelector
+
+-- instance ToText a => ToText (NonEmpty a) where
+instance ToText CompoundSelector where
+  toBuilder ns@(Universal{} :| xs)
+      | length ns > 1 = mconcat $ fmap toBuilder xs
+  toBuilder ns = mconcat $ N.toList (fmap toBuilder ns)
+
+instance Pretty CompoundSelector where
+  ppr ns@(Universal{} :| xs)
+      | length ns > 1 = mconcat $ fmap ppr xs
+  ppr ns = mconcat $ N.toList (fmap ppr ns)
+
+instance Minifiable CompoundSelector where
+  minifyWith (a :| xs) = liftA2 (:|) (minifyWith a) (mapM minifyWith xs)
+
+{-
+instance Specificity a => Specificity (NonEmpty a) where
+  specificity = foldr (\x y -> specificity x `addSpecificity` y) (0,0,0,0)
+-}
+
+-- | Certain selectors support namespace prefixes. Namespace prefixes are
+-- declared with the @namespace rule. A type selector containing a namespace
+-- prefix that has not been previously declared for namespaced selectors is an
+-- invalid selector.
+type Namespace = Text
+type Element = Text -- e.g.: h1, em, body, ...
+type Identifier = Text
+
+-- Characters in Selectors can be escaped with a backslash according to the same
+-- escaping rules as CSS. [CSS21].
+
+-- If a universal selector represented by * (i.e. without a namespace prefix)
+-- is not the only component of a sequence of simple selectors selectors or is
+-- immediately followed by a pseudo-element, then the * may be omitted and the
+-- universal selector's presence implied.
+
+-- | A simple selector is either a type selector, universal selector, attribute
+-- selector, class selector, ID selector, or pseudo-class.  The first selector
+-- must be a type or universal selector.  When none is specified, the universal
+-- selector is implied, i.e.: #myId is the same as *#myId
+-- mempty is used to denote an empty namespace
+data SimpleSelector = Type Namespace Element -- ^ e.g.: *, ns|*, h1, em, body
+                    | Universal Namespace    -- ^ '*'
+                    | AttributeSel Att
+                    | ClassSel Identifier
+                    | IdSel Identifier
+                    | PseudoElem Identifier
+                    | PseudoClass Identifier
+                    -- generic functional pseudo class
+                    | FunctionalPseudoClass Identifier Text
+                    -- :not() and :matches() fpc
+                    | FunctionalPseudoClass1 Identifier [CompoundSelector] -- :not( <selector># )
+                    -- :nth-of-type(), :nth-last-of-type(), :nth-column(), and
+                    -- :nth-of-last-column() fpc
+                    | FunctionalPseudoClass2 Identifier AnPlusB
+                    -- :nth-child(), nth-last-child()
+                    | FunctionalPseudoClass3 Identifier AnPlusB [CompoundSelector]
+  deriving (Eq, Show)
+
+instance Pretty SimpleSelector where
+  ppr (Type n e)
+      | T.null n  = strictText e
+      | otherwise = strictText n <> char '|' <> strictText e
+  ppr (Universal n)
+      | T.null n  = char '*'
+      | otherwise = strictText n <> strictText "|*"
+  ppr (AttributeSel att) = char '[' <> ppr att <> char ']'
+  ppr (ClassSel t)       = char '.' <> strictText t
+  ppr (IdSel t)          = char '#' <> strictText t
+  ppr (PseudoClass t)    = char ':' <> strictText t
+  ppr (PseudoElem t)     = if T.toCaseFold t `elem` specialPseudoElements
+                              then strictText ":" <> strictText t
+                              else strictText "::" <> strictText t
+  ppr (FunctionalPseudoClass i t)   = strictText i <> char '(' <> ppr t <> char ')'
+  -- ppr (FunctionalPseudoClass1 i cs) = strictText i <>
+
+instance ToText SimpleSelector where
+  toBuilder (Type n e)
+      | T.null n  = fromText e
+      | otherwise = fromText n <> singleton '|' <> fromText e
+  toBuilder (Universal n)
+      | T.null n  = singleton '*'
+      | otherwise = fromText n <> fromText "|*"
+  toBuilder (AttributeSel att) = singleton '[' <> toBuilder att <> singleton ']'
+  toBuilder (ClassSel t)       = singleton '.' <> fromText t
+  toBuilder (IdSel t)          = singleton '#' <> fromText t
+  toBuilder (PseudoClass t)    = singleton ':' <> fromText t
+  toBuilder (PseudoElem t)
+      | T.toCaseFold t `elem` specialPseudoElements = fromText ":" <> fromText t
+      | otherwise                                   = fromText "::" <> fromText t
+  toBuilder (FunctionalPseudoClass t x) = fromText t <> singleton '(' <> fromText x <> singleton ')'
+  toBuilder (FunctionalPseudoClass1 t ss) = singleton ':' <> fromText t <> singleton '('
+      <> mconcatIntersperse toBuilder (singleton ',') ss
+      <> singleton ')'
+  toBuilder (FunctionalPseudoClass2 t x) = singleton ':' <> fromText t
+      <> singleton '(' <> toBuilder x <> singleton ')'
+  toBuilder (FunctionalPseudoClass3 t a xs) = singleton ':' <> fromText t
+      <> singleton '(' <> toBuilder a <> f xs <> singleton ')'
+    where f [] = mempty
+          f (y:ys) = " of " <> toBuilder y
+            <> mconcat (fmap (\z -> singleton ',' <> toBuilder z) ys)
+
+-- Pseudo-elements that support the old pseudo-element syntax of a single
+-- semicolon, as well as the new one of two semicolons.
+specialPseudoElements :: [Text]
+specialPseudoElements = fmap T.toCaseFold
+    ["after", "before", "first-line", "first-letter"]
+
+instance Minifiable SimpleSelector where
+  minifyWith a@(AttributeSel att) = do
+      conf <- ask
+      pure $ if shouldRemoveQuotes conf
+                then AttributeSel (removeAttributeQuotes att)
+                else a
+  minifyWith a@(FunctionalPseudoClass2 i n) = do
+      conf <- ask
+      pure $ if shouldMinifyMicrosyntax conf
+                then FunctionalPseudoClass2 i (minifyAnPlusB n)
+                else a
+  minifyWith a@(FunctionalPseudoClass3 i n cs) = do
+      conf <- ask
+      pure $ if shouldMinifyMicrosyntax conf
+                then FunctionalPseudoClass3 i (minifyAnPlusB n) cs
+                else a
+  minifyWith (FunctionalPseudoClass1 i cs) = do
+      newcs <- mapM minifyWith cs
+      pure $ FunctionalPseudoClass1 i newcs
+  minifyWith x = pure x
+
+data Sign = Plus | Minus
+  deriving (Eq, Show)
+
+isPositive :: Maybe Sign -> Bool
+isPositive Nothing      = True
+isPositive (Just Plus)  = True
+isPositive (Just Minus) = False
+
+instance ToText Sign where
+  toBuilder Plus  = singleton '+'
+  toBuilder Minus = singleton '-'
+
+data AValue = Nwith (Maybe Sign) (Maybe Int) -- at least a lone 'n'
+            | NoValue -- The "An" part is omitted.
+  deriving (Eq, Show)
+instance ToText AValue where
+  toBuilder NoValue     = mempty
+  toBuilder (Nwith s i) = maybeToBuilder s <> maybeToBuilder i <> singleton 'n'
+    where maybeToBuilder :: ToText a => Maybe a -> Builder
+          maybeToBuilder = maybe mempty toBuilder
+
+minifyAValue :: AValue -> AValue
+minifyAValue (Nwith _ (Just 0)) = NoValue
+minifyAValue (Nwith s a)
+    | isPositive s = Nwith Nothing (maybe Nothing droppedOne a)
+    | otherwise    = Nwith s (maybe Nothing droppedOne a)
+  where droppedOne x = if x == 1
+                         then Nothing
+                         else Just x
+minifyAValue NoValue = NoValue
+
+-- We could maybe model the AB constructor with an Either,
+-- to make sure AB NoValue Nothing isn't possible (which is invalid).
+-- Also, modelling a BValue would cover all remaining cases,
+-- for example +6 vs 6, -0 vs 0 vs +0.
+data AnPlusB = Even
+             | Odd
+             | AB AValue (Maybe Int)
+  deriving (Eq, Show)
+instance ToText AnPlusB where
+  toBuilder Even     = "even"
+  toBuilder Odd      = "odd"
+  toBuilder (AB a b) = toBuilder a <> bToBuilder b
+    where bToBuilder
+              | a == NoValue = maybe (singleton '0') toBuilder
+              | otherwise    = maybe mempty (\x -> bSign x <> toBuilder x)
+          bSign x
+              | x < 0     = mempty
+              | otherwise = singleton '+'
+
+minifyAnPlusB :: AnPlusB -> AnPlusB
+minifyAnPlusB Even = AB (Nwith Nothing (Just 2)) Nothing
+minifyAnPlusB (AB n@(Nwith s a) (Just b))
+    | isPositive s && a == Just 2 =
+        if b == 1 || odd b && b < 0
+           then Odd
+           else if even b && b <= 0
+                then minifyAnPlusB Even
+                else AB (minifyAValue n) (Just b)
+    | otherwise = AB (minifyAValue n) $ if b == 0
+                                           then Nothing
+                                           else Just b
+minifyAnPlusB (AB n@Nwith{} Nothing) = AB (minifyAValue n) Nothing
+minifyAnPlusB x = x
+-- instance Specificity SimpleSelector where
+  -- specificity (IdSel _)    = (0,1,0,0)
+  -- specificity (ClassSel _) = (0,0,1,0)
+
+type AttId = Text
+type AttValue = Either Text StringType
+
+data Att = Attribute AttId
+         | AttId :=: AttValue    -- ^ \'=\'
+         | AttId :~=: AttValue   -- ^ \'~=\'
+         | AttId :|=: AttValue   -- ^ \'|=\'
+         | AttId :^=: AttValue   -- ^ \'^=\'
+         | AttId :$=: AttValue   -- ^ \'$=\'
+         | AttId :*=: AttValue   -- ^ \'*=\'
+  deriving (Eq, Show)
+
+removeAttributeQuotes :: Att -> Att
+removeAttributeQuotes (attId :=: val)  = attId :=: either Left removeQuotes val
+removeAttributeQuotes (attId :~=: val) = attId :~=: either Left removeQuotes val
+removeAttributeQuotes (attId :|=: val) = attId :|=: either Left removeQuotes val
+removeAttributeQuotes (attId :^=: val) = attId :^=: either Left removeQuotes val
+removeAttributeQuotes (attId :$=: val) = attId :$=: either Left removeQuotes val
+removeAttributeQuotes (attId :*=: val) = attId :*=: either Left removeQuotes val
+removeAttributeQuotes a@Attribute{}    = a
+
+instance Pretty Att where
+  ppr (Attribute t) = strictText t
+  ppr (attid :=: attval)  = strictText attid <> char '=' <> ppr attval
+  ppr (attid :~=: attval) = strictText attid <> strictText "~=" <> ppr attval
+  ppr (attid :|=: attval) = strictText attid <> strictText "|=" <> ppr attval
+  ppr (attid :^=: attval) = strictText attid <> strictText "^=" <> ppr attval
+  ppr (attid :$=: attval) = strictText attid <> strictText "$=" <> ppr attval
+  ppr (attid :*=: attval) = strictText attid <> strictText "*=" <> ppr attval
+instance ToText Att where
+  toBuilder (Attribute t) = fromText t
+  toBuilder (attid :=: attval)  = fromText attid <> singleton '=' <> toBuilder attval
+  toBuilder (attid :~=: attval) = fromText attid <> fromText "~=" <> toBuilder attval
+  toBuilder (attid :|=: attval) = fromText attid <> fromText "|=" <> toBuilder attval
+  toBuilder (attid :^=: attval) = fromText attid <> fromText "^=" <> toBuilder attval
+  toBuilder (attid :$=: attval) = fromText attid <> fromText "$=" <> toBuilder attval
+  toBuilder (attid :*=: attval) = fromText attid <> fromText "*=" <> toBuilder attval
diff --git a/src/Hasmin/Types/BgSize.hs b/src/Hasmin/Types/BgSize.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/BgSize.hs
@@ -0,0 +1,52 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.BgSize
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.BgSize (
+    BgSize(..), Auto(..)
+    ) where
+import Control.Monad.Reader (ask)
+import Data.Monoid ((<>))
+import Data.Text.Lazy.Builder (singleton)
+import Hasmin.Types.Class
+import Hasmin.Types.PercentageLength
+
+data Auto = Auto
+  deriving (Eq, Show)
+
+instance ToText Auto where
+  toBuilder Auto = "auto"
+
+data BgSize = Cover 
+            | Contain
+            | BgSize (Either PercentageLength Auto) (Maybe (Either PercentageLength Auto))
+  deriving (Eq, Show)
+
+instance ToText BgSize where
+  toBuilder Cover = "cover"
+  toBuilder Contain = "contain"
+  toBuilder (BgSize x y) = toBuilder x <> maybe mempty (\a -> singleton ' ' <> toBuilder a) y
+
+instance Minifiable BgSize where
+  minifyWith (BgSize x y) = do
+      conf <- ask
+      nx   <- minFirst x
+      ny   <- mapM minFirst y
+      let b = BgSize nx ny
+      pure $ if True {- shouldMinifyBgSize conf -}
+                then minifyBgSize b
+                else b
+    where minFirst (Left a) = Left <$> minifyWith a
+          minFirst (Right Auto) = pure (Right Auto)
+  minifyWith x = pure x
+
+minifyBgSize :: BgSize -> BgSize
+minifyBgSize (BgSize l (Just (Right Auto))) = BgSize l Nothing
+minifyBgSize x = x
+
diff --git a/src/Hasmin/Types/Class.hs b/src/Hasmin/Types/Class.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Class.hs
@@ -0,0 +1,47 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Class
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Class (
+    ToText(..), Minifiable(..)
+    ) where
+
+import Control.Monad.Reader (Reader, runReader)
+import Data.Word (Word8)
+import Data.Text (pack, Text)
+import Data.Text.Lazy (toStrict)
+import Data.Text.Lazy.Builder (Builder, fromText, toLazyText)
+import Hasmin.Config
+
+-- | Class for types that can be minified
+class Minifiable a where 
+  {-# MINIMAL minifyWith #-}
+  minifyWith :: a -> Reader Config a
+
+  -- Returns smallest equivalent representation for an element, using a default
+  -- configuration. Mostly used for testing.
+  minify :: a -> a 
+  minify x = runReader (minifyWith x) defaultConfig
+
+-- | Class for types that can be converted to Text. Used for priting the
+-- minified results.
+class ToText a where
+  {-# MINIMAL toText | toBuilder #-}
+  toText :: a -> Text
+  toBuilder :: a -> Builder
+  toText    = toStrict . toLazyText . toBuilder
+  toBuilder = fromText . toText
+instance ToText Word8 where
+  toText = pack . show
+instance ToText Int where
+  toText = pack . show
+instance ToText Text where
+  toText = id
+instance (ToText a, ToText b) => ToText (Either a b) where
+  toText = either toText toText
+  toBuilder = either toBuilder toBuilder
diff --git a/src/Hasmin/Types/Color.hs b/src/Hasmin/Types/Color.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Color.hs
@@ -0,0 +1,510 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Color
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- \<color> data type.
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Color
+  ( Color(Named)
+  , mkHex3, mkHex4, mkHex6, mkHex8, mkNamed
+  , mkHSL, mkHSLA, mkRGBInt, mkRGBPer, mkRGBAInt, mkRGBAPer
+  , keywordColors, minifyColor
+  ) where
+
+import Control.Arrow (first)
+import Control.Monad.Reader (ask)
+import Data.Char (isHexDigit, digitToInt, intToDigit, toLower)
+import Data.Maybe (fromMaybe)
+import Data.Ratio ((%))
+import Data.Text (pack, Text)
+import Data.Text.Lazy.Builder (Builder, singleton, fromText)
+import Data.Word (Word8)
+import qualified Data.Map.Strict as Map
+import qualified Data.Text as T
+import Text.PrettyPrint.Mainland (Pretty, ppr, strictText, string, char, (<>), (<+>), comma, rparen)
+
+import Hasmin.Config
+import Hasmin.Types.Class
+import Hasmin.Types.Numeric
+import Hasmin.Utils
+
+-- | The \<color\> CSS data type. Specifications:
+--
+-- 4. <https://drafts.csswg.org/css-color/#colorunits  CSS Color Module Level 4>
+-- 3. <https://drafts.csswg.org/css-color-3/#colorunits CSS Color Module Level 3>
+-- 2. <https://www.w3.org/TR/CSS2/syndata.html#value-def-color CSS2.1>
+-- 1. <https://www.w3.org/TR/CSS1/#color-units CSS1>
+data Color = Hex3     Char Char Char
+           | Hex4     Char Char Char Char
+           | Hex6     String String String
+           | Hex8     String String String String
+           | Named    Text
+           | RGBInt   Word8 Word8 Word8
+           | RGBPer   Percentage Percentage Percentage
+           | RGBAInt  Word8 Word8 Word8 Alphavalue
+           | RGBAPer  Percentage Percentage Percentage Alphavalue
+           | HSL      Int Percentage Percentage
+           | HSLA     Int Percentage Percentage Alphavalue
+  deriving (Show)
+
+-- | Equality is slightly relaxed, since percentages and real numbers are mapped
+-- to the [0,255] integer range, and then compared
+instance Eq Color where
+  (Hex6 r1 g1 b1) == (Hex6 r2 g2 b2) =
+    r1 == r2 && g1 == g2 && b1 == b2
+  (Hex8 r1 g1 b1 a1) == (Hex8 r2 g2 b2 a2) =
+    r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2
+  (Hex8 r1 g1 b1 a) == (Hex6 r2 g2 b2)
+    | a == "ff" = r1 == r2 && g1 == g2 && b1 == b2
+    | otherwise = False
+  (Hex6 r1 g1 b1) == (Hex8 r2 g2 b2 a)
+    | a == "ff" = r1 == r2 && g1 == g2 && b1 == b2
+    | otherwise = False
+  c1 == (Named s) = case Map.lookup (T.toLower s) colorMap of
+                      Just a  -> a == c1
+                      Nothing -> False
+  (Named s) == c2 = case Map.lookup (T.toLower s) colorMap of
+                      Just a  -> a == c2
+                      Nothing -> False
+  a == b = toLongHex a == toLongHex b
+
+instance Ord Color where
+  (Hex6 r1 g1 b1) <= (Hex6 r2 g2 b2) =
+    r1 < r2 || r1 == r2 && (g1 < g2 || (g1 == g2 && b1 <= b2))
+  (Hex8 r1 g1 b1 a1) <= (Hex8 r2 g2 b2 a2) =
+    r1 < r2 || r1 == r2 && (g1 < g2 || (g1 == g2 && (b1 < b2 || (b1 == b2 && a1 <= a2))))
+  (Hex8 r1 g1 b1 a) <= (Hex6 r2 g2 b2)
+    | a == "ff" =  r1 < r2 || r1 == r2 && (g1 < g2 || (g1 == g2 && b1 <= b2))
+    | otherwise  = True
+  (Hex6 r1 g1 b1) <= (Hex8 r2 g2 b2 a)
+    | a == "ff" =  r1 < r2 || r1 == r2 && (g1 < g2 || (g1 == g2 && b1 <= b2))
+    | otherwise  = False
+  c1 <= c2 = toLongHex c1 <= toLongHex c2
+
+instance Pretty Color where
+  ppr (Hex3 r g b)   = char '#' <> char r <> char g <> char b
+  ppr (Hex4 r g b a) = char '#' <> char r <> char g <> char b <> char a
+  ppr (Hex6 r g b)   = char '#' <> string r <> string g <> string b
+  ppr (Hex8 r g b a) = char '#' <> string r <> string g <> string b <> string a
+  ppr (Named n)      = strictText n
+  ppr (RGBInt r g b) = strictText "rgb(" <> ppr r <> comma
+                                        <+> ppr g <> comma
+                                        <+> ppr b <> rparen
+  ppr (RGBPer r g b) = strictText "rgb(" <> ppr r <> comma
+                                        <+> ppr g <> comma
+                                        <+> ppr b <> rparen
+  ppr (RGBAInt r g b a) = strictText "rgba(" <> ppr r <> comma
+                                            <+> ppr g <> comma
+                                            <+> ppr b <> comma
+                                            <+> ppr a <> rparen
+  ppr (RGBAPer r g b a) = strictText "rgba(" <> ppr r <> comma
+                                            <+> ppr g <> comma
+                                            <+> ppr b <> comma
+                                            <+> ppr a <> rparen
+  ppr (HSL h s l) = strictText "hsl(" <> ppr h <> comma
+                                     <+> ppr s <> comma
+                                     <+> ppr l <> rparen
+  ppr (HSLA h s l a) = strictText "hsla(" <> ppr h <> comma
+                                         <+> ppr s <> comma
+                                         <+> ppr l <> comma
+                                         <+> ppr a <> rparen
+
+instance Minifiable Color where
+  minifyWith c = do
+      conf <- ask
+      pure $ case colorSettings conf of
+                ColorMinOn  -> minifyColor c
+                ColorMinOff -> c
+
+minifyColor :: Color -> Color
+minifyColor c@Hex6{} = fromMaybe (toHexShorthand c) (Map.lookup c minimalColorMap)
+minifyColor c@(Hex8 r g b a)
+    | a == "ff" = minifyColor (Hex6 r g b)
+    | otherwise = toHexShorthand c
+minifyColor c@(RGBAPer r g b a)
+    | a >= 1    = minifyColor (RGBPer r g b)
+    | otherwise = minifyColor $ toLongHex c
+minifyColor c@(RGBAInt r g b a)
+    | a >= 1    = minifyColor (RGBInt r g b)
+    | otherwise = minifyColor $ toLongHex c
+minifyColor c@(HSLA h s l a)
+    | a >= 1    = minifyColor (HSL h s l)
+    | otherwise = minifyColor $ toLongHex c
+minifyColor c = case toLongHex c of
+                n@(Named _) -> n
+                other -> minifyColor other
+
+instance ToText Color where
+  toBuilder (RGBInt r g b)    = "rgb(" <> values <> singleton ')'
+    where values = toBuilderWithCommas [toText r, toText g, toText b]
+  toBuilder (RGBAInt r g b a) = "rgba(" <> values <> singleton ')'
+    where values = toBuilderWithCommas [toText r, toText g, toText b, toText a]
+  toBuilder (RGBPer r g b)    = "rgb(" <> values <> singleton ')'
+    where values = toBuilderWithCommas [toText r, toText g, toText b]
+  toBuilder (RGBAPer r g b a) = "rgba(" <> values <> singleton ')'
+    where values = toBuilderWithCommas [toText r, toText g, toText b, toText a]
+  toBuilder (HSL h s l)       = "hsl(" <> values <> singleton ')'
+    where values = toBuilderWithCommas [toText h, toText s, toText l]
+  toBuilder (HSLA h s l a)    = "hsla(" <> values <> singleton ')'
+    where values = toBuilderWithCommas [toText h, toText s, toText l, toText a]
+  toBuilder (Named a)         = fromText a
+  toBuilder (Hex3 r g b)      = singleton '#' <> singleton r <> singleton g <> singleton b
+  toBuilder (Hex4 r g b a)    = singleton '#' <> singleton r <> singleton g <> singleton b <> singleton a
+  toBuilder (Hex6 r g b)      = fromText . pack $ mconcat ["#", r, g, b]
+  toBuilder (Hex8 r g b a)    = fromText . pack $ mconcat ["#", r, g, b, a]
+
+toBuilderWithCommas :: [Text] -> Builder
+toBuilderWithCommas = mconcatIntersperse fromText (singleton ',')
+
+-- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+--                              Smart constructors
+-- %%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%%
+
+mkHex3 :: Char -> Char -> Char -> Color
+mkHex3 r g b
+  | isHexDigit r && isHexDigit g && isHexDigit b = Hex3 (toLower r) (toLower g) (toLower b)
+  | otherwise = error "passing non hexadecimal arguments to mkHex3"
+
+mkHex6 :: String -> String -> String -> Color
+mkHex6 r g b
+  | allHex r && allHex g && allHex b = Hex6 (strToLower r) (strToLower g) (strToLower b)
+  | otherwise = error "passing non hexadecimal arguments to mkHex6"
+
+mkHex4 :: Char -> Char -> Char -> Char -> Color
+mkHex4 r g b a
+  | isHexDigit r && isHexDigit g && isHexDigit b && isHexDigit a = Hex4 (toLower r) (toLower g) (toLower b) (toLower a)
+  | otherwise = error "passing non hexadecimal arguments to mkHex4"
+
+mkHex8 :: String -> String -> String -> String -> Color
+mkHex8 r g b a
+  | allHex r && allHex g && allHex b = Hex8 (strToLower r) (strToLower g) (strToLower b) (strToLower a)
+  | otherwise = error "passing non hexadecimal arguments to mkHex6"
+
+mkNamed :: Text -> Maybe Color
+mkNamed colorName
+    | Map.member name colorMap = Just (Named name)
+    | otherwise                = Nothing
+  where name = T.toLower colorName
+
+mkHSL :: Int -> Percentage -> Percentage -> Color
+mkHSL h s l = HSL (h `mod` 360) (bound s) (bound l)
+  where bound = restrict 0 100
+
+mkHSLA :: Int -> Percentage -> Percentage -> Alphavalue -> Color
+mkHSLA h s l = HSLA (h `mod` 360) (bound s) (bound l)
+  where bound = restrict 0 100
+
+mkRGBInt :: Word8 -> Word8 -> Word8 -> Color
+mkRGBInt r g b = RGBInt (bound r) (bound g) (bound b)
+  where bound = restrict 0 255
+
+mkRGBPer :: Percentage -> Percentage -> Percentage -> Color
+mkRGBPer r g b = RGBPer (bound r) (bound g) (bound b)
+  where bound = restrict 0 100
+
+mkRGBAInt :: Word8 -> Word8 -> Word8 -> Alphavalue -> Color
+mkRGBAInt r g b = RGBAInt (bound r) (bound g) (bound b)
+  where bound = restrict 0 255
+
+mkRGBAPer :: Percentage -> Percentage -> Percentage -> Alphavalue -> Color
+mkRGBAPer r g b =  RGBAPer (bound r) (bound g) (bound b)
+  where bound = restrict 0 100
+
+allHex :: String -> Bool
+allHex = all isHexDigit
+
+strToLower :: String -> String
+strToLower = map toLower
+-------------------------------------------------------------------------------
+toHexShorthand :: Color -> Color
+toHexShorthand c@(Hex6 [r1,r2] [g1,g2] [b1,b2])
+  | r1 == r2 && g1 == g2 && b1 == b2 = Hex3 r1 g1 b1
+  | otherwise                        = c
+toHexShorthand c@(Hex8 [r1,r2] [g1,g2] [b1,b2] [a1,a2])
+  | r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2 = Hex4 r1 g1 b1 a1
+  | otherwise                                    = c
+toHexShorthand h = h
+
+-- Returns hexadecimal equivalent as a string of two characters
+-- (i.e. for values in the range [0,15], a leading zero is added).
+word8ToHex :: Word8 -> String
+word8ToHex n | 0 <= n && n < 16 = '0':[intToDigit num]
+             | otherwise        = intToDigit sndRemainder : [intToDigit fstRemainder]
+  where num = fromIntegral n
+        fstRemainder = num `mod` 16
+        sndRemainder = (num `quot` 16) `mod` 16
+
+-- Takes a color to a Hex6 or Hex8 representation unless it's an invalid
+-- keyword, in which case it remains the same
+toLongHex :: Color -> Color
+toLongHex c@(Named s)         = fromMaybe c (Map.lookup (T.toLower s) colorMap)
+toLongHex (RGBAInt r g b a)   = Hex8 (word8ToHex r) (word8ToHex g) (word8ToHex b) (ratToHex a)
+  where ratToHex :: Alphavalue -> String
+        ratToHex n = word8ToHex . round $ toRational n * 255
+toLongHex (RGBInt r g b) = Hex6 (word8ToHex r) (word8ToHex g) (word8ToHex b)
+toLongHex c@RGBPer{}     = toLongHex $ toRGBAInt c
+toLongHex c@RGBAPer{}    = toLongHex $ toRGBAInt c
+toLongHex c@HSL{}        = toLongHex $ toRGBAInt c
+toLongHex c@HSLA{}       = toLongHex $ toRGBAInt c
+toLongHex (Hex3 r g b)   = Hex6 [r,r] [g,g] [b,b]
+toLongHex (Hex4 r g b a) = Hex8 [r,r] [g,g] [b,b] [a,a]
+toLongHex a              = a
+
+-- This fold works in general to convert hexadecimals into Integers, but we
+-- only need it for Word8
+hexToWord8 :: String -> Word8
+hexToWord8 = fromIntegral . foldl (\s c -> s*16 + digitToInt c) 0
+
+toRGBAInt :: Color -> Color
+toRGBAInt (Named s) = case Map.lookup (T.toLower s) colorMap of
+                        Just a  -> toRGBAInt a
+                        Nothing -> error e
+  where e = T.unpack $ "Invalid color keyword (" <> s <> "). Can't convert to rgba"
+toRGBAInt (Hex3 r g b) = RGBAInt (f [r,r]) (f [g,g]) (f [b,b]) 1
+  where f = fromIntegral . hexToWord8
+toRGBAInt (Hex6 r g b) = RGBAInt (hexToWord8 r) (hexToWord8 g) (hexToWord8 b) 1
+toRGBAInt (Hex4 r g b a) = RGBAInt (f [r,r]) (f [g,g]) (f [b,b]) (h [a,a])
+  where f = fromIntegral . hexToWord8
+        h = toAlphavalue . hexToWord8
+toRGBAInt (Hex8 r g b a) = RGBAInt (hexToWord8 r) (hexToWord8 g)
+                                   (hexToWord8 b) (toAlphavalue $ toRational (hexToWord8 a) / 255)
+toRGBAInt (RGBInt r g b) = RGBAInt r g b 1
+toRGBAInt (RGBPer r g b) = RGBAInt (f r) (f g) (f b) 1
+  where f = round . (2.55*)
+toRGBAInt (RGBAPer r g b a) = RGBAInt (f r) (f g) (f b) a
+  where f = round . (2.55*)
+toRGBAInt c@RGBAInt{}    = c
+toRGBAInt (HSL h s l)    = withAlpha 1 $ hslToRgb (h, s, l)
+toRGBAInt (HSLA h s l a) = withAlpha a $ hslToRgb (h, s, l)
+
+withAlpha :: Alphavalue -> (Word8, Word8, Word8) -> Color
+withAlpha a (r, g, b) = RGBAInt r g b a
+
+hslToRgb :: (Int, Percentage, Percentage) -> (Word8, Word8, Word8)
+hslToRgb (hue, sat, light) | s == 0    = (lumToRgb, lumToRgb, lumToRgb)
+                           | l <= 0.5  = hslToRgb' h l (l * (s+1))
+                           | otherwise = hslToRgb' h l (l + s - l*s)
+  where h = toPercentage hue / 360
+        s = sat / 100
+        l = light / 100
+        lumToRgb = round (l * 255)
+
+hslToRgb' :: Percentage -> Percentage -> Percentage -> (Word8, Word8, Word8)
+hslToRgb' h l t2 = (r, g, b)
+  where t1 = l*2 - t2
+        r = round $ 255 * hueToRgb t1 t2 (h + Percentage (1 % 3))
+        g = round $ 255 * hueToRgb t1 t2 h
+        b = round $ 255 * hueToRgb t1 t2 (h - Percentage (1 % 3))
+
+hueToRgb :: Percentage -> Percentage -> Percentage -> Percentage
+hueToRgb t1 t2 hue | hue < 0   = test t1 t2 (hue+1)
+                   | hue > 1   = test t1 t2 (hue-1)
+                   | otherwise = test t1 t2 hue
+  where test :: Percentage -> Percentage -> Percentage -> Percentage
+        test a b h | h * 6 < 1 = a + (b-a) * 6 * h
+                   | h * 2 < 1 = b
+                   | h * 3 < 2 = a + (b-a) * (Percentage (2 % 3) - h) * 6
+                   | otherwise = a
+
+-- | A map with hex values as keys, and their minimal colorname as value
+minimalColorMap :: Map.Map Color Color
+minimalColorMap = Map.fromList minimalColors
+
+minimalColors :: [(Color, Color)]
+minimalColors = [(Hex6 "ff" "00" "00", Named "red")
+                ,(Hex6 "d2" "b4" "8c", Named "tan")
+                ,(Hex6 "00" "ff" "ff", Named "aqua")
+                ,(Hex6 "00" "00" "ff", Named "blue")
+                ,(Hex6 "00" "ff" "ff", Named "cyan")
+                ,(Hex6 "ff" "d7" "00", Named "gold")
+                ,(Hex6 "80" "80" "80", Named "gray")
+                ,(Hex6 "80" "80" "80", Named "grey")
+                ,(Hex6 "00" "ff" "00", Named "lime")
+                ,(Hex6 "00" "00" "80", Named "navy")
+                ,(Hex6 "cd" "85" "3f", Named "peru")
+                ,(Hex6 "ff" "c0" "cb", Named "pink")
+                ,(Hex6 "dd" "a0" "dd", Named "plum")
+                ,(Hex6 "ff" "fa" "fa", Named "snow")
+                ,(Hex6 "00" "80" "80", Named "teal")
+                ,(Hex6 "f0" "ff" "ff", Named "azure")
+                ,(Hex6 "f5" "f5" "dc", Named "beige")
+                ,(Hex6 "a5" "2a" "2a", Named "brown")
+                ,(Hex6 "ff" "7f" "50", Named "coral")
+                ,(Hex6 "00" "80" "00", Named "green")
+                ,(Hex6 "ff" "ff" "f0", Named "ivory")
+                ,(Hex6 "f0" "e6" "8c", Named "khaki")
+                ,(Hex6 "fa" "f0" "e6", Named "linen")
+                ,(Hex6 "80" "80" "00", Named "olive")
+                ,(Hex6 "f5" "de" "b3", Named "wheat")
+                ,(Hex6 "ff" "e4" "c4", Named "bisque")
+                ,(Hex6 "4b" "00" "82", Named "indigo")
+                ,(Hex6 "80" "00" "00", Named "maroon")
+                ,(Hex6 "ff" "a5" "00", Named "orange")
+                ,(Hex6 "da" "70" "d6", Named "orchid")
+                ,(Hex6 "80" "00" "80", Named "purple")
+                ,(Hex6 "fa" "80" "72", Named "salmon")
+                ,(Hex6 "a0" "52" "2d", Named "sienna")
+                ,(Hex6 "c0" "c0" "c0", Named "silver")
+                ,(Hex6 "ff" "63" "47", Named "tomato")
+                ,(Hex6 "ee" "82" "ee", Named "violet")]
+
+-- | Mapping between color names and hex values
+colorMap :: Map.Map Text Color
+colorMap = Map.fromList keywordColors
+
+keywordColors :: [(Text, Color)]
+keywordColors = map (first T.toLower)
+  [("aliceblue",            Hex6 "f0" "f8" "ff")
+  ,("antiquewhite",         Hex6 "fa" "eb" "d7")
+  ,("aqua",                 Hex6 "00" "ff" "ff")
+  ,("aquamarine",           Hex6 "7f" "ff" "d4")
+  ,("azure",                Hex6 "f0" "ff" "ff")
+  ,("beige",                Hex6 "f5" "f5" "dc")
+  ,("bisque",               Hex6 "ff" "e4" "c4")
+  ,("black",                Hex6 "00" "00" "00")
+  ,("blanchedalmond",       Hex6 "ff" "eb" "cd")
+  ,("blue",                 Hex6 "00" "00" "ff")
+  ,("blueviolet",           Hex6 "8a" "2b" "e2")
+  ,("brown",                Hex6 "a5" "2a" "2a")
+  ,("burlywood",            Hex6 "de" "b8" "87")
+  ,("cadetblue",            Hex6 "5f" "9e" "a0")
+  ,("chartreuse",           Hex6 "7f" "ff" "00")
+  ,("chocolate",            Hex6 "d2" "69" "1e")
+  ,("coral",                Hex6 "ff" "7f" "50")
+  ,("cornflowerblue",       Hex6 "64" "95" "ed")
+  ,("cornsilk",             Hex6 "ff" "f8" "dc")
+  ,("crimson",              Hex6 "dc" "14" "3c")
+  ,("cyan",                 Hex6 "00" "ff" "ff")
+  ,("darkblue",             Hex6 "00" "00" "8b")
+  ,("darkcyan",             Hex6 "00" "8b" "8b")
+  ,("darkgoldenrod",        Hex6 "b8" "86" "0b")
+  ,("darkgray",             Hex6 "a9" "a9" "a9")
+  ,("darkgrey",             Hex6 "a9" "a9" "a9")
+  ,("darkgreen",            Hex6 "00" "64" "00")
+  ,("darkkhaki",            Hex6 "bd" "b7" "6b")
+  ,("darkmagenta",          Hex6 "8b" "00" "8b")
+  ,("darkolivegreen",       Hex6 "55" "6b" "2f")
+  ,("darkorange",           Hex6 "ff" "8c" "00")
+  ,("darkorchid",           Hex6 "99" "32" "cc")
+  ,("darkred",              Hex6 "8b" "00" "00")
+  ,("darksalmon",           Hex6 "e9" "96" "7a")
+  ,("darkseagreen",         Hex6 "8f" "bc" "8f")
+  ,("darkslateblue",        Hex6 "48" "3d" "8b")
+  ,("darkslategray",        Hex6 "2f" "4f" "4f")
+  ,("darkslategrey",        Hex6 "2f" "4f" "4f")
+  ,("darkturquoise",        Hex6 "00" "ce" "d1")
+  ,("darkviolet",           Hex6 "94" "00" "d3")
+  ,("deeppink",             Hex6 "ff" "14" "93")
+  ,("deepskyblue",          Hex6 "00" "bf" "ff")
+  ,("dimgray",              Hex6 "69" "69" "69")
+  ,("dimgrey",              Hex6 "69" "69" "69")
+  ,("dodgerblue",           Hex6 "1e" "90" "ff")
+  ,("firebrick",            Hex6 "b2" "22" "22")
+  ,("floralwhite",          Hex6 "ff" "fa" "f0")
+  ,("forestgreen",          Hex6 "22" "8b" "22")
+  ,("fuchsia",              Hex6 "ff" "00" "ff")
+  ,("gainsboro",            Hex6 "dc" "dc" "dc")
+  ,("ghostwhite",           Hex6 "f8" "f8" "ff")
+  ,("gold",                 Hex6 "ff" "d7" "00")
+  ,("goldenrod",            Hex6 "da" "a5" "20")
+  ,("gray",                 Hex6 "80" "80" "80")
+  ,("grey",                 Hex6 "80" "80" "80")
+  ,("green",                Hex6 "00" "80" "00")
+  ,("greenyellow",          Hex6 "ad" "ff" "2f")
+  ,("honeydew",             Hex6 "f0" "ff" "f0")
+  ,("hotpink",              Hex6 "ff" "69" "b4")
+  ,("indianred",            Hex6 "cd" "5c" "5c")
+  ,("indigo",               Hex6 "4b" "00" "82")
+  ,("ivory",                Hex6 "ff" "ff" "f0")
+  ,("khaki",                Hex6 "f0" "e6" "8c")
+  ,("lavender",             Hex6 "e6" "e6" "fa")
+  ,("lavenderblush",        Hex6 "ff" "f0" "f5")
+  ,("lawngreen",            Hex6 "7c" "fc" "00")
+  ,("lemonchiffon",         Hex6 "ff" "fa" "cd")
+  ,("lightblue",            Hex6 "ad" "d8" "e6")
+  ,("lightcoral",           Hex6 "f0" "80" "80")
+  ,("lightcyan",            Hex6 "e0" "ff" "ff")
+  ,("lightgoldenrodyellow", Hex6 "fa" "fa" "d2")
+  ,("lightgray",            Hex6 "d3" "d3" "d3")
+  ,("lightgrey",            Hex6 "d3" "d3" "d3")
+  ,("lightgreen",           Hex6 "90" "ee" "90")
+  ,("lightpink",            Hex6 "ff" "b6" "c1")
+  ,("lightsalmon",          Hex6 "ff" "a0" "7a")
+  ,("lightseagreen",        Hex6 "20" "b2" "aa")
+  ,("lightskyblue",         Hex6 "87" "ce" "fa")
+  ,("lightslategray",       Hex6 "77" "88" "99")
+  ,("lightslategrey",       Hex6 "77" "88" "99")
+  ,("lightsteelblue",       Hex6 "b0" "c4" "de")
+  ,("lightyellow",          Hex6 "ff" "ff" "e0")
+  ,("lime",                 Hex6 "00" "ff" "00")
+  ,("limegreen",            Hex6 "32" "cd" "32")
+  ,("linen",                Hex6 "fa" "f0" "e6")
+  ,("magenta",              Hex6 "ff" "00" "ff")
+  ,("maroon",               Hex6 "80" "00" "00")
+  ,("mediumaquamarine",     Hex6 "66" "cd" "aa")
+  ,("mediumblue",           Hex6 "00" "00" "cd")
+  ,("mediumorchid",         Hex6 "ba" "55" "d3")
+  ,("mediumpurple",         Hex6 "93" "70" "d8")
+  ,("mediumseagreen",       Hex6 "3c" "b3" "71")
+  ,("mediumslateblue",      Hex6 "7b" "68" "ee")
+  ,("mediumspringgreen",    Hex6 "00" "fa" "9a")
+  ,("mediumturquoise",      Hex6 "48" "d1" "cc")
+  ,("mediumvioletred",      Hex6 "c7" "15" "85")
+  ,("midnightblue",         Hex6 "19" "19" "70")
+  ,("mintcream",            Hex6 "f5" "ff" "fa")
+  ,("mistyrose",            Hex6 "ff" "e4" "e1")
+  ,("moccasin",             Hex6 "ff" "e4" "b5")
+  ,("navajowhite",          Hex6 "ff" "de" "ad")
+  ,("navy",                 Hex6 "00" "00" "80")
+  ,("oldlace",              Hex6 "fd" "f5" "e6")
+  ,("olive",                Hex6 "80" "80" "00")
+  ,("olivedrab",            Hex6 "6b" "8e" "23")
+  ,("orange",               Hex6 "ff" "a5" "00")
+  ,("orangered",            Hex6 "ff" "45" "00")
+  ,("orchid",               Hex6 "da" "70" "d6")
+  ,("palegoldenrod",        Hex6 "ee" "e8" "aa")
+  ,("palegreen",            Hex6 "98" "fb" "98")
+  ,("paleturquoise",        Hex6 "af" "ee" "ee")
+  ,("palevioletred",        Hex6 "d8" "70" "93")
+  ,("papayawhip",           Hex6 "ff" "ef" "d5")
+  ,("peachpuff",            Hex6 "ff" "da" "b9")
+  ,("peru",                 Hex6 "cd" "85" "3f")
+  ,("pink",                 Hex6 "ff" "c0" "cb")
+  ,("plum",                 Hex6 "dd" "a0" "dd")
+  ,("powderblue",           Hex6 "b0" "e0" "e6")
+  ,("purple",               Hex6 "80" "00" "80")
+  ,("red",                  Hex6 "ff" "00" "00")
+  ,("rosybrown",            Hex6 "bc" "8f" "8f")
+  ,("royalblue",            Hex6 "41" "69" "e1")
+  ,("saddlebrown",          Hex6 "8b" "45" "13")
+  ,("salmon",               Hex6 "fa" "80" "72")
+  ,("sandybrown",           Hex6 "f4" "a4" "60")
+  ,("seagreen",             Hex6 "2e" "8b" "57")
+  ,("seashell",             Hex6 "ff" "f5" "ee")
+  ,("sienna",               Hex6 "a0" "52" "2d")
+  ,("silver",               Hex6 "c0" "c0" "c0")
+  ,("skyblue",              Hex6 "87" "ce" "eb")
+  ,("slateblue",            Hex6 "6a" "5a" "cd")
+  ,("slategray",            Hex6 "70" "80" "90")
+  ,("slategrey",            Hex6 "70" "80" "90")
+  ,("snow",                 Hex6 "ff" "fa" "fa")
+  ,("springgreen",          Hex6 "00" "ff" "7f")
+  ,("steelblue",            Hex6 "46" "82" "b4")
+  ,("tan",                  Hex6 "d2" "b4" "8c")
+  ,("teal",                 Hex6 "00" "80" "80")
+  ,("thistle",              Hex6 "d8" "bf" "d8")
+  ,("transparent",          Hex8 "00" "00" "00" "00")
+  ,("tomato",               Hex6 "ff" "63" "47")
+  ,("turquoise",            Hex6 "40" "e0" "d0")
+  ,("violet",               Hex6 "ee" "82" "ee")
+  ,("wheat",                Hex6 "f5" "de" "b3")
+  ,("white",                Hex6 "ff" "ff" "ff")
+  ,("whitesmoke",           Hex6 "f5" "f5" "f5")
+  ,("yellow",               Hex6 "ff" "ff" "00")
+  ,("yellowgreen",          Hex6 "9a" "cd" "32")]
diff --git a/src/Hasmin/Types/Declaration.hs b/src/Hasmin/Types/Declaration.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Declaration.hs
@@ -0,0 +1,555 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Declaration
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+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  -- ends with !important
+                               , hasIEhack :: Bool    -- ends with \9
+                               } 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
+
+-- Map relating properties with specific functions to optimize them
+propertyOptimizations :: Map Text (Declaration -> Reader Config Declaration)
+propertyOptimizations = Map.fromList
+  [("transform",                  combineTransformFunctions)
+  ,("-webkit-transform",          combineTransformFunctions)
+  ,("-moz-transform",             combineTransformFunctions)
+  -- ,("font",              optimizeValues optimizeFontFamily)
+  ,("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")
+  -- ,("outline",         replaceWithZero "none")
+  -- ,("border",          replaceWithZero "none")
+  -- ,("background",      replaceWithZero "0 0")
+  ,("transform-origin",         optimizeTransformOrigin >=> nullPercentageToLength)
+  ,("-o-transform-origin",      optimizeTransformOrigin >=> nullPercentageToLength)
+  ,("-moz-transform-origin",    optimizeTransformOrigin >=> nullPercentageToLength)
+  ,("-ms-transform-origin",     optimizeTransformOrigin >=> nullPercentageToLength)
+  ,("-webkit-transform-origin", optimizeTransformOrigin >=> nullPercentageToLength)
+  ]
+
+-- Generic function to map some optimization to a property's values.
+optimizeValues :: (Value -> Reader Config Value) 
+               -> Declaration -> Reader Config Declaration
+optimizeValues f d@(Declaration _ vs _ _) = do
+    newV <- mapValues f vs
+    pure $ d {valueList = newV }
+
+-- converts 0% into 0 (of type <length>)
+-- Do NOT use it with height and max-height, since 0% /= 0
+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 
+
+-- For word-spacing, normal computes to 0.
+-- For vertical-align, baseline is the same as 0.
+--
+-- Careful: don't apply it for letter-spacing because there is a slight
+-- difference between normal and 0 for that property!
+replaceWithZero :: Text -> Declaration -> Reader Config Declaration
+replaceWithZero s d@(Declaration p (Values v vs) _ _)
+    | not (null vs) = pure d -- Some error occured, since there should be only one value
+    | 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
+
+-- Converts the keywords "normal" and "bold" to 400 and 700, respectively.
+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 is needed because we are using a generic parser for
+-- transform-origin, so 0 parses as a number instead of a distance.
+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
+
+-- transform-origin keyword meanings.
+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))]
+
+
+-- | Minifies a declaration, based on the property's specific traits (i.e. if
+-- it inherits or not, and what its initial value is), and the chosen
+-- configurations.
+minifyDec :: Declaration -> Maybe Values -> Bool -> Declaration
+minifyDec d@(Declaration p vs _ _) mv inherits =
+    case mv of 
+      -- Use the found initial values to try to reduce the declaration
+      Just vals ->
+          case Map.lookup (T.toCaseFold p) declarationExceptions of
+            -- Use a specific function to reduce the property if
+            -- needed, otherwise use the general property reducer
+            Just f  -> f d vals inherits
+            Nothing -> reduceDeclaration d vals inherits
+      -- Property with no defined initial values. Try to reduce css-wide keywords
+      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
+
+-- Map of property specific reducers, for properties that don't fit in the
+-- normal declaration minification scheme and need special treatment.
+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} -- "initial" is shorter than "weight style"
+      _  -> d {valueList = shortestEquiv vs initVals inherits}
+
+-- Function to reduce the great mayority of properties. Requires that:
+-- 1. The order between values doesn't matter, which is true for most
+--    properties because they take only one value.
+-- 2. Any default value may be removed, which tends to hold for shorthands
+--    because the default value is used when it isn't present. An example of a
+--    property that qualifies is border-bottom, and one that doesn't is
+--    font-synthesis (because it isn't a shorthand).
+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)]
+
+-- If the value was a css-wide keyword, return the shortest between css-wide
+-- keywords and the shortest initial value for the property.
+-- Otherwise, no property specific reduction could be done, so just return the
+-- values.
+shortestEquiv :: Values -> Values -> Bool -> Values
+shortestEquiv vs siv inherits
+    | inherits     && vs == inherit                = unset
+    | not inherits && vs == unset || vs == initial = minVal inherits siv
+    | otherwise = vs
+
+-- Returns the minimum value (in character length) between the shortest initial
+-- value, and the shortest css-wide keyword (initial or unset)
+minVal :: Bool -> Values -> Values
+minVal inherits vs
+    | textualLength globalKeyword <= textualLength vs = globalKeyword
+    | otherwise                                       = vs
+  where globalKeyword = mkValues [if not inherits then Unset else Initial]
+    
+-- Substract declaration values to the property's initial value list.
+-- If nothing remains (i.e. every value declared was an initial one and may be
+-- left implicit), then just replace it with whatever initial value is the
+-- shortest, otherwise whatever remains is the shortest equivalent declaration.
+analyzeValueDifference :: Values -> Values -> Maybe Values
+analyzeValueDifference vs initVals = 
+    case valuesDifference of
+      [] -> Nothing -- every value was an initial one
+      _  -> Just $ mkValues valuesDifference -- At least a value wasn't an initial one, or it was a css-wide keyword
+  where valuesDifference = valuesToList vs \\ valuesToList initVals
+
+-- Removes longhand rules overwritten by their shorthand further down in the
+-- declaration list, and merges shorthand declarations with longhand properties
+-- later declared.
+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 -- drop and keep cleaning
+  where pinfo = fromMaybe (PropertyInfo mempty mempty) -- No info on the property, use an empty one.
+                  (Map.lookup (propertyName d) shorthandAndLonghandsMap)
+
+-- Given a declaration, if it is a shorthand and it has a corresponding
+-- longhand later in the list, merges them. If it is a longhand overwritten by
+-- a shorthand, it deletes it. Otherwise, it keeps the value. In any case, it
+-- returns the new list to analyze and (if any) the value to keep.
+solveClashes :: [Declaration] -> Declaration 
+             -> PropertyInfo -> (Maybe Declaration, [Declaration])
+solveClashes ds = solveClashes' ds ds
+
+-- Local function, only to be called by solveClashes
+solveClashes' :: [Declaration] -> [Declaration] -> Declaration
+              -> PropertyInfo -> (Maybe Declaration, [Declaration])
+solveClashes' newDs []            dec _ = (Just dec, newDs)
+solveClashes' newDs (laterDec:ds) dec pinfo
+    -- Do not remove vendor-prefixed values, which are probably fallbacks.
+    | 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
+           -- important LH with a non-important SH later, or only one of the
+           -- two has the \9 ie hack; keep analyzing
+           then solveClashes' newDs ds dec pinfo
+           -- longhand overwritten by a shorthand; drop it
+           else (Nothing, newDs)
+    | propertyName dec == propertyName laterDec = {- && dec can be dropped (consider background-image!) -}
+        if isImportant dec && (not . isImportant) laterDec
+           -- drop non-important one; keep analyzing
+           then solveClashes' (delete laterDec newDs) ds dec pinfo
+           -- same property twice, drop the overwritten one
+           else (Nothing, newDs)
+    | otherwise = solveClashes' newDs ds dec pinfo -- keep analyzing the rest
+
+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
+      -- Put the merged result back on the list; see if any other
+      -- property also conflicts
+      Just m -> (Nothing, m : delete dec (delete laterDec newDs))
+      -- Couldn't merge, just keep analyzing
+      Nothing -> solveClashes' newDs ds dec pinfo
+
+-- TODO: make it such that the order of parameters doesn't matter
+-- First declaration = shorthand
+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)
+                                       --,("animation", 
+                                       --,("background",
+                                       --,("background-position", 
+                                       --,("border",
+                                       --,("border-bottom", 
+                                       --,("border-image",
+                                       --,("border-left", 
+                                       --,("border-radius"
+                                       --,("border-right",
+                                       --,("border-top"
+                                       --,("column-rule",
+                                       --,("columns",
+                                       --,("flex",  
+                                       --,("flex-flow",
+                                       --,("font",
+                                       --,("grid",
+                                       --,("grid-area"
+                                       --,("grid-column"
+                                       --,("grid-gap",
+                                       --,("grid-row",
+                                       --,("grid-template",
+                                       --,("list-style",
+                                       --,("mask", 
+                                       --,("outline",
+                                       --,("padding",
+                                       --,("text-decoration", 
+                                       --,("text-emphasis"
+                                       --,("transition",
+                                       ]
+
+
+-- TODO: consider the check in retDec. We aren't taking the minified length,
+-- only the one as it is. Ideally, it would take the length of the minified
+-- result, according to the program arguments.
+mergeIntoTRBL :: Declaration       -- ^ A margin declaration
+              -> Declaration       -- ^ Any of the margin longhands (e.g.: margin-top)
+              -> Maybe Declaration -- ^ If successful, the combination of both
+mergeIntoTRBL d1@(Declaration _ (Values v1 vs) i1 h1) d2@(Declaration p2 (Values v2 _) i2 h2)
+    | h1 || h2     = Nothing -- TODO handle ie hacks
+    | i1 && not i2 = Just $ reduceTRBL d1 -- margin:6px !important;margin-top:0; --> margin:6px !important
+    | not i1 && i2 = Nothing -- margin:6px;margin-top:0!important; --> stays the same
+    | 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 -- E.g.: an iehack read as a value.
+  where originalLength = textualLength d1 + textualLength d2 + 1 -- The (+1) is because of the ;
+        trblValues     = v1 : map snd vs
+        indexTable     = fmap (first T.toCaseFold) [("top",    0)
+                                                   ,("right",  1)
+                                                   ,("bottom", 2)
+                                                   ,("left",   3)]
+
+-- E.g.: margin: 6px 6px 6px 6px;  --> margin: 6px;
+--       margin: 1px 0 2px 0;      --> margin: 1px 0 2px;
+-- can be used with "border-image-outset" too.
+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
diff --git a/src/Hasmin/Types/Dimension.hs b/src/Hasmin/Types/Dimension.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Dimension.hs
@@ -0,0 +1,362 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Dimension
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-- CSS Dimension data types: \<length\> (distance), \<angle\>, \<duration\>,
+-- \<frequency\>, and \<resolution\>. Provides conversion of absolute
+-- dimensions into other equivalent dimensions.
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Dimension (
+      Distance(..)
+    , DistanceUnit(..)
+    , Angle(..)
+    , AngleUnit(..)
+    , Duration(..)
+    , DurationUnit(..)
+    , Frequency(..)
+    , FrequencyUnit(..)
+    , Resolution(..)
+    , ResolutionUnit(..)
+    , toInches
+    , toPixels
+    , toRadians
+    , isRelative
+    ) where
+
+import Control.Monad.Reader (asks)
+import Data.Monoid ((<>))
+import Data.Text.Lazy.Builder (singleton, fromText)
+import Data.Typeable (Typeable)
+import Hasmin.Types.Class
+import Hasmin.Types.Numeric
+import Hasmin.Config
+import Hasmin.Utils
+import Text.PrettyPrint.Mainland (char, Pretty, ppr, strictText)
+
+-- | The \<length\> CSS data type
+data Distance = Distance Number DistanceUnit
+  deriving (Show)
+instance Pretty Distance where
+  ppr (Distance 0 _) = char '0'
+  ppr (Distance r u) = ppr r <> ppr u
+instance Eq Distance where
+  (Distance r1 u1) == (Distance r2 u2)
+    | u1 == u2  = r1 == r2
+    | otherwise = toInches r1 u1 == toInches r2 u2
+instance Minifiable Distance where
+  minifyWith d@(Distance r u) = do
+      shouldMinifyUnits <- asks ((DimMinOn ==) . dimensionSettings)
+      pure $ if (not . isRelative) u && shouldMinifyUnits
+                then minDim Distance r u [Q, CM, MM, IN, PC, PT, PX]
+                else d
+
+isRelative :: DistanceUnit -> Bool
+isRelative x = x == EM || x == EX || x == CH || x == VH
+            || x == VW || x == VMIN || x == VMAX || x == REM
+
+instance ToText Distance where
+  toBuilder (Distance 0 _) = singleton '0'
+  toBuilder (Distance r u) = (fromText . toText) r <> (fromText . toText) u
+
+-- | The \<angle\> CSS data type
+data Angle = Angle Number AngleUnit
+  deriving (Show)
+instance Pretty Angle where
+  ppr (Angle 0 _) = char '0'
+  ppr (Angle r u) = ppr r <> ppr u
+instance Eq Angle where
+  (Angle r1 u1) == (Angle r2 u2)
+    | u1 == u2  = r1 == r2
+    | otherwise = toDegrees r1 u1 == toDegrees r2 u2
+instance Minifiable Angle where
+  minifyWith a = do
+      shouldMinifyUnits <- asks ((DimMinOn ==) . dimensionSettings)
+      pure $ if shouldMinifyUnits
+                then minifyAngle a
+                else a
+
+minifyAngle :: Angle -> Angle
+minifyAngle (Angle r u) = minDim Angle r u [Turn, Grad, Rad, Deg]
+
+instance ToText Angle where
+  toBuilder (Angle r u)
+      | abs r < toNumber eps = singleton '0'
+      | otherwise            = toBuilder r <> toBuilder u
+
+-- | The \<duration\> CSS data type
+data Duration = Duration Number DurationUnit
+  deriving (Show)
+instance Pretty Duration where
+  ppr (Duration r u) = ppr r <> ppr u
+instance Eq Duration where
+  (Duration r1 u1) == (Duration r2 u2)
+    | u1 == u2  = r1 == r2
+    | otherwise = toSeconds r1 u1 == toSeconds r2 u2
+instance Minifiable Duration where
+  minifyWith d@(Duration r u) = do
+      shouldMinifyUnits <- asks ((DimMinOn ==) . dimensionSettings)
+      pure $ if shouldMinifyUnits
+                then minDim Duration r u [S, Ms]
+                else d
+instance ToText Duration where
+  toBuilder (Duration r u) = toBuilder r <> toBuilder u
+
+-- | The \<frequency\> CSS data type
+data Frequency = Frequency Number FrequencyUnit
+  deriving (Show)
+instance Pretty Frequency where
+  ppr (Frequency r u) = ppr r <> ppr u
+instance Eq Frequency where
+  (Frequency r1 u1) == (Frequency r2 u2)
+    | u1 == u2  = r1 == r2
+    | otherwise = toHertz r1 u1 == toHertz r2 u2
+instance Minifiable Frequency where
+  minifyWith f@(Frequency r u) = do
+      dimSettings <- asks dimensionSettings
+      pure $ case dimSettings of
+                DimMinOn  -> minDim Frequency r u [Khz, Hz]
+                DimMinOff -> f
+instance ToText Frequency where
+  toBuilder (Frequency r u) = toBuilder r <> toBuilder u
+
+-- | The \<resolution\> CSS data type
+data Resolution = Resolution Number ResolutionUnit
+  deriving (Show)
+instance Pretty Resolution where
+  ppr (Resolution r u) = ppr r <> ppr u
+instance Eq Resolution where
+  (Resolution r1 u1) == (Resolution r2 u2)
+    | u1 == u2  = r1 == r2
+    | otherwise = toDpi r1 u1 == toDpi r2 u2
+instance Minifiable Resolution where
+  minifyWith x@(Resolution r u) = do
+      shouldMinifyUnits <- asks ((DimMinOn ==) . dimensionSettings)
+      pure $ if shouldMinifyUnits
+                then minDim Resolution r u [Dpcm, Dppx, Dpi]
+                else x
+instance ToText Resolution where
+  toBuilder (Resolution r u) = toBuilder r <> toBuilder u
+
+-- | Given a constructor, a number, and a unit, returns
+-- the shortest equivalent representation. If there is more than one, returns
+-- the latest found to "normalize" values, hopefully improving gzip compression.
+minDim :: (Unit a, ToText a) => (Number -> a -> b) -> Number -> a -> [a] -> b
+minDim constructor r u (x:xs)
+    | currentLength < newLength = minDim constructor r u xs
+    | otherwise                 = minDim constructor equivValue x xs
+  where equivValue    = convertTo x r u
+        currentLength = textualLength r + textualLength u
+        newLength     = textualLength equivValue + textualLength x
+minDim constructor r u [] = constructor r u
+
+class Unit a where
+  convertTo :: a -> Number -> a -> Number
+
+data DistanceUnit = IN | CM | MM | Q | PC | PT | PX            -- absolute
+                  | EM | EX | CH | VH | VW | VMIN | VMAX | REM -- relative
+  deriving (Show, Eq)
+instance ToText DistanceUnit where
+  toBuilder IN   = "in"
+  toBuilder CM   = "cm"
+  toBuilder MM   = "mm"
+  toBuilder Q    = "q"
+  toBuilder PC   = "pc"
+  toBuilder PT   = "pt"
+  toBuilder PX   = "px"
+  toBuilder EM   = "em"
+  toBuilder EX   = "ex"
+  toBuilder CH   = "ch"
+  toBuilder VH   = "vh"
+  toBuilder VW   = "vw"
+  toBuilder VMIN = "vmin"
+  toBuilder VMAX = "vmax"
+  toBuilder REM  = "rem"
+instance Pretty DistanceUnit where
+  ppr = strictText . toText
+instance  Unit DistanceUnit where
+  convertTo IN = toInches
+  convertTo CM = toCentimeters
+  convertTo MM = toMilimeters
+  convertTo Q  = toQuarterMilimeter
+  convertTo PT = toPoints
+  convertTo PC = toPica
+  convertTo PX = toPixels
+  convertTo _  = const
+
+data AngleUnit = Deg | Grad | Rad | Turn
+  deriving (Show, Eq, Typeable)
+instance ToText AngleUnit where
+  toBuilder Deg  = "deg"
+  toBuilder Grad = "grad"
+  toBuilder Rad  = "rad"
+  toBuilder Turn = "turn"
+instance Pretty AngleUnit where
+  ppr = strictText . toText
+instance Unit AngleUnit where
+  convertTo Deg  = toDegrees
+  convertTo Grad = toGradians
+  convertTo Rad  = toRadians
+  convertTo Turn = toTurns
+
+data DurationUnit = S -- seconds
+                  | Ms -- miliseconds
+  deriving (Show, Eq)
+instance ToText DurationUnit where
+  toBuilder S  = "s"
+  toBuilder Ms = "ms"
+instance Pretty DurationUnit where
+  ppr = strictText . toText
+instance Unit DurationUnit where
+  convertTo S  = toSeconds
+  convertTo Ms = toMiliseconds
+
+data FrequencyUnit = Hz | Khz
+  deriving (Show, Eq)
+instance ToText FrequencyUnit where
+  toBuilder Hz  = "hz"
+  toBuilder Khz = "khz"
+instance Pretty FrequencyUnit where
+  ppr = strictText . toText
+instance Unit FrequencyUnit where
+  convertTo Hz  = toHertz
+  convertTo Khz = toKilohertz
+
+data ResolutionUnit = Dpi | Dpcm | Dppx
+  deriving (Show, Eq)
+instance ToText ResolutionUnit where
+  toBuilder Dpi  = "dpi"
+  toBuilder Dpcm = "dpcm"
+  toBuilder Dppx = "dppx"
+instance Pretty ResolutionUnit where
+  ppr = strictText . toText
+instance Unit ResolutionUnit where
+  convertTo Dpi  = toDpi
+  convertTo Dpcm = toDpcm
+  convertTo Dppx = toDppx
+
+toInches :: Number -> DistanceUnit -> Number
+toInches d CM = d / 2.54
+toInches d MM = d / 25.4
+toInches d Q  = d / 101.6
+toInches d PT = d / 72
+toInches d PC = d / 6
+toInches d PX = d / 96
+toInches d _  = d -- IN, or any relative value
+
+toCentimeters :: Number -> DistanceUnit -> Number
+toCentimeters d IN = d * 2.54
+toCentimeters d MM = d / 10
+toCentimeters d Q  = d / 40
+toCentimeters d PT = d * (2.54 / 72)
+toCentimeters d PC = d * (2.54 / 6)
+toCentimeters d PX = d * (2.54 / 96)
+toCentimeters d _  = d -- CM, or any relative value
+
+toMilimeters :: Number -> DistanceUnit -> Number
+toMilimeters d IN = d * 25.4
+toMilimeters d CM = d * 10
+toMilimeters d Q  = d / 4
+toMilimeters d PT = d * (25.4 / 72)
+toMilimeters d PC = d * (25.4 / 6)
+toMilimeters d PX = d * (25.4 / 96)
+toMilimeters d _  = d
+
+toQuarterMilimeter :: Number -> DistanceUnit -> Number
+toQuarterMilimeter d IN = d * 101.6
+toQuarterMilimeter d CM = d * 40
+toQuarterMilimeter d MM = d * 4
+toQuarterMilimeter d PT = d * (101.6 / 72)
+toQuarterMilimeter d PC = d * (101.6 / 6)
+toQuarterMilimeter d PX = d * (101.6 / 96)
+toQuarterMilimeter d _  = d
+
+toPoints :: Number -> DistanceUnit -> Number
+toPoints d IN = d / 72
+toPoints d CM = d * (72 / 2.54)
+toPoints d MM = d * (72 / 25.4)
+toPoints d Q  = d * (72 / 101.6)
+toPoints d PC = d * 12
+toPoints d PX = d * (3 / 4)
+toPoints d _  = d
+
+toPica :: Number -> DistanceUnit -> Number
+toPica d IN = d / 6
+toPica d CM = d * (6 / 2.54)
+toPica d MM = d * (6 / 25.4)
+toPica d Q  = d * (6 / 101.6)
+toPica d PT = d / 12
+toPica d PX = d / 16
+toPica d _  = d
+
+toPixels :: Number -> DistanceUnit -> Number
+toPixels d IN = d * 96
+toPixels d CM = d * (96 / 2.54)
+toPixels d MM = d * (96 / 25.4)
+toPixels d Q  = d * (96 / 101.6)
+toPixels d PT = d * (4 / 3)
+toPixels d PC = d / 6
+toPixels d _  = d
+------------------------------------------------------------------------------
+rationalPi :: Number
+rationalPi = Number $ toRational (pi :: Double)
+
+toDegrees :: Number -> AngleUnit -> Number
+toDegrees d Deg  = d
+toDegrees d Grad = d * (9 / 10)
+toDegrees d Rad  = d * (180 / rationalPi)
+toDegrees d Turn = d * 360
+
+toGradians :: Number -> AngleUnit -> Number
+toGradians d Deg  = d * (10 / 9)
+toGradians d Grad = d
+toGradians d Rad  = d * (200 / rationalPi)
+toGradians d Turn = d * 400
+
+toRadians :: Number -> AngleUnit -> Number
+toRadians d Deg  = d * (rationalPi / 180)
+toRadians d Grad = d * (rationalPi / 200)
+toRadians d Rad  = d
+toRadians d Turn = d * 2 * rationalPi
+
+toTurns :: Number -> AngleUnit -> Number
+toTurns d Deg  = d / 360
+toTurns d Grad = d / 400
+toTurns d Rad  = d / (2 * rationalPi)
+toTurns d Turn = d
+------------------------------------------------------------------------------
+toSeconds :: Number -> DurationUnit -> Number
+toSeconds d S     = d
+toSeconds d Ms = d / 1000
+
+toMiliseconds :: Number -> DurationUnit -> Number
+toMiliseconds d S     = d * 1000
+toMiliseconds d Ms = d
+------------------------------------------------------------------------------
+toHertz :: Number -> FrequencyUnit -> Number
+toHertz d Hz  = d
+toHertz d Khz = d * 1000
+
+toKilohertz :: Number -> FrequencyUnit -> Number
+toKilohertz d Hz  = d / 1000
+toKilohertz d Khz = d
+------------------------------------------------------------------------------
+toDpi :: Number -> ResolutionUnit -> Number
+toDpi d Dpi  = d
+toDpi d Dpcm = d * 2.54
+toDpi d Dppx = d * 96
+
+toDpcm :: Number -> ResolutionUnit -> Number
+toDpcm d Dpi  = d / 2.54
+toDpcm d Dpcm = d
+toDpcm d Dppx = d * (2.54 / 96)
+
+toDppx :: Number -> ResolutionUnit -> Number
+toDppx d Dpi  = d / 96
+toDppx d Dpcm = d * (96 / 2.54)
+toDppx d Dppx = d
diff --git a/src/Hasmin/Types/FilterFunction.hs b/src/Hasmin/Types/FilterFunction.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/FilterFunction.hs
@@ -0,0 +1,103 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.FilterFunction
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.FilterFunction (
+      FilterFunction(..)
+    , minifyPseudoShadow
+    ) where
+
+import Control.Monad.Reader (Reader, ask)
+import Data.Semigroup ((<>))
+import Data.Text.Lazy.Builder (singleton, Builder)
+import Hasmin.Config
+import Hasmin.Types.Class
+import Hasmin.Types.Dimension
+import Hasmin.Types.Color
+import Hasmin.Types.Numeric
+
+-- Note: In a previous specification, translate3d() took two
+-- <https://www.w3.org/TR/css-transforms-1/#typedef-translation-value \<translation-value\>>,
+-- however in the <https://drafts.csswg.org/css-transforms/ latest draft>, it
+-- takes two \<length-percentage\> (which makes sense since translateX() and
+-- translateY() also do).
+
+data FilterFunction = Blur Distance
+                    | Brightness (Either Number Percentage)
+                    | Contrast (Either Number Percentage)
+                    | Grayscale (Either Number Percentage)
+                    | Invert (Either Number Percentage)
+                    | Opacity (Either Number Percentage)
+                    | Saturate (Either Number Percentage)
+                    | Sepia (Either Number Percentage)
+                    | HueRotate Angle
+                    | DropShadow Distance Distance (Maybe Distance) (Maybe Color)
+  deriving (Eq, Show)
+
+instance ToText FilterFunction where
+  toBuilder (Blur d)        = "blur("       <> toBuilder d  <> singleton ')'
+  toBuilder (Brightness np) = "brightness(" <> toBuilder np <> singleton ')'
+  toBuilder (HueRotate a)   = "hue-rotate(" <> toBuilder a  <> singleton ')'
+  toBuilder (Contrast np)   = "contrast("   <> toBuilder np <> singleton ')'
+  toBuilder (Grayscale np)  = "grayscale("  <> toBuilder np <> singleton ')'
+  toBuilder (Invert np)     = "invert("     <> toBuilder np <> singleton ')'
+  toBuilder (Opacity np)    = "opacity("    <> toBuilder np <> singleton ')'
+  toBuilder (Saturate np)   = "saturate("   <> toBuilder np <> singleton ')'
+  toBuilder (Sepia np)      = "sepia("      <> toBuilder np <> singleton ')'
+  toBuilder (DropShadow l1 l2 ml mc) =
+      let maybeToBuilder :: ToText a => Maybe a -> Builder
+          maybeToBuilder = maybe mempty (\x -> singleton ' ' <> toBuilder x)
+      in "drop-shadow(" <> toBuilder l1 <> singleton ' ' <> toBuilder l2
+       <> maybeToBuilder ml <> maybeToBuilder mc <> singleton ')'
+
+instance Minifiable FilterFunction where
+  minifyWith (Blur a)       = Blur <$> minifyWith a
+  minifyWith (HueRotate a)  = HueRotate <$> minifyWith a
+  minifyWith (Contrast x)   = Contrast <$> minifyNumberPercentage x
+  minifyWith (Brightness x) = Brightness <$> minifyNumberPercentage x
+  minifyWith (Grayscale x)  = Grayscale <$> minifyNumberPercentage x
+  minifyWith (Invert x)     = Invert <$> minifyNumberPercentage x
+  minifyWith (Opacity x)    = Opacity <$> minifyNumberPercentage x
+  minifyWith (Saturate x)   = Saturate <$> minifyNumberPercentage x
+  minifyWith (Sepia x)      = Sepia <$> minifyNumberPercentage x
+  minifyWith s@(DropShadow a b c d) = do
+      conf <- ask
+      if shouldMinifyFilterFunctions conf
+         then minifyPseudoShadow DropShadow a b c d
+         else pure s
+
+minifyPseudoShadow constr a b c d = do
+              x  <- minifyWith a
+              y  <- minifyWith b
+              z  <- case c of
+                      Just r -> if r == Distance 0 Q
+                                   then pure Nothing
+                                   else mapM minifyWith c
+                      Nothing -> pure Nothing
+              c2 <- mapM minifyWith d
+              pure $ constr x y z c2
+
+minifyNumberPercentage :: Either Number Percentage
+                       -> Reader Config (Either Number Percentage)
+minifyNumberPercentage x = do
+    conf <- ask
+    pure $ if shouldMinifyFilterFunctions conf
+              then either convertNumber convertPercentage x
+              else x
+
+convertNumber :: Number -> Either Number Percentage
+convertNumber x
+    | 0 < x && x < 0.1 = Right $ toPercentage (x * 100)
+    | otherwise        = Left x
+
+convertPercentage :: Percentage -> Either Number Percentage
+convertPercentage p
+    | p == 0            = Left 0
+    | 0 < p && p < 10   = Right p
+    | otherwise         = Left $ toNumber (p / 100)
diff --git a/src/Hasmin/Types/Gradient.hs b/src/Hasmin/Types/Gradient.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Gradient.hs
@@ -0,0 +1,309 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Gradient
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Gradient (
+    Gradient(..), Side(..), ColorStop(..), Size(..), Shape(..)
+    ) where
+
+import Control.Monad.Reader (Reader, ask)
+import Data.Semigroup ((<>))
+import Data.Text.Lazy.Builder (singleton)
+import Data.Maybe (catMaybes, fromJust, isNothing, isJust)
+import Hasmin.Config
+import Hasmin.Types.Class
+import Hasmin.Types.Color
+import Hasmin.Types.Dimension
+import Hasmin.Types.Numeric
+import Hasmin.Types.PercentageLength
+import Hasmin.Types.Position
+import Hasmin.Utils
+import Text.PrettyPrint.Mainland (Pretty, ppr, strictText)
+
+-- | CSS <side-or-corner> data type
+data Side = LeftSide | RightSide | TopSide | BottomSide
+  deriving (Show, Eq)
+instance ToText Side where
+  toBuilder LeftSide   = "left"
+  toBuilder RightSide  = "right"
+  toBuilder TopSide    = "top"
+  toBuilder BottomSide = "bottom"
+instance Pretty Side where
+  ppr = strictText . toText
+
+-- Possible pair of values, as expected by linear-gradient()
+type SideOrCorner = (Side, Maybe Side)
+
+-- | CSS <color-stop> data type
+data ColorStop = ColorStop { csColor :: Color 
+                           , colorHint :: Maybe PercentageLength
+                           } deriving (Show, Eq)
+instance ToText ColorStop where
+  toBuilder (ColorStop c mpl) = toBuilder c <> maybe mempty f mpl
+    where f (Left p)  = singleton ' ' <> toBuilder p
+          f (Right l) = singleton ' ' <> toBuilder l
+instance Pretty ColorStop where
+  ppr = strictText . toText
+instance Minifiable ColorStop where
+  minifyWith (ColorStop c mlp) = do 
+    newC   <- minifyWith c
+    newMlp <- (mapM . mapM) minifyWith mlp
+    pure $ ColorStop newC newMlp
+
+{-
+percentageLengthEq :: PercentageLength -> PercentageLength -> Bool
+x `percentageLengthEq` y
+    | isZero x  = isZero y
+    | otherwise = x == y
+-}
+
+-- minifies color hints in a \<color-stops\> list
+minifyColorHints :: [ColorStop] -> [ColorStop]
+minifyColorHints [c1,c2] = [newC1, newC2]
+  where ch1 = colorHint c1
+        ch2 = colorHint c2
+        newC1
+            | isJust ch1 && isZero (fromJust ch1) = c1 {colorHint = Nothing}
+            | otherwise = c1 
+        newC2
+            | ch2 == Just (Left (Percentage 100)) = c2 {colorHint = Nothing}
+            | otherwise = if ch2 `notGreaterThan` ch1
+                             then c2 {colorHint = Just $ Right (Distance 0 PX)}
+                             else c2
+minifyColorHints (c@(ColorStop a x):xs) = case x of
+                       Nothing -> c : analyzeList (Left $ Percentage 0) 1 (c:xs) xs
+                       Just y  -> if isZero y
+                                     then ColorStop a Nothing : analyzeList y 1 (c:xs) xs
+                                     else c: analyzeList y 1 (c:xs) xs
+minifyColorHints xs = error ("invalid <color-stop> list: " ++ show xs)
+
+-- Returns True if the first value is equal or less than the second one, and False
+-- otherwise, or if a comparison isn't possible.
+notGreaterThan :: Maybe PercentageLength -> Maybe PercentageLength -> Bool
+y `notGreaterThan` x
+    | isNothing x || isZero (fromJust x) = notPositive y
+    | otherwise = case fromJust x of
+                    Left p  -> maybe False (either (<= p) (const False)) y
+                    Right d -> maybe False (either (const False) (notGreaterThanDistance d)) y
+  where notPositive = maybe False (either (<= 0) (\(Distance d _) -> d <= 0))
+        notGreaterThanDistance (Distance r1 u1) (Distance r2 u2)
+            | u1 == u2 = r2 <= r1
+            | isRelative u1 || isRelative u2 = False
+            | otherwise = toInches r2 u2 <= toInches r1 u1
+
+-- Gathers at least three color stops to interpolate between the first and
+-- last, and see if the middle one can be removed. As long as the color hints
+-- are Nothing, keeps accumulating until it finds a Just it can use to interpolate.
+analyzeList :: PercentageLength -> Int -> [ColorStop]
+   -> [ColorStop] -> [ColorStop]
+analyzeList start n list (ColorStop _ mpl:xs)
+    | n < 2 = analyzeList start (n+1) list xs
+    | otherwise = 
+        case mpl of
+          Just y  -> let (newList, remainingList, startVal) = minifySegment start y n list
+                     in newList ++ analyzeList startVal 2 remainingList xs
+          Nothing -> analyzeList start (n+1) list xs
+analyzeList start n list [] =
+    case mpl of
+      Just (Left (Percentage 100)) -> [ColorStop x Nothing]
+      Nothing  -> let end = Left $ Percentage 100
+                      (newList, _, _) = minifySegment start end (n-1) list
+                  in newList ++ [(last list) {colorHint = Nothing}]
+      _        -> [c]
+  where c@(ColorStop x mpl) = last list
+
+-- Given two values and a count of values, uses them to create a list of
+-- interpolated values, and based on the list decides if it should remove a
+-- color hint or not.
+minifySegment :: PercentageLength -> PercentageLength -> Int -> [ColorStop]
+     -> ([ColorStop], [ColorStop], PercentageLength)
+minifySegment start end n list
+    | all isPercentage segment = handlePercentages (fromLeft' start) (fromLeft' end) n remainingList
+    -- add here support for dimension interpolation
+    | otherwise = (take (n-1) remainingList, remainingList, fromJust $ colorHint (head remainingList))
+  where segment = take (n+1) list
+        (_, remainingList) = splitAt (n-1) list
+        isPercentage x = maybe True isLeft (colorHint x)
+
+-- Handles the minification of color hint values between percentages
+handlePercentages :: Percentage -> Percentage -> Int
+                  -> [ColorStop] -> ([ColorStop], [ColorStop], PercentageLength)
+handlePercentages start end n remainingList =
+    let newList = zipWith simplifyValue remainingList interpolation
+    in (newList, remainingList, Left newStartVal)
+  where newStartVal = maybe (last interpolation) fromLeft' (colorHint $ head remainingList)
+        step = (end - start) / toPercentage n
+        interpolation = [start + (toPercentage x) * step | x <- [1..n-1]]
+        simplifyValue (ColorStop x mpl) y = ColorStop x $ mpl >>= \v ->
+            if fromLeft' v == y 
+               then Nothing
+               else if fromLeft' v <= start
+                       then Just $ Right (Distance 0 PX)
+                       else Just v
+
+-- OldLinearGradient is for the old syntax. Eventually it can probably be deleted.
+data Gradient = OldLinearGradient (Maybe (Either Angle SideOrCorner)) [ColorStop]
+              | LinearGradient (Maybe (Either Angle SideOrCorner)) [ColorStop]
+              | RadialGradient (Maybe Shape) (Maybe Size) (Maybe Position) [ColorStop]
+                -- TODO: replace with Maybe (These Shape Size)
+  deriving (Show)
+              -- ,| RepeatingLinearGradient
+              -- ,| RepeatingRadialGradient
+
+
+{- 
+  radial-gradient() = radial-gradient(
+    [ <ending-shape> || <size> ]? [ at <position> ]? ,
+    <color-stop-list>
+  )
+
+  radial-gradient(
+    [ [ circle || <length> ]                         [ at <position> ]? , |
+      [ ellipse || [ <length> | <percentage> ]{2} ]  [ at <position> ]? , |
+      [ [ circle | ellipse ] || <extent-keyword> ]   [ at <position> ]? , |
+      at <position> ,
+    ]?
+    <color-stop> [ , <color-stop> ]+
+  )
+  where <extent-keyword> = closest-corner | closest-side | farthest-corner | farthest-side
+    and <color-stop>     = <color> [ <percentage> | <length> ]? 
+-}
+
+data Size = ClosestCorner | ClosestSide | FarthestCorner | FarthestSide
+          | SL Distance
+          | PL PercentageLength PercentageLength
+  deriving (Eq, Show)
+
+instance ToText Size where
+  toBuilder ClosestCorner  = "closest-corner"
+  toBuilder ClosestSide    = "closest-side"
+  toBuilder FarthestCorner = "farthest-corner"
+  toBuilder FarthestSide   = "farthest-side"
+  toBuilder (SL d)         = toBuilder d
+  toBuilder (PL pl1 pl2)   = toBuilder pl1 <> singleton ' ' <> toBuilder pl2
+
+data Shape = Circle | Ellipse
+  deriving (Eq, Show)
+
+instance ToText Shape where
+  toBuilder Circle  = "circle"
+  toBuilder Ellipse = "ellipse"
+
+-- If the argument is to top, to right, to bottom, or to left, the angle of
+-- the gradient line is 0deg, 90deg, 180deg, or 270deg, respectively.
+instance Minifiable Gradient where
+  minifyWith g@(OldLinearGradient x cs) = do
+      conf <- ask
+      case gradientSettings conf of
+        GradientMinOn  -> do css  <- mapM minifyWith cs
+                             pure $ OldLinearGradient x (minifyColorHints css)
+        GradientMinOff -> pure g
+  minifyWith g@(LinearGradient x cs) = do
+      conf <- ask
+      case gradientSettings conf of
+        GradientMinOn  -> do css  <- mapM minifyWith cs
+                             newX <- minifyAngleOrSide x
+                             pure $ LinearGradient newX (minifyColorHints css)
+        GradientMinOff -> pure g
+  minifyWith g@(RadialGradient sh sz p cs) = do
+      conf <- ask
+      case gradientSettings conf of
+        GradientMinOn  -> do css  <- mapM minifyWith cs
+                             let np = minifyRadialPosition True {-shouldMinifyPosition conf-} p
+                             pure $ minShapeAndSize sh sz np (minifyColorHints css)
+        GradientMinOff -> pure g
+
+-- If a single length was used, the default shape is circle, otherwise ellipse.
+-- circle farthest-corner == circle
+-- ellipse farthest-corner == ellipse == farthest-corner
+minShapeAndSize :: Maybe Shape -> Maybe Size -> Maybe Position -> [ColorStop] -> Gradient
+minShapeAndSize (Just Circle) sz@(Just (SL _))       = RadialGradient Nothing sz
+minShapeAndSize (Just Circle) (Just FarthestCorner)  = RadialGradient (Just Circle) Nothing
+minShapeAndSize (Just Ellipse) sz@(Just (PL _ _))    = RadialGradient Nothing sz
+minShapeAndSize (Just Ellipse) (Just FarthestCorner) = RadialGradient Nothing Nothing
+minShapeAndSize (Just Ellipse) sz@(Just _)           = RadialGradient Nothing sz
+minShapeAndSize (Just Ellipse) Nothing               = RadialGradient Nothing Nothing
+minShapeAndSize Nothing (Just FarthestCorner)        = RadialGradient Nothing Nothing
+minShapeAndSize x sz                                 = RadialGradient x sz
+
+-- Minifies the position in the radial gradient based on the position
+-- minification settings. If positions should be minified, and if it is
+-- equivalent to 'center', it is removed. If positions should not be minified,
+-- it still removes it if it is equivalent to 'center', but leaves it untouched
+-- otherwise.
+minifyRadialPosition :: Bool -> Maybe Position -> Maybe Position
+minifyRadialPosition _ Nothing = Nothing
+minifyRadialPosition cond (Just p)
+    | minifiedPos == centerPos = Nothing
+    | cond                     = Just minifiedPos
+    | otherwise                = Just p
+  where centerPos = Position Nothing p50 Nothing Nothing
+        minifiedPos = minifyPosition p
+
+minifyAngleOrSide :: Maybe (Either Angle SideOrCorner)
+                  -> Reader Config (Maybe (Either Angle SideOrCorner))
+minifyAngleOrSide mas =
+    case mas of
+      Nothing -> pure Nothing 
+      Just y -> case y of
+                  Left a  -> if a == defaultGradientAngle
+                                then pure Nothing
+                                else minifyWith a >>= pure . Just . Left
+                  Right b -> if b == defaultGradientSideOrCorner
+                                then pure Nothing
+                                else pure $ Just (minifySide b)
+  where minifySide (TopSide, Nothing)    = Left (Angle 0 Deg)
+        minifySide (RightSide, Nothing)  = Left (Angle 90 Deg)
+        minifySide (BottomSide, Nothing) = Left (Angle 180 Deg)
+        minifySide (LeftSide, Nothing)   = Left (Angle 270 Deg)
+        minifySide z                     = Right z
+
+defaultGradientAngle :: Angle
+defaultGradientAngle = Angle 180 Deg
+
+defaultGradientSideOrCorner :: SideOrCorner
+defaultGradientSideOrCorner = (BottomSide, Nothing)
+
+instance ToText Gradient where
+  toBuilder (OldLinearGradient mas csl) = maybe mempty f mas
+      <> mconcatIntersperse id (singleton ',') (fmap toBuilder csl)
+    where f         = either ((<> singleton ',') . toBuilder) g
+          g (s, ms) = toBuilder s 
+                   <> maybe mempty (\x -> singleton ' ' <> toBuilder x) ms
+                   <> singleton ','
+  toBuilder (LinearGradient mas csl) = maybe mempty f mas
+      <> mconcatIntersperse id (singleton ',') (fmap toBuilder csl)
+    where f         = either ((<> singleton ',') . toBuilder) g
+          g (s, ms) = "to " <> toBuilder s 
+                   <> maybe mempty (\x -> singleton ' ' <> toBuilder x) ms
+                   <> singleton ','
+  toBuilder (RadialGradient sh sz p cs) = firstPart 
+      <> mconcatIntersperse id (singleton ',') (fmap toBuilder cs)
+    where l = catMaybes [fmap toBuilder sh, fmap toBuilder sz, fmap (\x -> "at " <> toBuilder x) p]
+          firstPart = if null l 
+                         then mempty
+                         else mconcatIntersperse id (singleton ' ') l <> singleton ','
+
+instance Eq Gradient where
+  LinearGradient x1 csl1 == LinearGradient x2 csl2 =
+      handleMaybe x1 x2 && csl1 == csl2
+    where handleMaybe Nothing Nothing      = True
+          handleMaybe (Just x) (Just y)    = handleEither x y
+          handleMaybe _ _                  = False
+          handleEither (Left a1) (Left a2) = a1 == a2
+          handleEither (Left a) (Right s)  = angleSideEq a s
+          handleEither (Right s) (Left a)  = angleSideEq a s
+          handleEither s1 s2               = s1 == s2
+
+angleSideEq :: Angle -> SideOrCorner -> Bool
+angleSideEq (Angle 0 Deg) (TopSide, Nothing)      = True
+angleSideEq (Angle 90 Deg) (RightSide, Nothing)   = True
+angleSideEq (Angle 180 Deg) (BottomSide, Nothing) = True
+angleSideEq (Angle 270 Deg) (LeftSide, Nothing)   = True
+angleSideEq _ _                                   = False
diff --git a/src/Hasmin/Types/Numeric.hs b/src/Hasmin/Types/Numeric.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Numeric.hs
@@ -0,0 +1,118 @@
+{-# LANGUAGE GeneralizedNewtypeDeriving, OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Numeric
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-- CSS Numeric data types: \<number\>, \<percentage\>, and \<alphavalue\>.
+-- All Rational newtypes to ensure dimension conversion precision.
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Numeric (
+    Percentage(..), toPercentage,
+    Number(..), toNumber, fromNumber,
+    Alphavalue(..), toAlphavalue, mkAlphavalue
+    ) where
+
+import Data.Text (pack)
+import Hasmin.Types.Class
+import Hasmin.Utils
+import Text.PrettyPrint.Mainland (strictText, text, Pretty, ppr)
+import Text.Printf (printf)
+
+-- | The \<number\> data type. Real numbers, possibly with a fractional component.
+-- When written literally, a number is either an integer, or zero or more
+-- decimal digits followed by a dot (.) followed by one or more decimal digits
+-- and optionally an exponent composed of "e" or "E" and an integer. It
+-- corresponds to the \<number-token\> production in the CSS Syntax Module
+-- [CSS3SYN]. As with integers, the first character of a number may be
+-- immediately preceded by - or + to indicate the number’s sign.
+-- Specifications:
+--
+-- 1. <https://drafts.csswg.org/css-values-3/#numbers CSS Values and Units Module Level 3 (§4.2)>
+-- 2. <https://www.w3.org/TR/CSS2/syndata.html#numbers CSS2.1 (§4.3.1)>
+-- 3. <https://www.w3.org/TR/CSS1/#units CSS1 (6 Units)>
+newtype Number = Number { getRational :: Rational }
+  deriving (Eq, Show, Ord, Num, Fractional, Real, RealFrac)
+
+instance ToText Number where
+  toText = pack . trimLeadingZeros . showRat . toRational -- check if scientific notation is shorter!
+instance Pretty Number where
+  ppr = strictText . toText
+
+toNumber :: Real a => a -> Number
+toNumber = Number . toRational 
+
+fromNumber :: Fractional a => Number -> a
+fromNumber = fromRational . toRational
+-- | The \<alphavalue\> data type. Syntactically a \<number\>. It is the
+-- uniform opacity setting to be applied across an entire object. Any values
+-- outside the range 0.0 (fully transparent) to 1.0 (fully opaque) are clamped
+-- to this range. Specification:
+--
+-- 1. <https://www.w3.org/TR/css3-color/#transparency CSS Color Module Level 3 (§3.2)
+newtype Alphavalue = Alphavalue Rational
+  deriving (Eq, Show, Ord, Real, RealFrac)
+
+instance Num Alphavalue where
+  a + b = mkAlphavalue $ toRational a + toRational b
+  a * b = mkAlphavalue $ toRational a * toRational b
+  abs = id -- numbers are never negative, so abs doesn't do anything
+  signum a | toRational a == 0 = 0
+           | otherwise         = 1
+  fromInteger = toAlphavalue
+  a - b = mkAlphavalue $ toRational a - toRational b
+
+instance ToText Alphavalue where
+  toText = pack .  trimLeadingZeros . showRat . toRational
+instance Pretty Alphavalue where
+  ppr = text . showRat . toRational
+instance Bounded Alphavalue where
+  minBound = 0
+  maxBound = 1
+instance Fractional Alphavalue where
+  fromRational = mkAlphavalue
+  (Alphavalue a) / (Alphavalue b) = mkAlphavalue (toRational a / toRational b)
+toAlphavalue :: Real a => a -> Alphavalue
+toAlphavalue = mkAlphavalue . toRational 
+
+mkAlphavalue :: Rational -> Alphavalue
+mkAlphavalue = Alphavalue . restrict 0 1
+
+-- | The \<percentage\> data type. Many CSS properties can take percentage
+-- values, often to define sizes in terms of parent objects. Percentages are
+-- formed by a \<number\> immediately followed by the percentage sign %.
+-- There is no space between the '%' and the number. Specification:
+--
+-- 1. <https://drafts.csswg.org/css-values-3/#percentages CSS Value and Units Module Level 3 (§4.3)
+-- 2. <https://www.w3.org/TR/CSS2/syndata.html#percentage-units CSS2.1 (§4.3.3)
+-- 3. <https://www.w3.org/TR/CSS1/#percentage-units CSS1 (§6.2)
+newtype Percentage = Percentage Rational
+  deriving (Eq, Show, Ord, Num, Fractional, Real, RealFrac)
+
+instance Pretty Percentage where
+  ppr = strictText . toText
+instance ToText Percentage where
+  toText = pack . (++ "%") . trimLeadingZeros . showRat . toRational
+
+toPercentage :: Real a => a -> Percentage
+toPercentage = Percentage . toRational 
+
+-- Note: printf used instead of show to avoid scientific notation
+-- | Show a Rational in decimal notation, removing leading zeros,
+-- and not displaying fractional part if the number is an integer.
+showRat :: Rational -> String
+showRat r | abs (r - fromInteger x) < eps = printf "%d" x
+          | otherwise                     = printf "%f" d
+  where x = round r        
+        d = fromRational r :: Double
+
+trimLeadingZeros :: String -> String
+trimLeadingZeros l@(x:xs) | x == '-'  = x : go xs
+                          | otherwise = go l
+  where go ('0':y:ys) = go (y:ys)
+        go z = z
+trimLeadingZeros []  = ""
diff --git a/src/Hasmin/Types/PercentageLength.hs b/src/Hasmin/Types/PercentageLength.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/PercentageLength.hs
@@ -0,0 +1,39 @@
+{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.PercentageLength
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.PercentageLength (
+    PercentageLength, isZero, isNonZeroPercentage
+    ) where
+
+import Hasmin.Types.Dimension
+import Hasmin.Types.Numeric
+import Hasmin.Types.Class
+
+-- | CSS <length-percentage> data type, i.e.: [length | percentage]
+-- Though because of the name it would be more intuitive to define:
+-- type LengthPercentage = Either Distance Percentage,
+-- they are inverted here to make use of Either's Functor instance, because it
+-- makes no sense to minify a Percentage.
+type PercentageLength = Either Percentage Distance
+
+-- TODO see if this instance can be deleted altogether.
+instance Minifiable PercentageLength where
+  minifyWith x@(Right _) = mapM minifyWith x
+  minifyWith x@(Left p) | p == 0    = pure $ Right (Distance 0 Q) -- minifies 0% to 0
+                        | otherwise = pure x
+
+isNonZeroPercentage :: PercentageLength -> Bool
+isNonZeroPercentage (Left p) = p /= 0
+isNonZeroPercentage _        = False
+
+isZero :: (Num a, Eq a) => Either a Distance -> Bool
+isZero = either (== 0) (== Distance 0 Q)
+
+
diff --git a/src/Hasmin/Types/Position.hs b/src/Hasmin/Types/Position.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Position.hs
@@ -0,0 +1,214 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Position
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Position (
+    Position(..), PosKeyword(..), minifyPosition, p50, l0
+    ) where
+
+import Data.Text (Text)
+import qualified Data.Text as T
+import Data.Text.Lazy.Builder (singleton, fromText)
+import Data.Maybe (isJust)
+import Hasmin.Types.Class
+import Hasmin.Types.Dimension
+import Hasmin.Types.PercentageLength
+import Hasmin.Utils
+
+data Position = Position { origin1 :: Maybe PosKeyword
+                         , offset1 :: Maybe PercentageLength
+                         , origin2 :: Maybe PosKeyword
+                         , offset2 :: Maybe PercentageLength
+                         } deriving (Eq, Show)
+
+data PosKeyword = PosCenter | PosLeft | PosRight | PosTop | PosBottom
+  deriving (Eq, Show)
+
+instance ToText PosKeyword where
+  toBuilder PosCenter = "center"
+  toBuilder PosTop    = "top"
+  toBuilder PosRight  = "right"
+  toBuilder PosBottom = "bottom"
+  toBuilder PosLeft   = "left"
+
+instance Minifiable Position where
+  minifyWith p = pure $ minifyPosition p
+
+instance ToText Position where
+  toBuilder (Position a b c d) = mconcatIntersperse fromText (singleton ' ') $ filter (not . T.null) [f a, f b, f c, f d]
+    where f :: ToText a => Maybe a -> Text
+          f = maybe mempty toText
+
+minifyPosition :: Position -> Position
+-- Single keyword
+minifyPosition p@(Position (Just x) Nothing Nothing Nothing) = f x
+  where mkPos1 y    = Position Nothing y Nothing Nothing
+        f PosCenter = mkPos1 p50
+        f PosRight  = mkPos1 p100
+        f PosLeft   = mkPos1 l0
+        f _         = p
+-- Keyword and <lenght-percentage>
+minifyPosition p@(Position (Just x) Nothing Nothing (Just y)) =
+    minifyPos2 x y
+  where mkPos2 i j = if isZero j
+                        then Position Nothing i Nothing l0
+                        else Position Nothing i Nothing (Just j)
+        minifyPos2 PosLeft a   = mkPos2 l0 a
+        minifyPos2 PosRight a  = mkPos2 p100 a
+        minifyPos2 PosCenter a = mkPos2 p50 a
+        minifyPos2 _ _         = p
+minifyPosition p@(Position Nothing (Just x) (Just y) Nothing) =
+    minifyPos2 x y
+  where mkPos2 i j = if isZero i
+                        then Position Nothing l0 Nothing j
+                        else Position Nothing (Just i) Nothing j
+        minifyPos2 a PosTop    = mkPos2 a l0
+        minifyPos2 a PosBottom = mkPos2 a p100
+        minifyPos2 a PosCenter = mkPos2 a p50
+        minifyPos2 _ _         = p
+-- Two <lenght-percentage>
+minifyPosition p@(Position Nothing (Just x) Nothing (Just y)) = f x y
+  where f :: PercentageLength -> PercentageLength -> Position
+        f (Left 50) (Left 50)   = Position Nothing p50 Nothing Nothing
+        f (Left 50) (Left 100)  = Position (Just PosBottom) Nothing Nothing Nothing
+        f (Left 100) (Left 100) = Position Nothing p100 Nothing p100
+        f (Left 100) (Left 50)  = Position Nothing p100 Nothing Nothing
+        f a b
+          | isZero a = if isZero b
+                          then Position Nothing l0 Nothing l0
+                          else case b of
+                                 Left 50  -> Position Nothing l0 Nothing Nothing
+                                 Left 100 -> Position Nothing l0 Nothing p100 
+                                 _        -> p { offset1 = l0 }
+          | isZero b = case a of
+                         Left 50  -> Position (Just PosTop) Nothing Nothing Nothing
+                         Left 100 -> Position Nothing p100 Nothing l0
+                         _        -> p { offset2 = l0 }
+          | b == Left 50 = Position Nothing (Just a) Nothing Nothing
+          | otherwise    = p
+minifyPosition (Position (Just x) (Just y) Nothing Nothing) = 
+  uncurry (\a b -> Position a b Nothing Nothing) (minAxis x y)
+-- 2 keywords
+minifyPosition p@(Position (Just x) Nothing (Just y) Nothing) = f x y
+  where f :: PosKeyword -> PosKeyword -> Position
+        -- 'center', 'center center' == '50% 50%'.
+        f PosCenter PosCenter =  Position Nothing p50 Nothing Nothing
+        -- 'left', 'left center', 'center left' == '0% 50%'.
+        f PosLeft PosCenter   = Position Nothing l0 Nothing Nothing
+        f PosCenter PosLeft   = Position Nothing l0 Nothing Nothing
+        -- 'top left', 'left top' == '0% 0%'.
+        f PosTop PosLeft      = Position Nothing l0 Nothing l0
+        f PosLeft PosTop      = Position Nothing l0 Nothing l0
+        -- 'top', 'top center', 'center top' == '50% 0%'.
+        f PosTop PosCenter    = p { origin2 = Nothing }
+        f PosCenter PosTop    = p { origin1 = Just PosTop, origin2 = Nothing }
+        -- 'right top' and 'top right' == '100% 0%'.
+        f PosRight PosTop     = Position Nothing p100 Nothing l0
+        f PosTop PosRight     = Position Nothing p100 Nothing l0 
+        -- 'right', 'right center', 'center right' == '100% 50%'.
+        f PosRight PosCenter  = Position Nothing p100 Nothing Nothing
+        f PosCenter PosRight  = Position Nothing p100 Nothing Nothing
+        -- 'bottom left' and 'left bottom' == '0% 100%'.
+        f PosBottom PosLeft   = Position Nothing l0 Nothing p100
+        f PosLeft PosBottom   = Position Nothing l0 Nothing p100
+        -- 'bottom', 'bottom center', 'center bottom' == '50% 100%'.
+        f PosBottom PosCenter = p { origin2 = Nothing }
+        f PosCenter PosBottom = p { origin1 = Just PosBottom, origin2 = Nothing }
+        -- 'bottom right', 'right bottom'             == '100% 100%'.
+        f PosBottom PosRight = Position Nothing p100 Nothing p100
+        f PosRight PosBottom = Position Nothing p100 Nothing p100
+        f _ _ = p
+-- keyword pl keyword syntax
+minifyPosition p@(Position (Just x) (Just y) (Just z) Nothing)
+    | x == PosTop || x == PosBottom || z == PosLeft || z == PosRight =
+        minifyPosition $ Position (Just z) Nothing (Just x) (Just y) 
+    | otherwise = minifyPos3 x y z
+  where minifyPos3 PosLeft b PosBottom
+          | isZero b     = Position Nothing l0 Nothing p100 
+          | b == Left 50 = Position (Just PosBottom) Nothing Nothing Nothing 
+          | otherwise    = Position Nothing (Just b) Nothing p100 
+        minifyPos3 PosLeft b PosTop
+          | isZero b     = Position Nothing l0 Nothing l0 
+          | b == Left 50 = Position (Just PosTop) Nothing Nothing Nothing 
+          | otherwise    = Position Nothing (Just b) Nothing l0 
+        minifyPos3 PosLeft b PosCenter
+          | isZero b     = Position Nothing l0 Nothing Nothing 
+          | b == Left 50 = Position Nothing p50 Nothing Nothing 
+          | otherwise    = Position Nothing (Just b) Nothing p50 
+        minifyPos3 PosRight b PosTop
+          | isZero b     = Position Nothing p100 Nothing l0 
+          | otherwise    = Position (Just PosRight) (Just b) Nothing l0 
+        minifyPos3 PosRight b PosBottom
+          | isZero b     = Position Nothing p100 Nothing p100 
+          | otherwise    = Position (Just PosRight) (Just b) Nothing p100
+        minifyPos3 PosRight b PosCenter
+          | isZero b     = Position Nothing p100 Nothing Nothing 
+          | otherwise    = Position (Just PosRight) (Just b) Nothing p50 
+        minifyPos3 _ _ _ = p
+minifyPosition p@(Position (Just c) Nothing (Just a) (Just b)) = f $ minAxis a b
+  where f (x, y) 
+          | c == PosLeft && x == Just PosTop && isJust y = minifyPosition $ Position Nothing l0 Nothing y
+          | otherwise = if Just a == x && Just b == y
+                           then p
+                           else minifyPosition $ p {origin2 = x, offset2 = y }
+-- 4 value syntax
+minifyPosition p@(Position (Just PosLeft) (Just _) (Just PosTop) (Just _)) = 
+    minifyPosition p { origin1 = Nothing, origin2 = Nothing }
+minifyPosition (Position (Just a) (Just b) (Just c) (Just d)) = 
+    minifyPos4 a b c d
+minifyPosition p = p
+
+-- Sort values so that the Xs are first, then the Ys, and later minify.
+minifyPos4 :: PosKeyword -> PercentageLength -> PosKeyword -> PercentageLength -> Position
+minifyPos4 v1 v2 v3 v4
+    | v1 == PosTop || v1 == PosBottom || v3 == PosLeft || v3 == PosRight = minifyPos4' v3 v4 v1 v2
+    | otherwise = minifyPos4' v1 v2 v3 v4
+  where minifyPos4' PosLeft a PosTop b  = minifyPosition $ Position Nothing (Just a) Nothing (Just b)
+        minifyPos4' PosRight a PosTop b
+            | isZero a  = minifyPosition $ Position Nothing p100 Nothing (Just b)
+            | isZero b  = minifyPosition $ Position (Just PosRight) (Just a) (Just PosTop) Nothing
+            | otherwise = Position (Just PosRight) (Just a) (Just PosTop) (Just b)
+        minifyPos4' PosLeft a PosBottom b
+            | isZero b  = minifyPosition $ Position Nothing (Just a) Nothing p100
+            | isZero a  = minifyPosition $ Position (Just PosLeft) Nothing (Just PosTop) (Just b)
+            | otherwise = minifyPosition $ Position (Just PosLeft) (Just a) (Just PosBottom) (Just b)
+        minifyPos4' PosRight a PosBottom b
+            | isZero a && isZero b = Position Nothing p100 Nothing p100
+            | otherwise            = Position (Just PosRight) (Just a) (Just PosBottom) (Just b)
+        minifyPos4' a b c d = Position (Just a) (Just b) (Just c) (Just d)
+
+minAxis :: PosKeyword -> PercentageLength -> (Maybe PosKeyword, Maybe PercentageLength)
+minAxis PosTop x = 
+    case x of
+      Left 50 -> (Just PosCenter, Nothing)
+      b       -> if isZero b 
+                    then (Just PosTop, Nothing)
+                    else (Just PosTop, Just x)
+minAxis PosLeft x =
+    case x of
+      Left 50 -> (Just PosCenter, Nothing)
+      smth    -> if isZero smth
+                    then (Just PosLeft, Nothing)
+                    else (Just PosLeft, Just x)
+minAxis PosRight x
+    | isZero x  = (Just PosRight, Nothing)
+    | otherwise = (Just PosRight, Just x)
+minAxis PosBottom x
+    | isZero x  = (Just PosBottom, Nothing)
+    | otherwise = (Just PosBottom, Just x)
+minAxis PosCenter x = (Just PosCenter, Just x)
+
+l0 :: Maybe PercentageLength
+l0 = Just (Right (Distance 0 Q))
+
+p100 :: Maybe PercentageLength
+p100 = Just $ Left 100
+
+p50 :: Maybe PercentageLength
+p50 = Just $ Left 50
diff --git a/src/Hasmin/Types/RepeatStyle.hs b/src/Hasmin/Types/RepeatStyle.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/RepeatStyle.hs
@@ -0,0 +1,76 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.RepeatStyle
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-- \<repeat-style> data type used in background-repeat. Specification:
+-- <https://drafts.csswg.org/css-backgrounds-3/#the-background-repeat CSS Backgrounds and Borders Module Level 3 (§3.4)>
+-- 
+-----------------------------------------------------------------------------
+module Hasmin.Types.RepeatStyle (
+    RepeatStyle(..), RSKeyword(..)
+    ) where
+
+import Control.Monad.Reader (ask)
+import Data.Monoid ((<>))
+import Data.Text.Lazy.Builder (singleton)
+import Hasmin.Types.Class
+
+data RepeatStyle = RepeatX 
+                 | RepeatY
+                 | RSPair RSKeyword (Maybe RSKeyword)
+  deriving (Show)
+
+data RSKeyword = RsRepeat | RsSpace | RsRound | RsNoRepeat
+  deriving (Eq, Show)
+
+instance Eq RepeatStyle where
+  RepeatX == RepeatX = True
+  a@RepeatX == b@RSPair{} = b == a
+  RepeatY == RepeatY = True
+  a@RepeatY == b@RSPair{} = b == a
+  RSPair RsNoRepeat (Just RsRepeat) == RepeatY = True
+  RSPair RsRepeat (Just RsNoRepeat) == RepeatX = True
+  RSPair RsNoRepeat (Just RsRepeat) == RSPair RsNoRepeat (Just RsRepeat) = True
+  RSPair RsRepeat (Just RsNoRepeat) == RSPair RsRepeat (Just RsNoRepeat) = True
+  RSPair RsSpace (Just RsSpace) == RSPair RsSpace Nothing = True
+  RSPair RsSpace (Just RsSpace) == RSPair RsSpace (Just RsSpace) = True
+  RSPair RsRound (Just RsRound) == RSPair RsRound Nothing = True
+  RSPair RsRound (Just RsRound) == RSPair RsRound (Just RsRound) = True
+  RSPair RsNoRepeat (Just RsNoRepeat) == RSPair RsNoRepeat Nothing = True
+  RSPair RsNoRepeat (Just RsNoRepeat) == RSPair RsNoRepeat (Just RsNoRepeat) = True
+  RSPair RsRepeat (Just RsRepeat) == RSPair RsRepeat Nothing = True
+  RSPair RsRepeat (Just RsRepeat) == RSPair RsRepeat (Just RsRepeat) = True
+  RSPair x Nothing == RSPair y Nothing = x == y
+  a@(RSPair _ Nothing) == b@(RSPair _ _) = b == a
+  _ == _ = False
+
+instance ToText RSKeyword where
+  toBuilder RsRepeat   = "repeat"
+  toBuilder RsSpace    = "space"
+  toBuilder RsRound    = "round"
+  toBuilder RsNoRepeat = "no-repeat"
+
+instance ToText RepeatStyle where
+  toBuilder RepeatX = "repeat-x"
+  toBuilder RepeatY = "repeat-y"
+  toBuilder (RSPair r1 r2 ) = toBuilder r1 <> maybe mempty (\x -> singleton ' ' <> toBuilder x) r2
+
+instance Minifiable RepeatStyle where
+  minifyWith r = do
+    conf <- ask
+    pure $ if True {- shouldMinifyRepeatStyle conf -}
+              then minifyRepeatStyle r
+              else r
+
+minifyRepeatStyle :: RepeatStyle -> RepeatStyle
+minifyRepeatStyle (RSPair RsRepeat (Just RsNoRepeat)) = RepeatX
+minifyRepeatStyle (RSPair RsNoRepeat (Just RsRepeat)) = RepeatY
+minifyRepeatStyle (RSPair x (Just y)) = if x == y
+                                           then RSPair x Nothing
+                                           else RSPair x (Just y)
+minifyRepeatStyle x = x
diff --git a/src/Hasmin/Types/Shadow.hs b/src/Hasmin/Types/Shadow.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Shadow.hs
@@ -0,0 +1,59 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Shadow
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Shadow (
+    Shadow(..)
+    ) where
+
+import Control.Monad.Reader (ask)
+import Data.Semigroup ((<>))
+import Data.Text.Lazy.Builder (singleton)
+import Data.Bool (bool)
+import Hasmin.Types.Class
+import Hasmin.Types.Color
+import Hasmin.Types.Dimension
+
+data Shadow = Shadow { inset :: Bool
+                     , sOffsetX :: Distance
+                     , sOffsetY :: Distance
+                     , blurRadius :: Maybe Distance
+                     , spreadRadius :: Maybe Distance
+                     , sColor :: Maybe Color
+                     } deriving (Eq, Show)
+
+instance ToText Shadow where
+  toBuilder (Shadow i ox oy br sr c) =
+      bool mempty "inset " i
+      <> toBuilder ox <> singleton ' '
+      <> toBuilder oy <> f br <> f sr <> f c
+    where f c = maybe mempty (\y -> singleton ' ' <> toBuilder y) c -- don't eta reduce this!
+
+instance Minifiable Shadow where
+  minifyWith (Shadow i ox oy br sr c) = do
+      conf <- ask
+      x  <- minifyWith ox
+      y  <- minifyWith oy
+      nb <- mapM minifyWith br
+      ns <- mapM minifyWith sr
+      c2 <- mapM minifyWith c
+      pure $ if True {- shouldMinifyShadows conf -}
+                then let (a, b) = minifyBlurAndSpread nb ns
+                     in Shadow i x y a b c2
+                else Shadow i x y nb ns c2
+
+minifyBlurAndSpread :: Maybe Distance -> Maybe Distance
+             -> (Maybe Distance, Maybe Distance)
+minifyBlurAndSpread (Just br) Nothing
+    | br == Distance 0 Q = (Nothing, Nothing)
+    | otherwise          = (Just br, Nothing)
+minifyBlurAndSpread (Just br) (Just sr)
+    | sr == Distance 0 Q = minifyBlurAndSpread (Just br) Nothing
+    | otherwise          = (Just br, Just sr)
+minifyBlurAndSpread x y = (x, y)
diff --git a/src/Hasmin/Types/String.hs b/src/Hasmin/Types/String.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/String.hs
@@ -0,0 +1,117 @@
+{-# LANGUAGE OverloadedStrings, FlexibleInstances #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.String
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.String 
+  ( removeQuotes, unquoteUrl, unquoteFontFamily, mapString
+  , StringType(..)
+  ) where
+
+import Control.Applicative (liftA2)
+import Control.Monad.Reader (ask, Reader)
+import Data.Attoparsec.Text (Parser, parse, IResult(..), maybeResult, feed)
+import Data.Monoid ((<>))
+import Data.Text (Text)
+import Data.Text.Lazy.Builder as LB
+import qualified Data.Attoparsec.Text as A
+import qualified Data.Char as C
+import qualified Data.Text as T
+import qualified Data.Text.Lazy as TL
+import Hasmin.Parser.Utils
+import Hasmin.Types.Class
+import Hasmin.Config
+import Text.PrettyPrint.Mainland (Pretty, ppr, strictText)
+
+-- | The \<string\> data type represents a string, formed by Unicode
+-- characters, delimited by either double (") or single (') quotes.
+-- Specification:
+--
+-- 1. <https://drafts.csswg.org/css-values-3/#strings CSS Values and Units Module Level 3 (§3.3)
+-- 2. <https://www.w3.org/TR/CSS2/syndata.html#strings CSS2.1 (§4.3.7)
+data StringType = DoubleQuotes Text
+                | SingleQuotes Text
+  deriving (Show, Eq)
+instance ToText StringType where
+  toBuilder (DoubleQuotes t) = singleton '\"' <> fromText t <> singleton '\"'
+  toBuilder (SingleQuotes t) = singleton '\'' <> fromText t <> singleton '\''
+instance Pretty StringType where
+  ppr = strictText . toText
+instance Minifiable StringType where
+  minifyWith (DoubleQuotes t) = do
+    conf <- ask
+    convertedText <- convertEscapedText t
+    pure $ if T.any ('\"' ==) convertedText
+              then DoubleQuotes t
+              else if shouldNormalizeQuotes conf
+              -- TODO make it so that the best quotes are used for the file
+                      then DoubleQuotes convertedText
+                      else DoubleQuotes convertedText
+  minifyWith (SingleQuotes t) = do
+    conf <- ask
+    convertedText <- convertEscapedText t
+    pure $ if T.any ('\'' ==) convertedText
+              then SingleQuotes t
+              else if shouldNormalizeQuotes conf && T.all ('\"' /=) convertedText
+                      then DoubleQuotes convertedText
+                      else SingleQuotes convertedText
+
+convertEscapedText :: Text -> Reader Config Text
+convertEscapedText t = do
+    conf <- ask
+    pure $ if shouldConvertEscaped conf
+              then either (const t) id (A.parseOnly convertEscaped t)
+              else t
+
+mapString :: (Text -> Reader Config Text) -> StringType -> Reader Config StringType
+mapString f (DoubleQuotes t) = f t >>= pure . DoubleQuotes
+mapString f (SingleQuotes t) = f t >>= pure . SingleQuotes
+
+unquoteStringType :: (Text -> Maybe Text) -> StringType -> Either Text StringType
+unquoteStringType g x@(DoubleQuotes s) = maybe (Right x) Left (g s)
+unquoteStringType g x@(SingleQuotes s) = maybe (Right x) Left (g s)
+
+removeQuotes :: StringType -> Either Text StringType
+removeQuotes = unquoteStringType toIdent
+
+unquoteUrl :: StringType -> Either Text StringType
+unquoteUrl = unquoteStringType toUnquotedURL
+
+unquoteFontFamily :: StringType -> Either Text StringType
+unquoteFontFamily = unquoteStringType toUnquotedFontFamily
+
+unquote :: Parser Text -> Text -> Maybe Text
+unquote p s = case parse p s of
+                Done i r      -> if T.null i
+                                    then Just r
+                                    else Nothing
+                par@Partial{} -> maybeResult (feed par mempty)
+                Fail{}        -> Nothing
+
+toIdent :: Text -> Maybe Text
+toIdent = unquote ident
+
+toUnquotedURL :: Text -> Maybe Text
+toUnquotedURL = unquote nonquotedurl
+
+toUnquotedFontFamily :: Text -> Maybe Text
+toUnquotedFontFamily = unquote fontfamilyname
+
+convertEscaped :: Parser Text
+convertEscaped = (TL.toStrict . toLazyText) <$> go
+  where go = do
+            t <- fromText <$> A.takeWhile (/= '\\')
+            c <- A.peekChar
+            case c of
+              Just '\\' -> A.char '\\' *> liftA2 (mappend . mappend t . singleton) utf8 go
+              _         -> pure t
+        utf8 = C.chr <$> A.hexadecimal
+
+instance Pretty (Either Text StringType) where
+  ppr (Left i)  = strictText i
+  ppr (Right s) = ppr s
diff --git a/src/Hasmin/Types/Stylesheet.hs b/src/Hasmin/Types/Stylesheet.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Stylesheet.hs
@@ -0,0 +1,253 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Stylesheet
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Stylesheet (
+    Expression(..), MediaQuery(..), Rule(..), KeyframeSelector(..),
+    KeyframeBlock(..), isEmpty
+    ) where
+
+import Control.Monad.Reader (Reader, ask)
+import Control.Applicative (liftA2)
+import Data.Monoid ((<>))
+import Data.Text (Text)
+import Data.Text.Lazy.Builder (singleton, fromText)
+import Data.List (sortBy, (\\))
+import Data.Map.Strict (Map)
+import qualified Data.Map.Strict as Map
+import qualified Data.Set as S
+import qualified Data.Text as T
+import Text.PrettyPrint.Mainland (Pretty, ppr, strictText,
+  lbrace, rbrace, (</>), (<+>), comma, folddoc, nest, semi, stack, char)
+
+import Hasmin.Config
+import Hasmin.Selector
+import Hasmin.Types.Class
+import Hasmin.Types.Value
+import Hasmin.Types.Declaration
+import Hasmin.Types.String
+import Hasmin.Types.Numeric
+import Hasmin.Utils
+
+data MediaQuery = MediaQuery1 Text Text [Expression]  -- ^ First possibility in the grammar
+                | MediaQuery2 [Expression] -- ^ Second possibility in the grammar
+  deriving (Show, Eq)
+instance Minifiable MediaQuery where
+  minifyWith (MediaQuery1 t1 t2 es) = MediaQuery1 t1 t2 <$> mapM minifyWith es
+  minifyWith (MediaQuery2 es)       = MediaQuery2 <$> mapM minifyWith es
+
+instance ToText MediaQuery where
+  toBuilder (MediaQuery1 t1 t2 es) = notOrOnly <> fromText t2 <> expressions
+    where notOrOnly   | T.null t1 = mempty
+                      | otherwise = fromText t1 <> singleton ' '
+          expressions = foldr (\x xs -> " and " <> toBuilder x <> xs) mempty es
+  toBuilder (MediaQuery2 es) = mconcatIntersperse toBuilder " and " es
+
+data Expression = Expression Text (Maybe Value)
+                | InvalidExpression Text
+  deriving (Show, Eq)
+instance Minifiable Expression where
+  minifyWith (Expression t mv) = Expression t <$> mapM minifyWith mv
+  minifyWith x = pure x
+instance ToText Expression where
+  toBuilder (Expression t mv) =
+      singleton '(' <> fromText t <> v <> singleton ')'
+    where v = maybe mempty (\x -> singleton ':' <> toBuilder x) mv
+  toBuilder (InvalidExpression t) =
+      singleton '(' <> fromText t <> singleton ')'
+
+data KeyframeSelector = From | To | KFPercentage Percentage
+  deriving (Eq, Show)
+instance ToText KeyframeSelector where
+  toText From             = "from"
+  toText To               = "to"
+  toText (KFPercentage p) = toText p
+instance Pretty KeyframeSelector where
+  ppr = strictText . toText
+instance Minifiable KeyframeSelector where
+  minifyWith x = do
+      conf <- ask
+      pure $ if shouldMinifyKeyframeSelectors conf
+                then minifyKFS x
+                else x
+
+minifyKFS :: KeyframeSelector -> KeyframeSelector
+minifyKFS From                            = KFPercentage $ Percentage 0
+minifyKFS (KFPercentage (Percentage 100)) = To
+minifyKFS x = x
+
+data KeyframeBlock = KeyframeBlock [KeyframeSelector] [Declaration]
+  deriving (Eq, Show)
+instance ToText KeyframeBlock where
+  toBuilder (KeyframeBlock ss ds) =
+      mconcatIntersperse toBuilder (singleton ',') ss
+      <> singleton '{'
+      <> mconcatIntersperse toBuilder (singleton ';') ds
+      <> singleton '}'
+instance Minifiable KeyframeBlock where
+  minifyWith (KeyframeBlock ss ds) = do
+      decs <- mapM minifyWith ds
+      sels <- mapM minifyWith ss
+      pure $ KeyframeBlock sels decs
+
+type VendorPrefix = Text
+
+data Rule = AtCharset StringType
+          | AtImport (Either StringType Url) [MediaQuery]
+          | AtNamespace Text (Either StringType Url)
+          | AtMedia [MediaQuery] [Rule]
+          | AtKeyframes VendorPrefix Text [KeyframeBlock]
+          | AtBlockWithRules Text [Rule]
+          | AtBlockWithDec Text [Declaration]
+          | StyleRule [Selector] [Declaration]
+ deriving (Show)
+instance Pretty Rule where
+  ppr (StyleRule ss ds) = folddoc (\x y -> x <> comma <+> y) (fmap ppr ss)
+                     <+> nest 4 (lbrace </> stack (fmap ((<> semi) . ppr) ds))
+                     </> rbrace
+  ppr (AtBlockWithRules t rs) = char '@' <> strictText t
+      <+> nest 4 (lbrace </> stack (fmap ppr rs)) </> rbrace
+  ppr (AtBlockWithDec t ds) = char '@' <> strictText t
+      <+> nest 4 (lbrace </> stack (fmap ppr ds)) </> rbrace
+  ppr _ = error "not implemented (TODO)"
+
+instance ToText Rule where
+  toBuilder (AtMedia mqs rs) = "@media " <> mconcatIntersperse toBuilder (singleton ',') mqs
+      <> singleton '{' <> mconcat (fmap toBuilder rs) <> singleton '}'
+  toBuilder (AtImport esu mqs) = "@import " <> toBuilder esu <> mediaqueries
+      <> singleton ';'
+    where mediaqueries =
+            case mqs of
+              [] -> mempty
+              _  -> singleton ' ' <> mconcatIntersperse toBuilder (singleton ',') mqs
+  toBuilder (AtCharset s) = "@charset " <> toBuilder s <> singleton ';'
+  toBuilder (AtNamespace t esu) = "@namespace "
+      <> prefix <> toBuilder esu <> singleton ';'
+    where prefix = if T.null t
+                      then mempty
+                      else toBuilder t <> singleton ' '
+  toBuilder (StyleRule ss ds) =
+    mconcat [mconcatIntersperse toBuilder (singleton ',') ss
+            ,singleton '{'
+            ,mconcatIntersperse toBuilder (singleton ';') ds
+            ,singleton '}']
+  toBuilder (AtBlockWithRules t rs) =
+    mconcat [singleton '@', fromText t, singleton '{'
+            , mconcat (fmap toBuilder rs), singleton '}']
+  toBuilder (AtBlockWithDec t ds)   =
+    mconcat [singleton '@', fromText t, singleton '{'
+            ,mconcatIntersperse id (singleton ';') (fmap toBuilder ds)
+            ,singleton '}']
+  toBuilder (AtKeyframes vp n bs) = singleton '@' <> fromText vp <> "keyframes"
+            <> singleton ' ' <> fromText n <> singleton '{'
+            <> mconcat (fmap toBuilder bs) <> singleton '}'
+
+instance Minifiable Rule where
+  minifyWith (AtMedia mqs rs) = liftA2 AtMedia (mapM minifyWith mqs) (mapM minifyWith rs)
+  minifyWith (AtKeyframes vp n bs) = AtKeyframes vp n <$> mapM minifyWith bs
+  minifyWith (AtBlockWithRules t rs) = AtBlockWithRules t <$> mapM minifyWith rs
+  minifyWith (AtBlockWithDec t ds) = do
+      decs <- cleanRule ds >>= compactLonghands >>= mapM minifyWith
+      pure $ AtBlockWithDec t decs
+  minifyWith (StyleRule ss ds) = do
+      decs <- cleanRule ds >>= compactLonghands >>= mapM minifyWith >>= sortDeclarations
+      sels <- mapM minifyWith ss >>= removeDuplicateSelectors >>= sortSelectors
+      pure $ StyleRule sels decs
+  minifyWith (AtImport esu mqs) = AtImport esu <$> mapM minifyWith mqs
+  minifyWith (AtCharset s) = AtCharset <$> mapString lowercaseText s
+  minifyWith x = pure x
+
+cleanRule :: [Declaration] -> Reader Config [Declaration]
+cleanRule ds = do
+    conf <- ask
+    pure $ if shouldCleanRules conf
+              then clean ds
+              else ds
+
+sortSelectors :: [Selector] -> Reader Config [Selector]
+sortSelectors sls = do
+    conf <- ask
+    pure $ case selectorSorting conf of
+                   Lexicographical -> sortBy lexico sls
+                   NoSorting       -> sls
+sortDeclarations :: [Declaration] -> Reader Config [Declaration]
+sortDeclarations ds = do
+    conf <- ask
+    pure $ case declarationSorting conf of
+             Lexicographical -> sortBy lexico ds
+             NoSorting       -> ds
+
+removeDuplicateSelectors :: [Selector] -> Reader Config [Selector]
+removeDuplicateSelectors sls = do
+    conf <- ask
+    pure $ if shouldRemoveDuplicateSelectors conf
+              then nub' sls
+              else sls
+
+gatherLonghands :: [Declaration] -> Map (Text, Bool) Declaration
+gatherLonghands = go Map.empty
+  where go m [] = m
+        go m (d@(Declaration p _ i _):ds)
+            | S.member p longhands = go (Map.insert (p,i) d m) ds
+            | otherwise            = go m ds
+        longhands = S.fromList (marginLonghands ++ paddingLonghands)
+
+-- TODO delete this.
+marginLonghands = ["margin-top", "margin-right", "margin-bottom", "margin-left"]
+paddingLonghands = ["padding-top", "padding-right", "padding-bottom", "padding-left"]
+
+compactTRBL :: Text -> [Text] -> Map (Text, Bool) Declaration
+            -> (Maybe Declaration, [Declaration])
+compactTRBL name lhs m =
+    case sequenceA (map getDeclaration lhs) of
+      Just l -> (Just (Declaration name (shValues l) False False), l)
+      Nothing -> (Nothing, [])
+  where getDeclaration x = Map.lookup (x, False) m
+        shValues = mkValues . map (head . valuesToList . valueList)
+
+compactMargin  = compactTRBL "margin" marginLonghands
+compactPadding = compactTRBL "padding" paddingLonghands
+-- ,("border-color", mergeIntoTRBL)
+-- ,("border-width", mergeIntoTRBL)
+-- ,("border-style", mergeIntoTRBL)
+
+compacter m ds = compacter' [compactMargin, compactPadding]
+  where compacter' [] = ds
+        compacter' (f:fs) = case f m of
+                              (Just sh, l) -> (compacter' fs \\ l) ++ [sh]
+                              (Nothing, _) -> compacter' fs
+
+compactLonghands :: [Declaration] -> Reader Config [Declaration]
+compactLonghands ds = do
+    conf <- ask
+    pure $ if True {- shouldGatherLonghands conf -}
+              then compacter (gatherLonghands ds) ds
+              else ds
+
+-- Used for sorting selectors
+-- TODO: move to the correct place, and maybe rename.
+lexico :: ToText a => a -> a -> Ordering
+lexico s1 s2 = compare (toText s1) (toText s2)
+
+isEmpty :: Rule -> Bool
+isEmpty (StyleRule _ ds)        = null ds
+isEmpty (AtMedia _ rs)          = null rs || all isEmpty rs
+isEmpty (AtKeyframes _ _ kfbs)  = null kfbs
+isEmpty (AtBlockWithDec _ ds)   = null ds
+isEmpty (AtBlockWithRules _ rs) = null rs || all isEmpty rs
+isEmpty _                       = False
+
+-- O(n log n) implementation, vs. O(n^2) which is the normal nub.
+-- Note that nub has only an Eq constraint, while this one has an Ord one.
+-- taken from: http://buffered.io/posts/a-better-nub/
+nub' :: (Ord a) => [a] -> [a]
+nub' = go S.empty
+  where go _ [] = []
+        go s (x:xs) | S.member x s = go s xs
+                    | otherwise    = x : go (S.insert x s) xs
diff --git a/src/Hasmin/Types/TimingFunction.hs b/src/Hasmin/Types/TimingFunction.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/TimingFunction.hs
@@ -0,0 +1,115 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.TimingFunction
+-- Copyright   : (c) 2016 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.TimingFunction (
+    TimingFunction(..), StepsSecondParam(..)
+    ) where
+
+import Control.Monad.Reader (ask)
+import Data.Semigroup ((<>))
+import Prelude hiding (sin, cos, acos, tan, atan)
+import Data.Maybe (isNothing, fromJust)
+import Data.Text.Lazy.Builder (singleton)
+import Hasmin.Config
+import Hasmin.Types.Class
+import Hasmin.Utils
+import Hasmin.Types.Numeric
+--import Text.PrettyPrint.Mainland (Pretty, ppr, strictText)
+
+-- | CSS \<single-transition-timing-function\> data type. Specifications:
+--
+-- 1. <https://drafts.csswg.org/css-transitions/#propdef-transition-timing-function CSS Transitions>
+-- 2. <https://drafts.csswg.org/css-animations/#animation-timing-function CSS Animations Level 1>
+-- 3. <https://developer.mozilla.org/en-US/docs/Web/CSS/timing-function Mozilla summary>
+data TimingFunction = CubicBezier Number Number Number Number
+                    | Steps Int (Maybe StepsSecondParam)
+                    | Ease | EaseIn | EaseInOut | EaseOut 
+                    | Linear | StepEnd | StepStart 
+  deriving (Show)
+
+instance Eq TimingFunction where
+  Steps i1 ms1 == Steps i2 ms2 = i1 == i2 && (ms1 == ms2 
+                                    || (isNothing ms1 && fromJust ms2 == End)
+                                    || (fromJust ms1 == End && isNothing ms2))
+  s@Steps{} == x         = maybe False (== s) (toSteps x)
+  x == s@Steps{}         = s == x
+  CubicBezier x1 x2 x3 x4 == CubicBezier y1 y2 y3 y4 = 
+      x1 == y1 && x2 == y2 && x3 == y3 && x4 == y4
+  c@CubicBezier{} == x   = maybe False (== c) (toCubicBezier x)
+  x == c@CubicBezier{}   = c == x
+  StepEnd == StepEnd     = True
+  StepStart == StepStart = True
+  Ease == Ease           = True
+  Linear == Linear       = True
+  EaseIn == EaseIn       = True
+  EaseOut == EaseOut     = True
+  EaseInOut == EaseInOut = True
+  _ == _                 = False
+  
+toCubicBezier :: TimingFunction -> Maybe TimingFunction
+toCubicBezier Ease      = Just $ CubicBezier 0.25 0.1 0.25 1
+toCubicBezier EaseIn    = Just $ CubicBezier 0.42 0   1    1
+toCubicBezier EaseInOut = Just $ CubicBezier 0.42 0   0.58 1
+toCubicBezier Linear    = Just $ CubicBezier 0    0   1    1
+toCubicBezier EaseOut   = Just $ CubicBezier 0    0   0.58 1
+toCubicBezier _         = Nothing 
+
+toSteps :: TimingFunction -> Maybe TimingFunction
+toSteps StepEnd   = Just $ Steps 1 (Just End)
+toSteps StepStart = Just $ Steps 1 (Just Start)
+toSteps _         = Nothing
+
+
+data StepsSecondParam = Start | End -- End is the default value
+  deriving (Eq, Show)
+instance ToText StepsSecondParam where
+  toBuilder Start = "start"
+  toBuilder End   = "end"
+
+instance ToText TimingFunction where
+  toBuilder (CubicBezier a b c d) = "cubic-bezier(" 
+      <> mconcatIntersperse toBuilder (singleton ',') [a,b,c,d]
+      <> singleton ')'
+  toBuilder (Steps i ms) = "steps(" <> toBuilder i <> sp <> singleton ')'
+    where sp = maybe mempty (\x -> singleton ',' <> toBuilder x) ms
+  toBuilder Ease      = "ease"
+  toBuilder Linear    = "linear"
+  toBuilder EaseIn    = "ease-in"
+  toBuilder EaseOut   = "ease-out"
+  toBuilder EaseInOut = "ease-in-out"
+  toBuilder StepStart = "step-start"
+  toBuilder StepEnd   = "step-end"
+
+instance Minifiable TimingFunction where
+  minifyWith x = do
+      conf <- ask
+      if shouldMinifyTimingFunctions conf
+         then pure $ minifyTimingFunction x
+         else pure x 
+
+minifyTimingFunction :: TimingFunction -> TimingFunction
+minifyTimingFunction x@(CubicBezier a b c d) 
+    | a == 0.25 && b == 0.1 && c == 0.25 && d == 1 = Ease
+    | a == 0.42 && b == 0 && d == 1 = if c == 1
+                                         then EaseIn
+                                         else if c == 0.58
+                                                 then EaseInOut
+                                                 else x
+    | a == 0 && b == 0 && d == 1 = if c == 1
+                                      then Linear
+                                      else if c == 0.58 
+                                              then EaseOut
+                                              else x
+    | otherwise = x
+minifyTimingFunction x@(Steps i ms)
+    | (isNothing ms || (fromJust ms == End)) && i == 1 = StepEnd
+    | i == 1 && (fromJust ms == Start) = StepStart
+    | otherwise = x
+minifyTimingFunction x = x
diff --git a/src/Hasmin/Types/TransformFunction.hs b/src/Hasmin/Types/TransformFunction.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/TransformFunction.hs
@@ -0,0 +1,580 @@
+{-# LANGUAGE OverloadedStrings #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.TransformFunction
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : non-portable
+--
+-- CSS \<transform-function> data type.
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.TransformFunction (
+    TransformFunction(..), mkMat, mkMat3d, combine
+    ) where
+
+import Control.Monad.Reader (mapReader, Reader, ask, local)
+import Control.Applicative (liftA2)
+import Data.Monoid ((<>))
+import qualified Data.Text as T
+import Data.Number.FixedFunctions (sin, cos, acos, tan, atan)
+import Prelude hiding (sin, cos, acos, tan, atan)
+import qualified Data.Matrix as M
+import Data.Matrix (Matrix) 
+import Data.List (groupBy)
+import Data.Maybe (fromMaybe, isNothing, isJust, fromJust)
+import Data.Text.Lazy.Builder (toLazyText, singleton, Builder)
+import Data.Text.Lazy (toStrict)
+import Hasmin.Config
+import Hasmin.Types.Class
+import Hasmin.Utils
+import Hasmin.Types.Dimension
+import Hasmin.Types.PercentageLength
+import Hasmin.Types.Numeric
+import Text.PrettyPrint.Mainland (Doc, Pretty, ppr, char)
+
+-- Note: In a previous specification, translate3d() took two 
+-- <https://www.w3.org/TR/css-transforms-1/#typedef-translation-value \<translation-value\>>,
+-- however in the <https://drafts.csswg.org/css-transforms/ latest draft>, it
+-- takes two \<length-percentage\> (which makes sense since translateX() and
+-- translateY() also do).
+
+data TransformFunction = Mat (Matrix Number)
+                       | Mat3d (Matrix Number)
+                       | Perspective Distance
+                       | Rotate Angle
+                       | Rotate3d Number Number Number Angle
+                       | RotateX Angle
+                       | RotateY Angle
+                       | RotateZ Angle
+                       | Scale Number (Maybe Number)
+                       | Scale3d Number Number Number
+                       | ScaleX Number
+                       | ScaleY Number
+                       | ScaleZ Number
+                       | Skew Angle (Maybe Angle)
+                       | SkewX Angle
+                       | SkewY Angle
+                       | Translate PercentageLength (Maybe PercentageLength)
+                       | Translate3d PercentageLength PercentageLength Distance
+                       | TranslateX PercentageLength
+                       | TranslateY PercentageLength
+                       | TranslateZ Distance
+  deriving (Eq, Show)
+-- | There are a series of equivalences to keep in mind: translate(0),
+-- translate3d(0, 0, 0), translateX(0), translateY(0), translateZ(0), scale(1),
+-- scaleX(1), scaleY(1), scaleZ(1), rotate(0), rotate3d(1,1,1,0), rotateX(0),
+-- rotateY(0), rotateZ(0), perspective(infinity), matrix(1,0,0,1,0,0),
+-- matrix3d(1,0,0,0,0,1,0,0,0,0,1,0,0,0,0,1), skewX(0), skewY(0), and the
+-- shortest of them all: skew(0)
+-- All of these translate to the 4x4 identity matrix.
+instance Minifiable TransformFunction where
+  minifyWith (Mat3d m) = do
+      conf <- ask
+      if shouldMinifyTransformFunction conf
+         then case possibleRepresentations m of
+                []     -> pure (Mat3d m)
+                (x:xs) -> let simplifyAndConvertUnits a = local (const $ conf { dimensionSettings = DimMinOn }) (simplify a) 
+                          in go simplifyAndConvertUnits x xs
+         else pure (Mat3d m)
+    where go f y []     = f y 
+          go f y (z:zs) = do
+              currentLength <- textualLength <$> f y
+              newLength     <- textualLength <$> f z
+              if currentLength < newLength
+                 then go f y zs
+                 else go f z zs
+  minifyWith x = do
+      conf <- ask
+      if shouldMinifyTransformFunction conf
+         then case toMatrix3d x of
+                Just mat3d -> minifyWith mat3d
+                Nothing    -> simplify x
+         else pure x
+
+{-
+s = SkewX (Angle 45 Deg)
+tx = TranslateX (Right $ Distance (-6) PX)
+ry = RotateY (Angle (-9) Deg)
+ry2 = Rotate3d 0 (-1) 0 (Angle 9 Deg)
+t = TranslateZ (Distance 100 PX)
+t2 = Translate (Right (Distance 100 PX)) Nothing
+tr = Rotate3d 0 0 1 (Angle 90 Deg)
+tr1 = Rotate3d 0 0 1 (Angle 45 Deg)
+g x = runReader x defaultConfig
+-}
+
+instance ToText TransformFunction where
+  toBuilder (Translate pl mpl)   = "translate(" 
+      <> toBuilder pl <> maybeWithComma mpl <> singleton ')'
+  toBuilder (TranslateX pl)      = "translatex(" 
+      <> either toBuilder toBuilder pl <> singleton ')'
+  toBuilder (TranslateY pl)      = "translatey(" <> either toBuilder toBuilder pl <> singleton ')'
+  toBuilder (TranslateZ d)       = "translatez(" <> toBuilder d <> singleton ')'
+  toBuilder (Scale n mn)         = "scale(" <> toBuilder n <> maybeWithComma mn <> singleton ')'
+  toBuilder (ScaleX n)           = "scalex(" <> toBuilder n <> singleton ')'
+  toBuilder (ScaleY n)           = "scaley(" <> toBuilder n <> singleton ')'
+  toBuilder (ScaleZ n)           = "scalez(" <> toBuilder n <> singleton ')'
+  toBuilder (Skew a ma)          = "skew(" <> toBuilder a <> maybeWithComma ma <> singleton ')'
+  toBuilder (SkewX a)            = "skewx(" <> toBuilder a <> singleton ')'
+  toBuilder (SkewY a)            = "skewy(" <> toBuilder a <> singleton ')'
+  toBuilder (Rotate a)           = "rotate(" <> toBuilder a <> singleton ')'
+  toBuilder (RotateX a)          = "rotatex(" <> toBuilder a <> singleton ')'
+  toBuilder (RotateY a)          = "rotatey(" <> toBuilder a <> singleton ')'
+  toBuilder (RotateZ a)          = "rotatez(" <> toBuilder a <> singleton ')'
+  toBuilder (Rotate3d x y z a)   = "rotate3d(" <> toBuilder x <> singleton ',' 
+      <> toBuilder y <> singleton ',' <> toBuilder z <> singleton ',' 
+      <> toBuilder a <> singleton ')'
+  toBuilder (Scale3d x y z)      = "scale3d(" <> toBuilder x <> singleton ','
+      <> toBuilder y <> singleton ',' <> toBuilder z <> singleton ')'
+  toBuilder (Perspective d)      = "perspective(" <> toBuilder d <> singleton ')'
+  toBuilder (Translate3d x y z ) = "translate3d(" <> toBuilder x <> singleton ','
+      <> toBuilder y <> singleton ',' <> toBuilder z <> singleton ')'
+  toBuilder (Mat m)              = "matrix(" 
+      <> mconcatIntersperse toBuilder (singleton ',') (M.toList m) <> singleton ')'
+  toBuilder (Mat3d m)            = "matrix3d(" 
+      <> mconcatIntersperse toBuilder (singleton ',') (M.toList m) <> singleton ')'
+
+maybeWithComma :: ToText a => Maybe a -> Builder
+maybeWithComma = maybe mempty (\x -> singleton ',' <> toBuilder x)
+
+instance Pretty TransformFunction where
+  ppr (Translate pl mpl)  = "translate(" <> eitherToDoc pl 
+                         <> maybe mempty (\x -> char ',' <> eitherToDoc x) mpl 
+                         <> char ')'
+  ppr (TranslateX pl)     = "translatex(" <> either ppr ppr pl <> char ')'
+  ppr (TranslateY pl)     = "translatey(" <> either ppr ppr pl <> char ')'
+  ppr (TranslateZ d)      = "translatez(" <> ppr d <> char ')'
+  ppr (Scale n mn)        = "scale(" <> ppr n <> maybeToDoc mn <> char ')'
+  ppr (ScaleX n)          = "scalex(" <> ppr n <> char ')'
+  ppr (ScaleY n)          = "scaley(" <> ppr n <> char ')'
+  ppr (ScaleZ n)          = "scalez(" <> ppr n <> char ')'
+  ppr (Skew a ma)         = "skew(" <> ppr a <> maybeToDoc ma <> char ')'
+  ppr (SkewX a)           = "skewx(" <> ppr a <> char ')'
+  ppr (SkewY a)           = "skewy(" <> ppr a <> char ')'
+  ppr (Rotate a)          = "rotate(" <> ppr a <> char ')'
+  ppr (RotateX a)         = "rotatex(" <> ppr a <> char ')'
+  ppr (RotateY a)         = "rotatey(" <> ppr a <> char ')'
+  ppr (RotateZ a)         = "rotatez(" <> ppr a <> char ')'
+  ppr (Rotate3d x y z a)  = "rotate3d(" <> ppr x <> char ',' <> ppr y 
+                          <> char ',' <> ppr z <> char ',' <> ppr a <> char ')'
+  ppr (Scale3d x y z)     = "scale3d(" <> ppr x <> char ',' <> ppr y 
+                          <> char ',' <> ppr z <> char ')'
+  ppr (Perspective d)     = "perspective(" <> ppr d <> char ')'
+  ppr (Translate3d x y z) = "translate3d(" <> eitherToDoc x <> char ',' 
+                          <> eitherToDoc y <> char ',' <> ppr z <> char ')'
+  ppr (Mat m)             = "matrix(" 
+      <> mconcatIntersperse ppr (char ',') (M.toList m) <> char ')'
+  ppr (Mat3d m)           = "matrix3d(" 
+      <> mconcatIntersperse ppr (char ',') (M.toList m) <> char ')'
+
+maybeToDoc :: Pretty a => Maybe a -> Doc
+maybeToDoc = maybe mempty (\x -> char ',' <> ppr x)
+
+eitherToDoc :: (Pretty a, Pretty b) => Either a b -> Doc
+eitherToDoc = either ppr ppr
+
+mkMat :: [Number] -> TransformFunction
+mkMat = Mat . M.fromList 3 2
+
+mkMat3d :: [Number] -> TransformFunction
+mkMat3d = Mat3d . M.fromList 4 4
+
+toMatrix3d :: TransformFunction -> Maybe TransformFunction
+toMatrix3d m@Mat3d{} = Just m
+toMatrix3d (Mat x)   = Just $ toMat3d (M.toList x)
+  where toMat3d [a,b,c,d,e,f] = mkMat3d [a, c, 0, e,
+                                         b, d, 0, f,
+                                         0, 0, 1, 0,
+                                         0, 0, 0, 1]
+        toMat3d _ = error "invalid matrix size!"
+toMatrix3d (Translate pl mpl)
+    | isNonZeroPercentage pl = Nothing
+    | isJust mpl && isNonZeroPercentage (fromJust mpl) = Nothing
+    | otherwise = Just . Mat3d $ mkTranslate3dMatrix x y 0
+  where x = either (const 0) fromPixelsToNum pl
+        y = maybe 0 (fromPixelsToNum . fromRight') mpl
+toMatrix3d (TranslateX pl)
+    | isNonZeroPercentage pl = Nothing 
+    | isRight pl && isRelativeDistance (fromRight' pl) = Nothing 
+    | otherwise = Just . Mat3d $ mkTranslate3dMatrix x 0 0 
+  where x = either (const 0) fromPixelsToNum pl
+toMatrix3d (TranslateY pl)
+    | isNonZeroPercentage pl = Nothing 
+    | isRight pl && isRelativeDistance (fromRight' pl) = Nothing 
+    | otherwise = Just . Mat3d $ mkTranslate3dMatrix 0 y 0
+  where y = either (const 0) fromPixelsToNum pl
+toMatrix3d (TranslateZ d)
+    | isRelativeDistance d  = Nothing 
+    | otherwise = Just . Mat3d $ mkTranslate3dMatrix 0 0 z
+  where z = fromPixelsToNum d
+toMatrix3d (Scale n mn) = Just . Mat3d $ mkScale3dMatrix n y 1
+  where y = fromMaybe n mn
+toMatrix3d (ScaleX n) = Just . Mat3d $ mkScale3dMatrix n 1 1
+toMatrix3d (ScaleY n) = Just . Mat3d $ mkScale3dMatrix 1 n 1
+toMatrix3d (ScaleZ n) = Just . Mat3d $ mkScale3dMatrix 1 1 n
+toMatrix3d (Skew a ma) = Just . Mat3d $ mkSkewMatrix α β 
+  where α = tangent a
+        β = maybe 0 tangent ma
+toMatrix3d (SkewX a) = Just . Mat3d $ mkSkewMatrix (tangent a) 0
+toMatrix3d (SkewY a) = Just . Mat3d $ mkSkewMatrix 0 (tangent a)
+toMatrix3d (Translate3d pl1 pl2 d)
+    | isNonZeroPercentage pl1 || isNonZeroPercentage pl2 = Nothing 
+    | isRight pl1 && isRelativeDistance (fromRight' pl1) = Nothing
+    | isRight pl2 && isRelativeDistance (fromRight' pl2) = Nothing 
+    | isRelativeDistance d = Nothing 
+    | otherwise = let x = either (const 0) fromPixelsToNum pl1
+                      y = either (const 0) fromPixelsToNum pl2
+                      z = fromPixelsToNum d
+                  in Just . Mat3d $ mkTranslate3dMatrix x y z
+toMatrix3d (Scale3d x y z) = Just . Mat3d $ mkScale3dMatrix x y z
+toMatrix3d (Perspective d)
+    | d == Distance 0 Q = Nothing
+    | otherwise         = let c = fromPixelsToNum d
+                          in Just . Mat3d $ mkPerspectiveMatrix c 
+-- Note: The commented code is fine, but until we implement the 
+-- function that converts a matrix back to a rotate function, uncommenting this
+-- breaks the minification, e.g. it says that:
+-- minify rotate(90deg) == matrix(0,1,-1,0,0,0)
+{-
+toMatrix3d (Rotate a) = Just $ rotateIn3d (0,0,1) (fromRadiansToNum a)
+toMatrix3d (Rotate3d x y z a) = Just $ rotateIn3d (x,y,z) (fromRadiansToNum a)
+toMatrix3d (RotateX a) = Just $ rotateIn3d (1,0,0) (fromRadiansToNum a)
+toMatrix3d (RotateY a) = Just $ rotateIn3d (0,1,0) (fromRadiansToNum a)
+toMatrix3d (RotateZ a) = Just $ rotateIn3d (0,0,1) (fromRadiansToNum a)
+-}
+toMatrix3d _ = Nothing
+
+-- Used for the rotate3d(), rotate(), ..., functions. Given an [x,y,z] vector
+-- and an angle, create its corresponding rotation matrix.
+{-
+rotateIn3d :: (Number, Number, Number) -> Number -> TransformFunction
+rotateIn3d (b,c,d) a = mkMat3d $ fmap toNumber
+    [1-2*(y^2 + z^2)*sq, 2*(x*y*sq - z*sc),  2*(x*z*sq + y*sc),  0,
+     2*(x*y*sq + z*sc),  1-2*(x^2 + z^2)*sq, 2*(y*z*sq - x*sc),  0,
+     2*(x*z*sq - y*sc),  2*(y*z*sq + x*sc),  1-2*(x^2 + y^2)*sq, 0,
+     0,                  0,                  0,                  1]    
+  where sc = sin epsilon (α/2) * cos epsilon (α/2)  :: Rational
+        sq = sin epsilon (α/2) ^ 2 :: Rational
+        x  = toRational b
+        y  = toRational c
+        z  = toRational d
+        α  = toRational a
+-}
+
+-- Code translated from: http://www.euclideanspace.com/maths/geometry/rotations/conversions/matrixToAngle/
+-- TODO: see how to get different representations, so that negative values are
+-- still properly handled, i.e. make things such as rotateY(-9deg) work.
+matrixToRotate3d :: Matrix Number -> [TransformFunction]
+matrixToRotate3d _ = []
+{-
+    -- if it has translation values, then we can't convert it to a rotation function
+    | M.unsafeGet 3 4 m /= 0 || M.unsafeGet 2 4 m /= 0 || M.unsafeGet 1 4 m /= 0 = []
+    -- | not isRotationMatrix = []
+    | abs (m12 - m21) < ep && abs (m13 - m31) < ep
+                           && abs (m23 - m32) < ep = [handleSingularity]
+    | otherwise = let a = arccos $ (m11 + m22 + m33 - 1)/2
+                      qx = (m32 - m23)/(2 * sine a)
+                      qy = (m13 - m31)/(2 * sine a)
+                      qz = (m21 - m12)/(2 * sine a)
+                  in [Rotate3d qx qy qz (Angle a Rad)]
+  where m11 = M.unsafeGet 1 1 m
+        m22 = M.unsafeGet 2 2 m
+        m33 = M.unsafeGet 3 3 m
+        m32 = M.unsafeGet 3 2 m
+        m23 = M.unsafeGet 2 3 m
+        m13 = M.unsafeGet 1 3 m
+        m31 = M.unsafeGet 3 1 m
+        m21 = M.unsafeGet 2 1 m
+        m12 = M.unsafeGet 1 2 m
+        ep = 0.01 -- margin to allow for rounding errors
+        ep2 = 0.1
+-- Make sure that the input is a pure rotation matrix.
+-- The condition for this is:
+-- R' * R = I, and det(R) = 1
+        isRotationMatrix 
+            |    abs (m11*m12 + m12*m22 + m13*m23) > ep
+              || abs (m11*m31 + m12*m32 + m13*m33) > ep
+              || abs (m21*m31 + m22*m32 + m23*m33) > ep
+              || abs (m11*m11 + m12*m12 + m13*m13 - 1) > ep
+              || abs (m21*m21 + m22*m11 + m23*m23 - 1) > ep
+              || abs (m31*m31 + m32*m32 + m33*m33 - 1) > ep = False
+            | otherwise = abs (detLU m - 1) < ep
+        handleSingularity 
+            |    abs (m12 + m21) < ep2 
+              && abs (m13 + m31) < ep2 
+              && abs (m23 - m32) < ep2 
+              && abs (m11 + m22 + m33 - 3) < ep2 = Rotate3d 1 0 0 (Angle 0 Deg)
+            | otherwise = let xx = (m11+1)/2
+                              yy = (m22+1)/2
+                              zz = (m33+1)/2
+                              xy = (m12 + m21)/4
+                              xz = (m13 + m31)/4
+                              yz = (m23 + m32)/4
+                          in handle180Singularity xx yy zz xy xz yz
+          where handle180Singularity xx yy zz xy xz yz
+                    | xx > yy && xx > zz = if xx < ep
+                                              then result 0 0.7071 0.7071
+                                              else let x = toNumber . sqrt $ fromNumber xx
+                                                   in result x (xy/x) (xz/x)
+                    | yy > zz = if yy < ep
+                                   then result 0.7071 0 0.7071
+                                   else let y = toNumber . sqrt $ fromNumber yy
+                                        in result (xy/y) y (yz/y)
+                    | otherwise = if zz < ep
+                                     then result 0.7071 0.7071 0
+                                     else let z = toNumber . sqrt $ fromNumber zz
+                                          in result (xz/z) (yz/z) z
+                  where result vx vy vz = Rotate3d vx vy vz (Angle 180 Deg)
+-}
+
+isRelativeDistance :: Distance -> Bool
+isRelativeDistance (Distance _ u) = isRelative u
+
+fromPixelsToNum :: Distance -> Number
+fromPixelsToNum (Distance n u) = toPixels n u
+
+fromRadiansToNum :: Angle -> Number
+fromRadiansToNum (Angle n u) = toRadians n u
+
+tangent :: Angle -> Number
+tangent =  toNumber . tan epsilon . fromNumber . fromRadiansToNum
+
+arctan :: Number -> Number
+arctan = toNumber . atan epsilon . fromNumber 
+
+-- sine :: Number -> Number
+-- sine = toNumber . sin epsilon . fromNumber 
+
+-- arccos :: Number -> Number
+-- arccos = toNumber . acos epsilon . fromNumber 
+
+getMat :: TransformFunction -> Matrix Number
+getMat (Mat q)   = q
+getMat (Mat3d q) = q
+getMat _         = error "getMat: not a matrix!"
+
+-- toMat :: TransformFunction -> Matrix Number
+-- toMat x = getMat . fromJust $ toMatrix3d x
+
+------------------------------------------------------------------------------
+
+-- | Convert a matrix to all the other possible equivalent
+-- transformation functions (if any).
+possibleRepresentations :: Matrix Number -> [TransformFunction]
+possibleRepresentations m = matrixToRotate3d m ++ mconcat
+    [matrixToSkewFunctions m, matrixToTranslateFunctions m
+    ,matrixToMat m, matrixToPerspective m
+    ,matrixToScaleFunctions m] -- TODO add rotation when implemented
+
+matrixToSkewFunctions :: Matrix Number -> [TransformFunction]
+matrixToSkewFunctions m
+    | skewMatrix == m = Skew a (Just b) : others
+    | otherwise       = []
+  where α = M.unsafeGet 1 2 m 
+        β = M.unsafeGet 2 1 m
+        a = Angle (arctan α) Rad
+        b = Angle (arctan β) Rad
+        skewMatrix = mkSkewMatrix α β
+        others
+            | α /= 0 && β == 0 = [SkewX a]
+            | β /= 0 && α == 0 = [SkewY b]
+            | otherwise        = [] -- The only case when we can use either of
+                                    -- them is when both α and β are zero, but if so
+                                    -- skew is already shorter, so don't return it. 
+
+matrixToTranslateFunctions :: Matrix Number -> [TransformFunction]
+matrixToTranslateFunctions m
+    | mkTranslate3dMatrix x y z == m = Translate3d tx ty tz : others
+    | otherwise                      = []
+  where x  = M.unsafeGet 1 4 m
+        tx = Right $ Distance x PX
+        y  = M.unsafeGet 2 4 m
+        ty = Right $ Distance y PX
+        z  = M.unsafeGet 3 4 m
+        tz = Distance z PX
+        others 
+            | z == 0 && y == 0 = [TranslateX tx, Translate tx (Just ty)]
+            | x == 0 && z == 0 = [TranslateY ty, Translate tx (Just ty)]
+            | y == 0 && x == 0 = [TranslateZ tz]
+            | otherwise        = []
+
+matrixToScaleFunctions :: Matrix Number -> [TransformFunction]
+matrixToScaleFunctions m
+    | mkScale3dMatrix x y z == m = Scale3d x y z : others
+    | otherwise                  = []
+  where x = M.unsafeGet 1 1 m
+        y = M.unsafeGet 2 2 m
+        z = M.unsafeGet 3 3 m
+        others 
+            | z == 1 && y == 1 = [ScaleX x, Scale x Nothing]
+            | y == 1 && x == 1 = [ScaleZ z]
+            | x == 1 && z == 1 = [ScaleY y, Scale x (Just y)]
+            | otherwise        = []
+
+matrixToPerspective :: Matrix Number -> [TransformFunction]
+matrixToPerspective m
+    | c /= 0 && mkPerspectiveMatrix d == m = [Perspective $ Distance d PX]
+    | otherwise = []
+  where c = M.unsafeGet 4 3 m
+        d = (-1)/c
+
+matrixToMat :: Matrix Number -> [TransformFunction]
+matrixToMat m
+    | matrix == m = [mkMat [a,b,c,d,e,f]]
+    | otherwise   = []
+  where a = M.unsafeGet 1 1 m
+        b = M.unsafeGet 2 1 m
+        c = M.unsafeGet 1 2 m
+        d = M.unsafeGet 2 2 m
+        e = M.unsafeGet 1 4 m
+        f = M.unsafeGet 2 4 m
+        matrix = mkMatMatrix a b c d e f
+
+mkMatMatrix :: Number -> Number -> Number -> Number
+            -> Number -> Number -> Matrix Number
+mkMatMatrix a b c d e f = mk4x4Matrix [a, c, 0, e,
+                                       b, d, 0, f,
+                                       0, 0, 1, 0,
+                                       0, 0, 0, 1]
+mkTranslate3dMatrix :: Number -> Number -> Number -> Matrix Number
+mkTranslate3dMatrix x y z = mk4x4Matrix [1, 0, 0, x,
+                                         0, 1, 0, y,
+                                         0, 0, 1, z,
+                                         0, 0, 0, 1]
+
+mkScale3dMatrix :: Number -> Number -> Number -> Matrix Number
+mkScale3dMatrix x y z = mk4x4Matrix [x, 0, 0, 0,
+                                     0, y, 0, 0,
+                                     0, 0, z, 0,
+                                     0, 0, 0, 1]
+
+mkSkewMatrix :: Number -> Number -> Matrix Number 
+mkSkewMatrix a b = mk4x4Matrix [1, a, 0, 0,
+                                b, 1, 0, 0,
+                                0, 0, 1, 0,
+                                0, 0, 0, 1]
+
+mkPerspectiveMatrix :: Number -> Matrix Number
+mkPerspectiveMatrix c = let d = (-1/c) 
+                        in mk4x4Matrix [1, 0, 0, 0,
+                                        0, 1, 0, 0,
+                                        0, 0, 1, 0,
+                                        0, 0, d, 0]
+mk4x4Matrix :: [Number] -> Matrix Number
+mk4x4Matrix = M.fromList 4 4 
+
+-- | Simplifies a \<transform-function\> without converting it to a 4x4 matrix,
+-- by doing some simple conversions between the different functions, or
+-- minifying the dimension arguments (\<length\> and \<angle\> values)
+simplify :: TransformFunction -> Reader Config TransformFunction
+simplify (Translate pl mpl) 
+    | isNothing mpl || isZero (fromJust mpl) = do
+        x <- mapM minifyWith pl
+        pure $ Translate x Nothing
+    | otherwise = do x <- mapM minifyWith pl
+                     y <- (mapM . mapM) minifyWith mpl
+                     pure $ Translate x y
+simplify (TranslateX pl) = do
+    x <- mapM minifyWith pl
+    simplify $ Translate x Nothing 
+-- Note: It always makes sense to convert from translateX to translate
+-- Not sure about translateY. Converting to translate(0,a) might aid
+-- compression, even when we are adding one character, because we might be
+-- reducing entropy.
+simplify (TranslateY pl) = do
+    y <- mapM minifyWith pl
+    pure $ TranslateY y
+-- In scale(), if the second parameter isn't present, it defaults to the first.
+-- Therefore:  scale(a,a) == scale(a)
+simplify s@(Scale n mn)  = pure $ maybe s removeDefaultArgument mn
+  where removeDefaultArgument x
+            | n == x    = Scale n Nothing
+            | otherwise = s
+-- TODO see if it is better to leave them as is, or convert them to scale(x,1)
+-- and scale(1,x), respectively, to aid gzip compression
+simplify s@(ScaleX _)    = pure s
+simplify s@(ScaleY _)    = pure s 
+-- In skew(), if the second parameter isn't present, it defaults to the zero.
+simplify (Skew a ma)
+      | defaultSecondArgument = do ang <- minifyWith a
+                                   simplify $ Skew ang Nothing
+      | otherwise             = liftA2 Skew (minifyWith a) (mapM minifyWith ma)
+    where defaultSecondArgument = maybe False (== Angle 0 Deg) ma
+simplify (SkewY a)
+      | a == Angle 0 Deg = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise        = fmap SkewY (minifyWith a)
+simplify (SkewX a)
+      | a == Angle 0 Deg = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise        = fmap SkewX (minifyWith a)
+simplify (Rotate a)
+      | a == Angle 0 Deg = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise        = fmap Rotate (minifyWith a)
+simplify (RotateX a)
+      | a == Angle 0 Deg = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise        = fmap RotateX (minifyWith a)
+simplify (RotateY a)
+      | a == Angle 0 Deg = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise        = fmap RotateY (minifyWith a)
+-- rotateZ(a) is the same as rotate3d(0,0,1,a), which also equals rotate(a)
+simplify (RotateZ a)
+      | a == Angle 0 Deg = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise        = fmap Rotate (minifyWith a)
+simplify (Rotate3d x y z a)
+      | abs (x - 1) < ep && abs y < ep && abs z < ep = simplify $ RotateX a
+      | abs x < ep && abs (y - 1) < ep && abs z < ep = simplify $ RotateY a
+      | abs x < ep && abs y < ep && abs (z - 1) < ep = fmap Rotate (minifyWith a)
+  where ep = toNumber epsilon
+simplify (ScaleZ n)
+      | n == 1    = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise = pure $ ScaleZ n
+simplify (Perspective d) = fmap Perspective (minifyWith d)
+simplify (TranslateZ d)
+      | d == Distance 0 Q = pure $ Skew (Angle 0 Deg) Nothing
+      | otherwise         = fmap TranslateZ (minifyWith d)
+simplify s@(Scale3d x y z)
+      | z == 1           = simplify $ Scale x (Just y)
+      | x == 1 && y == 1 = simplify $ ScaleZ z 
+      | otherwise        = pure s
+simplify (Translate3d x y z )
+      | isZero y && z == Distance 0 Q = either (f TranslateX) (g TranslateX) x
+      | isZero x && isZero y          = simplify $ TranslateZ z
+      | isZero x && z == Distance 0 Q = either (f TranslateY) (g TranslateY) y
+    where f con a | a == 0    = simplify . con . Right $ Distance 0 Q
+                  | otherwise = simplify . con . Left $ a
+          g con a = simplify . con $ Right a -- A distance, transform an minify
+simplify x = pure x 
+
+-- | Combines consecutive \<transform-functions\> whenever possible, leaving
+-- translate functions that can't be converted to a matrix (because they use
+-- percentages or relative units) as they are, in the position they are in the
+-- list (since matrix multiplication isn't conmutative)
+-- Example:
+-- >>> import Control.Monad.Reader
+-- >>> let t10 = Translate (Right (Distance 10 PX)) Nothing
+-- >>> let tp  = Translate (Left (Percentage 100)) Nothing
+-- >>> let s90 = Skew (Angle 90 Deg) Nothing
+-- >>> let s45 = Skew (Angle 45 Deg) Nothing
+-- >>> let sx5 = ScaleX 5
+-- >>> let f x = runReader (combine x) defaultConfig
+-- >>> fmap toText $ f [t10, s45, sx5]
+-- ["matrix(5,0,1,1,10,0)"]
+-- >>> fmap toText $ f [s45,tp,sx5,sx5,sx5]
+-- ["skew(45deg)","translate(100%)","scale(125)"]
+combine :: [TransformFunction] -> Reader Config [TransformFunction]
+combine xs = do
+    combinedLength <- mapReader (getLength . asBuilder) combinedFunctions
+    originalLength <- mapReader (getLength . asBuilder) minifiedOriginal
+    if combinedLength < originalLength
+       then combinedFunctions
+       else minifiedOriginal  
+  where getLength = T.length . toStrict . toLazyText
+        asBuilder = mconcatIntersperse toBuilder (singleton ' ')
+        combinedFunctions = mapM handleMatrices . groupByMatrices $ zip (fmap toMatrix3d xs) xs
+        minifiedOriginal = mapM minifyWith xs
+        groupByMatrices   = groupBy (\(a,_) (b,_) -> isJust a && isJust b) 
+        handleMatrices l@((x,a):_)
+            | isJust x  = minifyWith . Mat3d . foldr (*) (M.identity 4 :: Matrix Number) $ fmap (getMat . fromJust . fst) l
+            | otherwise = simplify a
+        handleMatrices [] = error "empty list as argument to handleMatrices"
diff --git a/src/Hasmin/Types/Value.hs b/src/Hasmin/Types/Value.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Types/Value.hs
@@ -0,0 +1,454 @@
+{-# LANGUAGE OverloadedStrings, FlexibleInstances, GeneralizedNewtypeDeriving #-}
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Types.Value
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Types.Value (
+    Value(..), Values(..), TextV(..), Separator(..), Url(..), mkOther,
+    mkValues, valuesToList, optimizeFontFamily, lowercaseText
+    ) where
+
+import Control.Monad.Reader (ask, Reader, mapReader)
+import Data.Monoid ((<>))
+import Data.Maybe (isJust, catMaybes, isNothing)
+import Data.Text (Text, toCaseFold)
+import qualified Data.Text as T
+import Data.Text.Lazy.Builder (fromText, singleton, Builder)
+import Data.String (IsString)
+import Hasmin.Config
+import Hasmin.Types.Class
+import Hasmin.Types.Color
+import Hasmin.Types.Dimension
+import Hasmin.Types.Gradient
+import Hasmin.Types.Numeric
+import Hasmin.Types.String
+import Hasmin.Types.Position
+import Hasmin.Types.RepeatStyle
+import Hasmin.Types.BgSize
+import Hasmin.Types.TransformFunction
+import Hasmin.Types.TimingFunction
+import Hasmin.Types.FilterFunction
+import Hasmin.Types.Shadow
+import Hasmin.Utils
+import Text.PrettyPrint.Mainland (Pretty, ppr, strictText, space, comma, char)
+
+data Value = Inherit
+           | Initial
+           | Unset
+           | NumberV Number
+           | PercentageV Percentage
+           | DistanceV Distance
+           | AngleV Angle
+           | DurationV Duration
+           | FrequencyV Frequency
+           | ResolutionV Resolution
+           | ColorV Color
+           | GradientV Text Gradient -- the Text is just to handle function prefixes
+           | GenericFunc Text Values
+           | TransformV TransformFunction
+           | TimingFuncV TimingFunction
+           | FilterV FilterFunction
+           | ShadowV Shadow
+           | ShadowText Distance Distance (Maybe Distance) (Maybe Color) -- <shadow-t> type
+           | PositionV Position
+           | RepeatStyleV RepeatStyle
+           | BgSizeV BgSize
+           --          <bg-image> || <position> [ / <bg-size> ]?  || <repeat-style>    || <attachment>    || <box>{1,2}
+           | BgLayer (Maybe Value) (Maybe Position) (Maybe BgSize) (Maybe RepeatStyle) (Maybe TextV) (Maybe TextV) (Maybe TextV)
+           --              <bg-image> || <position> [ / <bg-size> ]? || <repeat-style> || <attachment> || <box> || <box> || <'background-color'>
+           | FinalBgLayer (Maybe Value) (Maybe Position) (Maybe BgSize) (Maybe RepeatStyle) (Maybe TextV) (Maybe TextV) (Maybe TextV) (Maybe Color)
+           -- [ none | <single-transition-property> ] || <time> || <single-transition-timing-function> || <time>
+           | SingleTransition (Maybe TextV) (Maybe Duration) (Maybe TimingFunction) (Maybe Duration)
+           --                         <time> || <timing-function> || <time> || <iteration-count> || <animation-direction> || <animation-fill-mode> || <animation-play-state> || [ none | <keyframes-name> ]
+           | SingleAnimation (Maybe Duration) (Maybe TimingFunction) (Maybe Duration) (Maybe Value)  (Maybe TextV) (Maybe TextV) (Maybe TextV) (Maybe Value)
+           -- [ [ <'font-style'> || <font-variant-css21> || <'font-weight'> || <'font-stretch'> ]? <'font-size'> [ / <'line-height'> ]? <'font-family'> ] |
+           | FontV (Maybe TextV) (Maybe TextV) (Maybe Value) (Maybe TextV) Value (Maybe Value) [Value]
+           | StringV StringType
+           | UrlV Url
+           | Format [StringType]
+           | Local (Either Text StringType)
+           | Rect Distance Distance Distance Distance -- Should be in <shape>, but that accepts only rect()
+           | Other TextV
+  deriving (Eq, Show)
+
+-- | Redefines equality to be case-insensitive, since CSS literal values such as
+-- "auto", "none", etc. are so.
+newtype TextV = TextV { getText :: Text }
+  deriving (Show, Ord, IsString)
+
+instance Eq TextV where
+  TextV t1 == TextV t2 = toCaseFold t1 == toCaseFold t2
+instance ToText TextV where
+  toText = getText
+instance Pretty Value where
+  ppr (NumberV n)     = ppr n
+  ppr (PercentageV p) = ppr p
+  ppr (DistanceV d)   = ppr d
+  ppr (AngleV a)      = ppr a
+  ppr (DurationV x)   = ppr x
+  ppr (FrequencyV x)  = ppr x
+  ppr (ResolutionV x) = ppr x
+  ppr (ColorV x)      = ppr x
+  ppr (Other x)       = ppr (getText x)
+  ppr x               = (strictText . toText) x
+
+mkOther :: Text -> Value
+mkOther = Other . TextV
+
+newtype Url = Url (Either Text StringType)
+  deriving (Eq, Show)
+instance ToText Url where
+  toBuilder (Url x) = "url(" <> toBuilder x <> singleton ')'
+instance Pretty Url where
+  ppr (Url x) = strictText "url(" <> ppr x <> char ')'
+instance Minifiable Url where
+  minifyWith u@(Url x) = do
+      conf <- ask
+      pure $ if shouldRemoveQuotes conf
+                then Url $ either Left unquoteUrl x
+                else u
+
+instance ToText Value where
+  toBuilder Initial         = "initial"
+  toBuilder Inherit         = "inherit"
+  toBuilder Unset           = "unset"
+  toBuilder (NumberV n)     = toBuilder n
+  toBuilder (PercentageV p) = (fromText . toText) p
+  toBuilder (ColorV c)      = toBuilder c
+  toBuilder (DistanceV d)   = toBuilder d
+  toBuilder (AngleV a)      = toBuilder a
+  toBuilder (DurationV d)   = toBuilder d
+  toBuilder (FrequencyV f)  = toBuilder f
+  toBuilder (ResolutionV r) = toBuilder r
+  toBuilder (FilterV f)     = toBuilder f
+  toBuilder (ShadowV s)     = toBuilder s
+  toBuilder (StringV s)     = toBuilder s
+  toBuilder (Other t)       = fromText (getText t)
+  toBuilder (UrlV u)        = toBuilder u
+  toBuilder (GenericFunc n vs) = toBuilder n <> singleton '(' <> toBuilder vs <> singleton ')'
+  toBuilder (Local x)       = "local(" <> toBuilder x <> singleton ')'
+  toBuilder (Format x)      = "format(" <> formatString <> singleton ')'
+    where formatString = mconcatIntersperse id (singleton ',') (fmap toBuilder x)
+  toBuilder (GradientV t g) = fromText t <> singleton '(' <> toBuilder g <> singleton ')'
+  toBuilder (TransformV x)  = toBuilder x
+  toBuilder (TimingFuncV x) = toBuilder x
+  toBuilder (Rect a b c d)  = "rect(" <> funcValues <> singleton ')'
+    where funcValues = mconcatIntersperse toBuilder (singleton ',') [a, b, c, d]
+  toBuilder (PositionV p)   = toBuilder p
+  toBuilder (RepeatStyleV r) = toBuilder r
+  toBuilder (BgSizeV b)     = toBuilder b
+  toBuilder (BgLayer a b c d e f g) =
+      let sz  = maybe mempty (\x -> singleton '/' <> toBuilder x) c
+          list = catMaybes [bld a, bld d, bld e, bld f, bld g]
+      in if null list
+            then maybe mempty toBuilder b <> sz
+            else mconcatIntersperse id (singleton ' ') list <> maybe mempty (\x -> singleton ' ' <> toBuilder x) b <> sz
+  toBuilder (FinalBgLayer a b c d e f g col) =
+      let sz  = maybe mempty (\x -> singleton '/' <> toBuilder x) c
+          list = catMaybes [bld a, bld d, bld e, bld f, bld g]
+      in if null list
+            then let posAndSize = maybe mempty toBuilder b <> sz
+                 in if mempty == posAndSize
+                       then maybe mempty toBuilder col
+                       else posAndSize <> spacePrefixed col
+            else mconcatIntersperse id (singleton ' ') list <> spacePrefixed b <> sz <> spacePrefixed col
+    where spacePrefixed :: ToText a => Maybe a -> Builder
+          spacePrefixed = maybe mempty (\x -> singleton ' ' <> toBuilder x)
+  toBuilder (SingleTransition prop t1 tf t2) =
+      mconcatIntersperse id (singleton ' ') $ catMaybes [bld tf, bld t1, bld t2, bld prop]
+  toBuilder (SingleAnimation t1 tf t2 ic ad af ap kf) =
+      let list = catMaybes [bld t1, bld t2, bld tf, bld ic, bld ad, bld af, bld ap, bld kf]
+      in mconcatIntersperse id (singleton ' ') list
+  toBuilder (FontV fsty fvar fwgt fstr fsz lh ff) =
+      let bldLh = maybe mempty (\x -> singleton '/' <> toBuilder x) lh
+          list  = catMaybes [bld fsty, bld fvar, bld fwgt, bld fstr]
+          ffam  = singleton ' ' <> mconcatIntersperse toBuilder (singleton ',') ff
+      in if null list
+            then toBuilder fsz <> bldLh <> ffam
+            else mconcatIntersperse id (singleton ' ') list <> singleton ' ' <> toBuilder fsz <> bldLh <> ffam
+  toBuilder (ShadowText l1 l2 ml mc) =
+      let maybeToBuilder :: ToText a => Maybe a -> Builder
+          maybeToBuilder = maybe mempty (\x -> singleton ' ' <> toBuilder x)
+      in toBuilder l1 <> singleton ' ' <> toBuilder l2
+       <> maybeToBuilder ml <> maybeToBuilder mc
+
+instance Minifiable Value where
+  minifyWith (ColorV c)       = ColorV <$> minifyWith c
+  minifyWith (DistanceV d)    = DistanceV <$> minifyWith d
+  minifyWith (AngleV a)       = AngleV <$> minifyWith a
+  minifyWith (DurationV d)    = DurationV <$> minifyWith d
+  minifyWith (FrequencyV f)   = FrequencyV <$> minifyWith f
+  minifyWith (ResolutionV r)  = ResolutionV <$> minifyWith r
+  minifyWith (GradientV t g)  = GradientV t <$> minifyWith g
+  minifyWith (FilterV f)      = FilterV <$> minifyWith f
+  minifyWith (ShadowV s)      = ShadowV <$> minifyWith s
+  minifyWith (ShadowText l1 l2 ml mc) = minifyPseudoShadow ShadowText l1 l2 ml mc
+  minifyWith (TransformV tf)  = TransformV <$> minifyWith tf
+  minifyWith (TimingFuncV tf) = TimingFuncV <$> minifyWith tf
+  minifyWith (StringV s)      = StringV <$> minifyWith s
+  minifyWith (UrlV u)         = UrlV <$> minifyWith u
+  minifyWith (Format x)       = Format <$> mapM minifyWith x
+  minifyWith (PositionV p)    = PositionV <$> minifyWith p
+  minifyWith (RepeatStyleV r) = RepeatStyleV <$> minifyWith r
+  minifyWith (BgSizeV b)      = BgSizeV <$> minifyWith b
+  minifyWith (BgLayer img pos sz rst att b1 b2) = do
+      conf <- ask
+      i <- handleImage img
+      s <- handleBgSize sz
+      p <- let cannotRemovePos = isJust s -- can only be removed if there is no later <bg-size>
+           in handlePosition cannotRemovePos pos
+      r <- handleRepeatStyle rst
+      a <- handleAttachment att
+      (bgOrigin, bgClip) <- handleBoxes b1 b2
+      pure $ if isNothing i && isNothing p && isNothing s && isNothing r
+                && isNothing a && isNothing bgOrigin && isNothing bgClip
+                -- Shortest would be a '0 0' position, but 'none' compresses
+                -- better because it is more frequent.
+                then BgLayer (Just $ mkOther "none") p s r a bgOrigin bgClip
+                else BgLayer i p s r a bgOrigin bgClip
+  minifyWith (FinalBgLayer img pos sz rst att b1 b2 col) = do
+      conf <- ask
+      i <- handleImage img
+      s <- handleBgSize sz
+      p <- let cannotRemovePos = isJust s -- can only be removed if there is no later <bg-size>
+           in handlePosition cannotRemovePos pos
+      r <- handleRepeatStyle rst
+      a <- handleAttachment att
+      c <- handleColor col
+      (bgOrigin, bgClip) <- handleBoxes b1 b2
+      pure $ if isNothing i && isNothing p && isNothing s && isNothing r
+                && isNothing a && isNothing bgOrigin && isNothing bgClip && isNothing c
+                -- Shortest would be a '0 0' position, but 'none' compresses
+                -- better because it is more frequent.
+                then FinalBgLayer (Just $ mkOther "none") p s r a bgOrigin bgClip c
+                else FinalBgLayer i p s r a bgOrigin bgClip c
+  minifyWith (SingleTransition prop tdur tf tdel) = do
+      let p = if prop == Just (TextV "all")
+                 then Nothing
+                 else prop -- TODO lowercase here
+      (tDuration, tDelay) <- handleTime tdur tdel
+      tfunc               <- handleTimingFunction tf
+      pure $ if isNothing p && isNothing tDuration && isNothing tDelay && isNothing tfunc
+                then SingleTransition p (Just $ Duration 0 S) tfunc tDelay
+                else SingleTransition p tDuration tfunc tDelay
+  minifyWith (SingleAnimation t1 tf t2 ic ad af ap kf) = do
+      (tdur, tdel) <- handleTime t1 t2
+      tfunc        <- handleTimingFunction tf
+      icount       <- handleIterationCount ic
+      (kfrms, adir, afm, p) <- handleKeywords kf ad af ap
+      if isNothing tdur && isNothing tdel && isNothing tfunc && isNothing
+         icount && isNothing kfrms && isNothing adir && isNothing afm && isNothing p
+         then pure $ SingleAnimation tdur tfunc tdel (Just $ NumberV 1) adir afm p kfrms
+         else pure $ SingleAnimation tdur tfunc tdel icount adir afm p kfrms
+    where handleIterationCount :: Maybe Value -> Reader Config (Maybe Value)
+          handleIterationCount Nothing  = pure Nothing
+          handleIterationCount (Just x) =
+              case x of
+              NumberV 1 -> pure Nothing
+              _         -> pure (Just x)
+          handleKeywords Nothing x y z = pure (Nothing, simplifyDirection x, simplifyFillMode y, simplifyPauseState z)
+          handleKeywords v@(Just w) x y z
+              | w `elem` fmap mkOther ["normal", "reverse", "alternate", "alternate-reverse"] =
+                  pure (Just w, x, simplifyFillMode y, simplifyPauseState z)
+              | w == mkOther "none" && v == (Other <$> y) =
+                  pure (Nothing, simplifyDirection x, Nothing, simplifyPauseState z)
+              | w `elem` fmap mkOther ["forwards", "backwards", "both"] =
+                  pure (Just w, simplifyDirection x, y, simplifyPauseState z)
+              | w `elem` fmap mkOther ["running", "paused"] =
+                  pure (Just w, simplifyDirection x, simplifyFillMode y, z)
+              | otherwise = pure (Just w, simplifyDirection x, simplifyFillMode y, simplifyPauseState z)
+          simplifyDirection  = removeIfEqualTo "normal"
+          simplifyPauseState = removeIfEqualTo "running"
+          simplifyFillMode   = removeIfEqualTo "none"
+  minifyWith (FontV fsty fvar fwgt fstr fsz lh ff) = do
+      let sty = removeIfEqualTo "normal" fsty
+          var = removeIfEqualTo "normal" fvar
+          str = removeIfEqualTo "normal" fstr
+      wgt <- optimizeFontWeight fwgt
+      sz  <- minifyWith fsz
+      l   <- optimizeLineHeight lh
+      fam <- traverse optimizeFontFamily ff
+      pure $ FontV sty var wgt str sz l fam
+    where optimizeFontWeight :: Maybe Value -> Reader Config (Maybe Value)
+          optimizeFontWeight Nothing = pure Nothing
+          optimizeFontWeight (Just x) = do
+              conf <- ask
+              pure $ replaceForSynonym (fontweightSettings conf) x
+            where replaceForSynonym s (Other t)
+                    | t == TextV "normal"                       = Nothing
+                    | t == TextV "bold" && s == FontWeightMinOn = Just $ NumberV 700
+                    | otherwise                                 = Just $ Other t
+                  replaceForSynonym _ (NumberV 400) = Nothing
+                  replaceForSynonym _ y = Just y
+          optimizeLineHeight Nothing = pure Nothing
+          optimizeLineHeight (Just x) =
+              case x of
+                Other t -> if t == TextV "normal"
+                              then pure Nothing
+                              else Just <$> minifyWith x
+                y       -> Just <$> minifyWith y
+
+  minifyWith (GenericFunc n vs) = GenericFunc n <$> minifyWith vs
+  minifyWith (Local x)        = do
+      conf <- ask
+      v <- lowercaseParameters x
+      pure . Local $ if shouldRemoveQuotes conf
+                        then case v of
+                               Right s -> unquoteFontFamily s
+                               _       -> v
+                        else v
+    where lowercaseParameters :: Either Text StringType -> Reader Config (Either Text StringType)
+          lowercaseParameters y = do
+            conf <- ask
+            case letterCase conf of
+              Lowercase -> case y of
+                             Left  a -> mapReader Left $ lowercaseText a
+                             Right b -> mapReader Right $ mapString lowercaseText b >>= minifyWith
+              Original  -> pure y
+  minifyWith x = pure x
+
+handleRepeatStyle :: Maybe RepeatStyle -> Reader Config (Maybe RepeatStyle)
+handleRepeatStyle (Just x)
+    | x == RSPair RsRepeat Nothing = pure Nothing
+    | otherwise                    = Just <$> minifyWith x
+handleRepeatStyle Nothing = pure Nothing
+
+handleImage :: Maybe Value -> Reader Config (Maybe Value)
+handleImage (Just x)
+    | x == Other "none" = pure Nothing
+    | otherwise         = Just <$> minifyWith x
+handleImage Nothing = pure Nothing
+
+handleBoxes :: Maybe TextV -> Maybe TextV -> Reader Config (Maybe TextV, Maybe TextV)
+handleBoxes (Just o) (Just c)
+    | o == c = pure (Just o, Nothing) -- TODO need lowercasing here too.
+    | o == "padding-box" && c == "border-box" = pure (Nothing, Nothing)
+    | otherwise = pure (Just o, Just c)
+handleBoxes x y = pure (x, y)
+
+handleAttachment :: Maybe TextV -> Reader Config (Maybe TextV)
+handleAttachment = maybe (pure Nothing) f
+  where f x = pure $ if x == TextV "scroll"
+                        then Nothing
+                        else Just x -- TODO might need to lowercase here.
+
+handleColor :: Maybe Color -> Reader Config (Maybe Color)
+handleColor = maybe (pure Nothing) f
+  where f x = if x == Named "transparent"
+                 then pure Nothing
+                 else Just <$> minifyWith x
+
+handleBgSize :: Maybe BgSize -> Reader Config (Maybe BgSize)
+handleBgSize (Just b@BgSize{}) = do
+    minb <- minifyWith b
+    pure $ if minb == BgSize (Right Auto) Nothing
+              then Nothing
+              else Just minb
+handleBgSize x = pure x
+
+handlePosition :: Bool -> Maybe Position -> Reader Config (Maybe Position)
+handlePosition _ Nothing = pure Nothing
+handlePosition cannotRemovePos (Just x)
+    | cannotRemovePos = Just <$> minifyWith x
+    | otherwise    = do
+        conf <- ask
+        mx   <- minifyWith x
+        pure $ if mx == Position Nothing l0 Nothing l0
+                  then Nothing
+                  else Just $ if True {- shouldMinifyPosition conf -}
+                                 then mx
+                                 else x
+
+data Values = Values Value [(Separator, Value)]
+  deriving (Show, Eq)
+instance Minifiable Values where
+  minifyWith (Values v vs) = do
+      newV  <- minifyWith v
+      newVs <- (mapM . mapM) minifyWith vs
+      pure $ Values newV newVs
+
+instance ToText Values where
+  toBuilder (Values v vs) = toBuilder v <> foldr f mempty vs
+    where f (sep, val) z = toBuilder sep <> toBuilder val <> z
+instance Pretty Values where
+  ppr (Values v vs) = ppr v <> foldr f mempty vs
+    where f (sep, val) xs = ppr sep <> ppr val <> xs
+
+data Separator = Space | Slash | Comma
+  deriving (Show, Eq)
+instance ToText Separator where
+  toText Space = " "
+  toText Comma = ","
+  toText Slash = "/" -- Used, for example, by font to separate font-size and line-height
+instance Pretty Separator where
+  ppr Space = space
+  ppr Comma = comma
+  ppr Slash = strictText (toText Slash)
+
+valuesToList :: Values -> [Value]
+valuesToList (Values v vs) = v : map snd vs
+
+lowercaseText :: Text -> Reader Config Text
+lowercaseText t = do
+    conf <- ask
+    pure $ case letterCase conf of
+             Lowercase -> T.toLower t
+             Original  -> t
+
+-- Used to rewrap a list of values into the Values data type.
+-- Only call it with non-empty lists!
+mkValues :: [Value] -> Values
+mkValues (x:xs) = Values x (zip (repeat Space) xs)
+mkValues [ ]    = error "An empty list of values isn't valid"
+
+bld :: ToText a => Maybe a -> Maybe Builder
+bld = fmap toBuilder
+
+-- Used for SingleAnimation and SingleTransition minification.
+handleTimingFunction :: Maybe TimingFunction -> Reader Config (Maybe TimingFunction)
+handleTimingFunction Nothing = pure Nothing
+handleTimingFunction (Just tfunc)
+    | tfunc == Ease = pure Nothing
+    | otherwise = Just <$> minifyWith tfunc
+
+-- Used for SingleAnimation and SingleTransition minification.
+handleTime :: Maybe Duration -> Maybe Duration -> Reader Config (Maybe Duration, Maybe Duration)
+handleTime (Just t) Nothing = if t == Duration 0 S
+                                 then pure (Nothing, Nothing)
+                                 else do newT <- minifyWith t
+                                         pure (Just newT, Nothing)
+handleTime (Just t1) (Just t2)
+    | t1 == Duration 0 S = if t2 == t1
+                              then pure (Nothing, Nothing)
+                              else do newT2 <- minifyWith t2
+                                      newT1 <- minifyWith t1
+                                      pure (Just newT1, Just newT2)
+    | otherwise = do newT1 <- minifyWith t1
+                     if t2 == Duration 0 S
+                        then pure (Just t1, Nothing)
+                        else do newT2 <- minifyWith t2
+                                pure (Just newT1, Just newT2)
+handleTime _ _ = pure (Nothing, Nothing)
+
+removeIfEqualTo :: Text -> Maybe TextV -> Maybe TextV
+removeIfEqualTo _ Nothing  = Nothing
+removeIfEqualTo s (Just x)
+    | x == TextV s = Nothing
+    | otherwise    = Just x
+
+-- Unquotes font family names when possible
+optimizeFontFamily :: Value -> Reader Config Value
+optimizeFontFamily (Other t) = mkOther <$> lowercaseText (getText t)
+optimizeFontFamily (StringV s) = do
+    conf <- ask
+    ffamily <- mapString lowercaseText s
+    pure $ if shouldRemoveQuotes conf
+              then either mkOther StringV (unquoteFontFamily ffamily)
+              else StringV ffamily
+optimizeFontFamily x = pure x
+
diff --git a/src/Hasmin/Utils.hs b/src/Hasmin/Utils.hs
new file mode 100644
--- /dev/null
+++ b/src/Hasmin/Utils.hs
@@ -0,0 +1,67 @@
+-----------------------------------------------------------------------------
+-- |
+-- Module      : Hasmin.Utils
+-- Copyright   : (c) 2017 Cristian Adrián Ontivero
+-- License     : BSD3
+-- Stability   : experimental
+-- Portability : unknown
+--
+-----------------------------------------------------------------------------
+module Hasmin.Utils (
+      isRight
+    , isLeft
+    , epsilon
+    , eps
+    , fromLeft'
+    , fromRight'
+    , mconcatIntersperse
+    , restrict
+    , textualLength
+    , replaceAt
+    ) where
+
+import Data.Monoid ((<>))
+import qualified Data.Text as T
+import Hasmin.Types.Class
+
+textualLength :: ToText a => a -> Int
+textualLength = T.length . toText
+
+restrict :: Ord a => a -> a -> a -> a
+restrict minv maxv val | val >= maxv = maxv
+                       | val < minv  = minv
+                       | otherwise   = val
+
+mconcatIntersperse :: Monoid b => (a -> b) -> b -> [a] -> b
+mconcatIntersperse _ _ []            = mempty
+mconcatIntersperse toMonoid y (x:xs) = toMonoid x <> rest
+  where rest | null xs   = mempty
+             | otherwise = y <> mconcatIntersperse toMonoid y xs
+
+replaceAt :: Int -> a -> [a] -> [a]
+replaceAt i v ls = let (a, _:vs) = splitAt i ls
+                   in a ++ (v:vs)
+
+fromRight' :: Either a b -> b
+fromRight' (Right x) = x
+fromRight' _         = error "fromRight'"
+
+fromLeft' :: Either a b -> a
+fromLeft' (Left x) = x
+fromLeft' _        = error "fromLeft'"
+
+isRight :: Either a b -> Bool
+isRight Right{} = True
+isRight _       = False
+
+isLeft :: Either a b -> Bool
+isLeft Left{} = True
+isLeft _      = False
+
+-- TODO: Find out how precise is enough.
+-- Can we use double and round when printing?
+epsilon :: Rational
+epsilon = 2.2204460492503131e-30 :: Rational
+
+eps :: Rational
+eps = 0.000001 -- See https://bug-30341-attachments.webkit.org/attachment.cgi?id=45276
diff --git a/tests/DocTest.hs b/tests/DocTest.hs
new file mode 100644
--- /dev/null
+++ b/tests/DocTest.hs
@@ -0,0 +1,6 @@
+-- {-# OPTIONS_GHC -F -pgmF doctest-discover #-}
+
+import Test.DocTest (doctest)
+
+main :: IO ()
+main = doctest ["src"]
diff --git a/tests/Hasmin/Parser/InternalSpec.hs b/tests/Hasmin/Parser/InternalSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Parser/InternalSpec.hs
@@ -0,0 +1,207 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Parser.InternalSpec where
+
+import Test.Hspec
+-- import Test.QuickCheck
+import Hasmin.Parser.Internal
+import Hasmin.Parser.Value
+
+import Test.Hspec.Attoparsec (shouldSucceedOn, shouldFailOn)
+import Data.Text (Text)
+import Hasmin.TestUtils
+
+declarationParserTests :: Spec
+declarationParserTests =
+  describe "declaration parser tests" $
+    mapM_ (matchSpecWithDesc declaration) declarationTestInfo
+
+declarationTestInfo :: [(String, Text, Text)]
+declarationTestInfo = 
+  [("parses declaration with unknown property and value, ending in ';'",
+      "a:b;", "a:b")
+  ,("parses declaration with unknown property and value, ending in '}'",
+      "a:b}", "a:b")
+  ,("ignores comments before colon",
+      "a/**/:b;", "a:b")
+  ,("ignores comments after colon",
+      "a:/**/b;", "a:b")
+  ,("parses !important and prints it back without space",
+      "a:b ! important;", "a:b!important")
+  ,("ignores comments before semicolon",
+    "a:b/**/;", "a:b")
+  ,("parse declaration starting with '+' (IE <= 7)",
+    "+property:a", "+property:a")
+  ]
+
+declarationParsersTests :: Spec
+declarationParsersTests = 
+    describe "Declaration parsers tests" $ do
+      it "Parses valid font-style values, and fails with invalids" $ do
+        fontStyle `shouldSucceedOn` ("normal" :: Text)
+        fontStyle `shouldSucceedOn` ("italic" :: Text)
+        fontStyle `shouldSucceedOn` ("oblique" :: Text)
+        fontStyle `shouldFailOn` ("anything else" :: Text)
+
+{-
+styleRuleParser :: Spec
+  describe "style rule parser" $ do
+    it "parses rule whose final declaration ends in ';'" $ do
+      "a { margin : 0;}" ~> (styleRule 
+-}
+
+styleRuleTestInfo :: [(String, Text, Text)]
+styleRuleTestInfo = 
+  [("simple style rule",
+    "a { margin : 0 ;}", "a{margin:0}")
+  ,("handles unset", 
+    "h1 {\n    border: unset\n}", "h1{border:unset}")
+  ,("handles initial", 
+    "camelCase {\r color: initial;}", "camelCase{color:initial}")
+  ,("handles inherit", 
+    "h1 {\n\tpadding: inherit\f}", "h1{padding:inherit}")
+  ]
+
+selectorParserTests :: Spec
+selectorParserTests =
+  describe "selector parser tests" $
+    mapM_ (matchSpecWithDesc selector) selectorTestInfo
+
+selectorTestInfo :: [(String, Text, Text)]
+selectorTestInfo = 
+  [("Element", "h6", "h6")
+  ,("Element with namespace", "ns|body", "ns|body")
+  ,("Element with any namespace", "*|p", "*|p")
+  ,("Element with '|' but no namespace", "|html", "html") -- bar shouldn't show
+  ,("Class", ".class", ".class")
+  ,("Class name with high BMP char", ".無", ".無")
+  --,("Class name starting with number or dash", ".\\1\\#-\\.1 .\\--wie.-gehts", ".1#-.1.--wie.-gehts")
+  ,("Class name with emoji", ".☺", ".☺")
+  ,("Class name with multiple emoji", ".👊✊", ".👊✊")
+  ,("Id", "#AnId", "#AnId")
+  --,("Id starting with a number", "#\\5\\#-\\.5", "#5#-.5")
+  --,("Id starting with a number", "#\\5\\#-\\.5", "#5#-.5")
+  ,("Id with latin-1 character", "#ñ", "#ñ")
+-- The following four have a pseudo class syntax, but are pseudo elements
+  ,(":after pseudo element", ":after", ":after")
+  ,(":before pseudo element", ":before", ":before")
+  ,(":first-line pseudo element", ":first-line", ":first-line")
+  ,(":first-letter pseudo element", ":first-letter", ":first-letter")
+-- Since both syntaxes are valid, we print the shorter one (with a single ':')
+  ,("::after pseudo element", "::after", ":after")
+  ,("::before pseudo element", "::before", ":before")
+  ,("::first-line pseudo element", "::first-line", ":first-line")
+  ,("::first-letter pseudo element", "::first-letter", ":first-letter")
+  ,("Pseudo class", ":link", ":link")
+  --,("Pseudo class with escapes", ":na\\me", ":name")
+  --,("Pseudo class with content"
+  ,("Universal selector", "*", "*")
+  ,("Universal selector with namespace", "n|*", "n|*")
+  ,("Universal selector with any namespace", "*|*", "*|*")
+  ,("Universal selector with '|' but no namespace", "|*", "*") -- bar shouldn't show
+  ,(":not() functional pseudo-class of a selector list",
+    ":not(:nth-last-child(2n), elem, #id, .class)", ":not(:nth-last-child(2n),elem,#id,.class)")
+  ,(":matches() functional pseudo-class of a selector list",
+    ".class:matches(:nth-child(2) , element, #AnID, .aClass)", ".class:matches(:nth-child(2),element,#AnID,.aClass)")
+  ]
+{-
+  [("any element","*", "*")
+  ,("an element of type E","E","E")
+  ,("an E element with a \"foo\" attribute",
+    "E[foo]", "E[foo]")
+  ,("an E element whose \"foo\" attribute value is exactly equal to \"bar\"",
+    "E[foo=\"bar\"]","E[foo=\"bar\"]" )
+  ,("an E element whose \"foo\" attribute value is a list of whitespace-separated values, one of which is exactly equal to \"bar\"",
+    "E[foo~=\"bar\"]","E[foo~=\"bar\"]")
+  ,("an E element whose \"foo\" attribute value begins exactly with the string \"bar\"",
+    "E[foo^=\"bar\"]","E[foo^=\"bar\"]")
+  ,("an E element whose \"foo\" attribute value ends exactly with the string \"bar\"",
+    "E[foo$=\"bar\"]","E[foo$=\"bar\"]")
+  ,("an E element whose \"foo\" attribute value contains the substring \"bar\"",
+    "E[foo*=\"bar\"]","E[foo*=\"bar\"]")
+  ,("an E element whose \"foo\" attribute has a hyphen-separated list of values beginning (from the left) with \"en\"",
+    "E[foo|=\"en\"]","E[foo|=\"en\"]")
+  ,("an E element, root of the document",
+    "E:root","E:root")
+  ,("an E element, the n-th child of its parent",
+    "E:nth-child(n)","E:nth-child(n)")
+  ,("an E element, the n-th child of its parent, counting from the last one",
+    "E:nth-last-child(n)","E:nth-last-child(n)")
+  ,("an E element, the n-th sibling of its type",
+    "E:nth-of-type(n)","E:nth-of-type(n)")
+  ,("an E element, the n-th sibling of its type, counting from the last one",
+    "E:nth-last-of-type(n)","E:nth-last-of-type(n)")
+  ,("an E element, first child of its parent",
+    "E:first-child","E:first-child")
+  ,("an E element, last child of its parent",
+    "E:last-child","E:last-child")
+  ,("an E element, first sibling of its type",
+    "E:first-of-type","E:first-of-type")
+  ,("an E element, last sibling of its type",
+    "E:last-of-type","E:last-of-type")
+  ,("an E element, only child of its parent",
+    "E:only-child","E:only-child")
+  ,("an E element, only sibling of its type",
+    "E:only-of-type","E:only-of-type")
+  ,("an E element that has no children (including text nodes)",
+    "E:empty","E:empty")
+  ,("An E element being the source anchor of a hyperlink of which the target is not yet visited",
+    "E:link", "E:link") 
+  ,("An E element being the source anchor of a hyperlink of which the target is already visited",
+    "E:visited", "E:visited") 
+  ,("an E element during active action",
+    "E:active","E:active")
+  ,("an E element during hover action",
+    "E:hover","E:hover")
+  ,("an E element during focus action",
+    "E:focus","E:focus")
+  ,("an E element being the target of the referring URI",
+    "E:target","E:target")
+  ,("an element of type E in language \"fr\" (the document language specifies how language is determined)",
+    "E:lang(fr)","E:lang(fr)")
+  ,("a user interface element E which is enabled",
+    "E:enabled", "E:enabled")
+  ,("a user interface element E which is disabled",
+    "E:disabled","E:disabled")
+  ,("a user interface element E which is checked (for instance a radio-button or checkbox)",
+    "E:checked","E:checked")
+  ,("the first formatted line of an E element",
+    "E::first-line","E:first-line")
+  ,("the first formatted letter of an E element",
+    "E::first-letter","E:first-letter")
+  ,("generated content before an E element",
+    "E::before","E:before")
+  ,("generated content after an E element",
+    "E::after","E:after")
+  ,("an E element whose class is \"warning\" (the document language specifies how class is determined).",
+    "E.warning","E.warning")
+  ,("an E element with ID equal to \"myid\".",
+    "E#myid","E#myid")
+  ,("an E element that does not match simple selector s",
+    "E:not(s)","E:not(s)")
+  ,("An F element descendant of an E element",
+    "E F", "E F")
+  ,("An F element child of an E element",
+    "E > F", "E>F")
+  ,("An F element immediately preceded by an E element",
+    "E + F", "E+F")
+  ,("An F element preceded by an E element",
+    "E ~ F", "E~F")
+  ]
+-}
+
+--stylesheetParserTests :: Spec
+--stylesheetParserTests =
+--  describe "stylesheet parser tests" $
+--    mapM_ (matchSpecWithDesc stylesheet) stylesheetTestInfo
+
+--stylesheetTestInfo :: [(String, Text, Text)]
+--stylesheetTestInfo = 
+
+spec :: Spec
+spec = do declarationParserTests
+          selectorParserTests
+          declarationParsersTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Parser/ValueSpec.hs b/tests/Hasmin/Parser/ValueSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Parser/ValueSpec.hs
@@ -0,0 +1,120 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Parser.ValueSpec where
+
+import Test.Hspec
+-- import Test.QuickCheck
+-- import Hasmin.Parser.Internal
+
+-- import Test.Hspec.Attoparsec (shouldParse, parseSatisfies, (~>))
+import Data.Text (Text)
+-- import Data.Attoparsec.Text (Parser)
+import Hasmin.Parser.Value
+import Hasmin.TestUtils
+
+valueParserTest :: Spec
+valueParserTest = 
+  describe "value parser tests" $ do
+    describe "dimension parsing  tests" $
+      mapM_ (matchSpecWithDesc value) dimensionTestsInfo
+    describe "<gradient> parsing  tests" $
+      mapM_ (matchSpecWithDesc value) gradientTestsInfo
+    describe "<transform-function> parsing  tests" $
+      mapM_ (matchSpec value) transformFunctionParserTestsInfo
+      
+dimensionTestsInfo :: [(String, Text, Text)]
+dimensionTestsInfo =
+  [("parse px",   "-1px",   "-1px")
+  ,("parse em",   "+1em",   "1em")
+  ,("parse ex",   ".1e3ex", "100ex")
+  ,("parse ch",   "2ch",    "2ch")
+  ,("parse vh",   "1.3vh",  "1.3vh")
+  ,("parse vw",   "1.98vw", "1.98vw")
+  ,("parse vmin", "3vmin",  "3vmin")
+  ,("parse vmax", "4vmax",  "4vmax")
+  ,("parse rem",  "0.8rem", ".8rem")
+  ,("parse q",    "10q",    "10q")
+  ,("parse cm",   "7cm",    "7cm")
+  ,("parse mm",   "6mm",    "6mm")
+  ,("parse in",   "1in",    "1in")
+  ,("parse pc",   "1em",    "1em")
+  ,("parse pt",   "1pt",    "1pt")
+  ,("parse px",   "1px",    "1px")
+  ,("parse deg",  "1deg",   "1deg")
+  ,("parse grad", "1grad",  "1grad")
+  ,("parse rad",  "1rad",   "1rad")
+  ,("parse turn", "1turn",  "1turn")
+  ,("parse s",    "1s",     "1s")
+  ,("parse ms",   "1ms",    "1ms")
+  ,("parse hz",   "1hz",    "1hz")
+  ,("parse khz",  "1khz",   "1khz")
+  ,("parse dpi",  "1dpi",   "1dpi")
+  ,("parse dpcm", "1dpcm",  "1dpcm")
+  ,("parse dppx", "1dppx",  "1dppx")
+  ,("parse %",    "25e-1%", "2.5%")
+  ]
+
+numberParserTests :: Spec
+numberParserTests =
+  describe "number parser tests. Succeeds in parsing:" $
+    mapM_ (matchSpecWithDesc number) numberTestsInfo
+
+numberTestsInfo :: [(String, Text, Text)]
+numberTestsInfo = 
+  [("zero", "0", "0")
+  ,("floating point zero",
+    "0.0", "0")
+  ,("zero with a leading +",
+     "+0.0", "0")
+  ,("zero with a leading -", -- it is a valid value!
+     "-0.0", "0")
+  ,("a simple case of scientific notation",
+     "10e3", "10000")
+  ,("a complex case of scientific notation",
+     "-3.4e-2", "-.034")
+  ,("number starting with a dot",
+     ".60", ".6")
+  ,("a positive integer without leading +",
+    "12", "12")
+  ,("a positive integer with a leading +",
+    "+123", "123")
+  ]
+
+gradientTestsInfo :: [(String, Text, Text)]
+gradientTestsInfo =
+  [("linear-gradient() with default <side-or-corner> value",
+    "linear-gradient(to bottom, red, green)","linear-gradient(to bottom,red,green)")
+  ,("linear-gradient() with default <angle> value", 
+    "linear-gradient(180deg, #000, white)", "linear-gradient(180deg,#000,white)")
+  ,("linear-gradient() ommiting default value", 
+    "linear-gradient(rgb(0,0,0), hsl(300,100%,25%))", "linear-gradient(rgb(0,0,0),hsl(300,100%,25%))")
+  ,("linear-gradient() with comments in-between", 
+    "linear-gradient(/**/pink/**/,/**/peru/**/)", "linear-gradient(pink,peru)")
+  ]
+
+transformFunctionParserTestsInfo :: [(Text, Text)]
+transformFunctionParserTestsInfo =
+  [("skew( 0deg )",               "skew(0)")
+  ,("skew(23GRAD /**/,/**/0deg)", "skew(23grad,0)")
+  ,("sKeWY(/**/1rad)",            "skewy(1rad)")
+  ,("SkeWx(0.2turn/**/)",         "skewx(.2turn)")
+  ,("SkeWx(0.2turn)",             "skewx(.2turn)")
+  ,("SkeWx(0.2turn)",             "skewx(.2turn)")
+  ,("translate(0% )",             "translate(0%)")
+  ,("translate( 0 )",             "translate(0)")
+  ,("translate( 1in )",           "translate(1in)")
+  ,("translate(0%, 0%)",          "translate(0%,0%)")
+  ,("translate(0, 0%)",           "translate(0,0%)")
+  ,("translate(0%, 0)",           "translate(0%,0)")
+  ,("translate(0, 0)",            "translate(0,0)")
+  ,("translate(10px, 3em)",       "translate(10px,3em)")
+  ,("translateX(0%)",             "translatex(0%)")
+  ,("translateX(/* 3q */ 4q)",    "translatex(4q)")
+  ]
+
+spec :: Spec
+spec = do numberParserTests
+          valueParserTest
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/SelectorSpec.hs b/tests/Hasmin/SelectorSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/SelectorSpec.hs
@@ -0,0 +1,58 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.SelectorSpec where
+
+import Test.Hspec
+import Hasmin.Parser.Internal
+import Hasmin.TestUtils
+
+import Control.Monad.Reader (runReader)
+import Data.Text (Text)
+import Hasmin.Types.Class
+import Hasmin.Config
+
+anplusbMinificationTests :: Spec
+anplusbMinificationTests =
+    describe "<an+b> minification tests" $
+      mapM_ (matchSpec (f <$> selector)) anplusbMinificationTestsInfo
+  where f x = runReader (minifyWith x) defaultConfig
+
+anplusbMinificationTestsInfo :: [(Text, Text)]
+anplusbMinificationTestsInfo = 
+  [(":nth-child(-1n)",      ":nth-child(-n)")
+  ,(":nth-child(-n)",       ":nth-child(-n)")
+  ,(":nth-child(+n)",       ":nth-child(n)")
+  ,(":nth-child(+1n)",      ":nth-child(n)")
+  ,(":nth-child( even )",   ":nth-child(2n)")
+  ,(":nth-child( 2n - 2 )", ":nth-child(2n)")
+  ,(":nth-child( 2n - 4 )", ":nth-child(2n)")
+  ,(":nth-child( 2n + 1 )", ":nth-child(odd)")
+  ,(":nth-child( 2n - 1 )", ":nth-child(odd)")
+  ,(":nth-child( 2n - 3 )", ":nth-child(odd)")
+  ,(":nth-child(0n)",       ":nth-child(0)")
+  ,(":nth-child(0n+0)",     ":nth-child(0)")
+  ,(":nth-child(0n-0)",     ":nth-child(0)")
+  ,(":nth-child(0n-1)",     ":nth-child(-1)")
+  ,(":nth-child(1n+0)",     ":nth-child(n)")
+  ,(":nth-child(1n-0)",     ":nth-child(n)")
+  ]
+
+attributeQuoteRemovalTest :: Spec
+attributeQuoteRemovalTest =
+    describe "attribute quote removal tests" $
+      mapM_ (matchSpec (f <$> selector)) attributeQuoteRemovalTestInfo
+  where f x = runReader (minifyWith x) defaultConfig
+
+attributeQuoteRemovalTestInfo :: [(Text, Text)]
+attributeQuoteRemovalTestInfo =
+  [("[type=\"remove-double-quotes\"]", "[type=remove-double-quotes]")
+  ,("[type='remove-single-quotes']",   "[type=remove-single-quotes]")
+  ,("[a^='\"']", "[a^='\"']")
+  ]
+
+spec :: Spec
+spec = do anplusbMinificationTests
+          attributeQuoteRemovalTest
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/TestUtils.hs b/tests/Hasmin/TestUtils.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/TestUtils.hs
@@ -0,0 +1,28 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.TestUtils where
+
+import Test.Hspec
+
+import Data.Text (Text, unpack, singleton)
+import Test.Hspec.Attoparsec (parseSatisfies, (~>))
+import Data.Attoparsec.Text (Parser)
+import Hasmin.Types.Class
+import Hasmin.Types.Declaration
+import Hasmin.Utils
+
+-- Given a parser and a 3-tuple, prints a test description,
+-- applies the parser, and compares its result with the expected result
+matchSpecWithDesc :: ToText a => Parser a -> (String, Text, Text) -> Spec
+matchSpecWithDesc parser (description, textToParse, expectedResult) = 
+  it description $
+    (toText <$> (textToParse ~> parser)) `parseSatisfies` (== expectedResult)
+
+matchSpec :: ToText a => Parser a -> (Text, Text) -> Spec
+matchSpec parser (textToParse, expectedResult) =
+  it (unpack textToParse) $ (toText <$> (textToParse ~> parser)) `parseSatisfies` (== expectedResult)
+
+data Declarations = Declarations [Declaration]
+instance ToText Declarations where
+  toText (Declarations ds) = mconcatIntersperse toText (singleton ';') ds
+
diff --git a/tests/Hasmin/Types/ColorSpec.hs b/tests/Hasmin/Types/ColorSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/ColorSpec.hs
@@ -0,0 +1,155 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.ColorSpec where
+
+import Test.Hspec
+import Test.QuickCheck
+import Hasmin.Parser.Value
+import Hasmin.TestUtils
+
+import Test.Hspec.Attoparsec (parseSatisfies, (~>))
+import Data.Text (Text)
+import Data.Foldable
+import Data.Maybe (fromJust)
+import Control.Monad
+import Hasmin.Types.Color
+import Hasmin.Types.Class
+import Hasmin.Types.Numeric
+
+instance Arbitrary Color where
+  arbitrary = oneof [ fmap   (fromJust . mkNamed) colorKeyword
+                    , liftM3 mkHex3 hexChar hexChar hexChar
+                    , liftM3 mkHex6 hexString hexString hexString
+                    , liftM4 mkHex4 hexChar hexChar hexChar hexChar
+                    , liftM4 mkHex8 hexString hexString hexString hexString
+                    , liftM3 mkRGBInt intRange intRange intRange
+                    , liftM3 mkRGBPer ratRange ratRange ratRange 
+                    , liftM4 mkRGBAInt intRange intRange intRange alphaRange
+                    , liftM4 mkRGBAPer ratRange ratRange ratRange alphaRange 
+                    , liftM3 mkHSL hueRange ratRange ratRange
+                    , liftM4 mkHSLA hueRange ratRange ratRange alphaRange 
+                    ]
+    where intRange   = choose (0, 255)
+          ratRange   = toPercentage <$> (choose (0, 100) :: Gen Float)
+          alphaRange = toAlphavalue <$> (choose (0, 1) :: Gen Float)
+          hueRange   = choose (0, 360)
+
+-- | Generates color keywords uniformly distributed 
+colorKeyword :: Gen Text
+colorKeyword = do
+  index <- choose (0, len)
+  pure $ keywords !! index
+  where len      = length keywordColors - 1
+        keywords = fmap fst keywordColors
+
+-- | Generates a hexadecimal character uniformly distributed 
+hexChar :: Gen Char
+hexChar = (hexadecimals !!) <$> choose (0, length hexadecimals - 1)
+
+hexString :: Gen String
+hexString = (\x y -> [x,y]) <$> hexChar <*> hexChar
+
+-- | Check that a color is equivalent to their minified representation form
+prop_minificationEq :: Color -> Bool
+prop_minificationEq c = c == minifyColor c
+
+colorTests :: Spec
+colorTests =
+  describe "Color datatype tests" $
+    it "color minify semantically equivalent" $
+      quickCheck prop_minificationEq
+
+colorParserTests :: Spec
+colorParserTests =
+  describe "Color Parser tests" $
+    it "succeeds in parsing and minifying different red color representations" $
+      traverse_ ((`parseSatisfies` (==(fromJust $ mkNamed "red"))) . (~> (minifyColor <$> color))) redColor
+
+colorParserSpacesAndCommentsTests :: Spec
+colorParserSpacesAndCommentsTests =
+  describe "Color Parser test" $
+    it "succeeds in parsing different yellow color representations with spaces and comments in-between" $
+      traverse_ ((`parseSatisfies` (==(fromJust $ mkNamed "yellow"))) . (~> color)) commentsAndSpacesInColors
+
+-- | Multiple equivalent red color representations
+redColor :: [Text]
+redColor = 
+  [ "red" 
+  , "#f00", "#F00", "#ff0000", "#fF0000", "#Ff0000", "#FF0000"
+  , "rgb(255,0,0)" , "rgb(100%,0%,0%)"
+  , "rgba(255,0,0,1)", "rgba(255,0,0,1.0)"
+  , "rgba(100%,0%,0%,1)", "rgba(100%,0%,0%,1.0)"
+  , "hsl(0,100%,50%)", "hsl(360,100%,50%)"
+  , "hsla(0,100%,50%,1)", "hsla(0,100%,50%,1.0)"
+  , "hsla(360,100%,50%,1)", "hsla(360,100%,50%,1.0)"
+  ]
+
+-- | All 4096 (16^3) possible 3 char shorthands
+allHex3 :: [String]
+allHex3 = replicateM 3 hexadecimals
+
+hexadecimals :: String
+hexadecimals = "0123456789abcdef" 
+
+-- Every color is yellow
+commentsAndSpacesInColors :: [Text]
+commentsAndSpacesInColors = 
+  [ "rgb(/**/255,255,0)"
+  , "rgb(255,/**/255,0)"
+  , "rgb(255,255,/**/0)"
+  , "rgb(255/**/,255,0)"
+  , "rgb(255,255/**/,0)"
+  , "rgb(255,255,0/**/)"
+  , "rgb(/* */255/* */,/* */255/* */,/* */0/* */)"
+  , "rgb( /* */ 255 /* */ , /* */ 255 /* */ , /* */ 0 /* */ )"
+  , "rgba(/**/255,255,0,1)"
+  , "rgba(255,/**/255,0,1)"
+  , "rgba(255,255,/**/0,1)"
+  , "rgba(255,255,0,/**/1)"
+  , "rgba(255/**/,255,0,1)"
+  , "rgba(255,255/**/,0,1)"
+  , "rgba(255,255,0/**/,1)"
+  , "rgba(255,255,0,1/**/)"
+  , "rgba(/* */255/* */,/* */255/* */,/* */0/* */,/* */1/* */)"
+  , "rgba( /* */ 255 /* */ , /* */ 255 /* */ , /* */ 0 /* */ , /* */ 1 /* */)"
+  , "hsl(/**/60,100%,50%)"
+  , "hsl(60,/**/100%,50%)"
+  , "hsl(60,100%,/**/50%)"
+  , "hsl(60/**/,100%,50%)"
+  , "hsl(60,100%/**/,50%)"
+  , "hsl(60,100%,50%/**/)"
+  , "hsl(/* */60/* */,/* */100%/* */,/* */50%/* */)"
+  , "hsl( /* */ 60 /* */ , /* */ 100% /* */ , /* */ 50% /* */ )"
+  , "hsla(/**/60,100%,50%,1)"
+  , "hsla(60,/**/100%,50%,1)"
+  , "hsla(60,100%,/**/50%,1)"
+  , "hsla(60,100%,50%,/**/1)"
+  , "hsla(60/**/,100%,50%,1)"
+  , "hsla(60,100%/**/,50%,1)"
+  , "hsla(60,100%,50%/**/,1)"
+  , "hsla(60,100%,50%,1/**/)"
+  , "hsla(/* */60/* */,/* */100%/* */,/* */50%/* */,/* */1/* */)"
+  , "hsla( /* */ 60 /* */ , /* */ 100% /* */ , /* */ 50% /* */ , /* */ 1 /* */)"
+  ]
+
+colorMinificationTests :: Spec
+colorMinificationTests = 
+    describe "color minification" $
+      mapM_ (matchSpec f) colorMinificationTestsInfo
+  where f = minify <$> color
+
+colorMinificationTestsInfo :: [(Text, Text)]
+colorMinificationTestsInfo =
+  [ ("rgba(0,0,0,.4)", "#0006")
+  , ("hsla(0,0%,0%,.4)", "#0006")
+  , ("#00000066", "#0006")
+  ]
+
+spec :: Spec
+spec = do colorTests 
+          colorParserTests
+          colorMinificationTests
+          colorParserSpacesAndCommentsTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/DeclarationSpec.hs b/tests/Hasmin/Types/DeclarationSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/DeclarationSpec.hs
@@ -0,0 +1,446 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.DeclarationSpec where
+
+import Test.Hspec
+import Hasmin.Parser.Internal
+import Hasmin.TestUtils
+
+import Data.Text (Text)
+import Data.Monoid ((<>))
+import Data.Foldable (traverse_)
+import Hasmin.Types.Class
+import Hasmin.Types.Declaration
+
+declarationTests :: Spec
+declarationTests =
+    describe "declaration" $
+      mapM_ (matchSpec declaration) declarationTestsInfo
+
+propertySpecificTests :: Spec
+propertySpecificTests =
+    describe "property specific changes" $
+      mapM_ (matchSpec f) propertySpecificTestsInfo
+  where f = minify <$> declaration
+
+propertySpecificTestsInfo :: [(Text, Text)] 
+propertySpecificTestsInfo = 
+  [("font-family:Arial Black Long Name", "font-family:arial black long name")
+  ,("font:9px Arial Black Long Name", "font:9px arial black long name")
+  ,("font-family:\"SANS-SERIF\"", "font-family:\"sans-serif\"")
+  ,("font-family:'Helvetica','Arial',sans-serif", "font-family:helvetica,arial,sans-serif")
+  -- TODO make this work
+  -- should fallback and parse till ';'
+  -- ,("font-size:1em\nline-height:1.38;", "font-size:1em\nline-height:1.38;")
+
+  ,("background-size:1px auto, 2px auto", "background-size:1px,2px")
+
+  ,("text-shadow: 1px 1px 1px #ff0000", "text-shadow:1px 1px 1px red")
+  ,("text-shadow: 12px 12px 12px", "text-shadow:9pt 9pt 9pt")
+  ,("text-shadow: 1px 1px 0 green", "text-shadow:1px 1px green")
+  ,("text-shadow: 1px 1px 0 green, 2px 2px 0 blue", "text-shadow:1px 1px green,2px 2px blue")
+
+  ,("font-weight:normal", "font-weight:400")
+  ,("font-weight:bold",   "font-weight:700")
+
+  ,("word-spacing:normal",  "word-spacing:0")
+  ,("word-spacing:initial", "word-spacing:0")
+  ,("word-spacing:unset",   "word-spacing:unset")
+  ,("word-spacing:inherit", "word-spacing:unset")
+
+  ,("vertical-align:baseline", "vertical-align:0")
+  ,("vertical-align:initial",  "vertical-align:0")
+  ,("vertical-align:unset",    "vertical-align:0")
+
+  ,("transform-origin:left top", "transform-origin:0 0")
+  ,("transform-origin:left 0",   "transform-origin:0 0")
+  ,("transform-origin:0 top",    "transform-origin:0 0")
+  ,("transform-origin:0 0",      "transform-origin:0 0")
+  ,("transform-origin:top left", "transform-origin:0 0")
+  ,("transform-origin:top 0",    "transform-origin:0 0")
+  ,("transform-origin:0 left",   "transform-origin:0 0")
+
+  ,("transform-origin:left bottom", "transform-origin:0 100%")
+  ,("transform-origin:0 bottom",    "transform-origin:0 100%")
+  ,("transform-origin:left 100%",   "transform-origin:0 100%")
+  ,("transform-origin:0 100%",      "transform-origin:0 100%")
+  ,("transform-origin:bottom left", "transform-origin:0 100%")
+  ,("transform-origin:100% left",   "transform-origin:0 100%")
+  ,("transform-origin:bottom 0",    "transform-origin:0 100%")
+
+  ,("transform-origin:right top", "transform-origin:100% 0")
+  ,("transform-origin:100% top",  "transform-origin:100% 0")
+  ,("transform-origin:right 0",   "transform-origin:100% 0")
+  ,("transform-origin:top 100%",  "transform-origin:100% 0")
+  ,("transform-origin:0% right",  "transform-origin:100% 0")
+  ,("transform-origin:top right", "transform-origin:100% 0")
+
+  ,("transform-origin:right bottom", "transform-origin:100% 100%")
+  ,("transform-origin:100% bottom",  "transform-origin:100% 100%")
+  ,("transform-origin:right 100%",   "transform-origin:100% 100%")
+  ,("transform-origin:bottom 100%",  "transform-origin:100% 100%")
+  ,("transform-origin:100% right",   "transform-origin:100% 100%")
+  ,("transform-origin:bottom right", "transform-origin:100% 100%")
+
+  ,("transform-origin:center bottom", "transform-origin:bottom")
+  ,("transform-origin:50% bottom",    "transform-origin:bottom")
+  ,("transform-origin:center 100%",   "transform-origin:bottom")
+  ,("transform-origin:bottom 50%",    "transform-origin:bottom")
+  ,("transform-origin:bottom center", "transform-origin:bottom")
+
+  ,("transform-origin:center top", "transform-origin:top")
+  ,("transform-origin:50% top",    "transform-origin:top")
+  ,("transform-origin:center 0px", "transform-origin:top")
+  ,("transform-origin:top 50%",    "transform-origin:top")
+  ,("transform-origin:top center", "transform-origin:top")
+
+  ,("transform-origin:center right", "transform-origin:100%")
+  ,("transform-origin:50% right",    "transform-origin:100%")
+  ,("transform-origin:right 50%",    "transform-origin:100%")
+  ,("transform-origin:100% center",  "transform-origin:100%")
+  ,("transform-origin:right center", "transform-origin:100%")
+
+  ,("transform-origin:center left", "transform-origin:0")
+  ,("transform-origin:50% left",    "transform-origin:0")
+  ,("transform-origin:left 50%",    "transform-origin:0")
+  ,("transform-origin:0% center",   "transform-origin:0")
+  ,("transform-origin:left center", "transform-origin:0")
+
+  ,("transform-origin:center",            "transform-origin:50%")
+  ,("transform-origin:center center",     "transform-origin:50%")
+  ,("transform-origin:center 50%",        "transform-origin:50%")
+  ,("transform-origin:50% center",        "transform-origin:50%")
+  ,("transform-origin:50% 50%",           "transform-origin:50%")
+  ,("transform-origin:center center 0",   "transform-origin:50%")
+  ,("transform-origin:center center 1px", "transform-origin:50% 50% 1px")
+
+  ,("transform-origin:left 30px",   "transform-origin:0 30px")
+  ,("transform-origin:30px left",   "transform-origin:0 30px")
+  ,("transform-origin:right 30px",  "transform-origin:100% 30px")
+  ,("transform-origin:30px right",  "transform-origin:100% 30px")
+  ,("transform-origin:bottom 30px", "transform-origin:30px 100%")
+  ,("transform-origin:30px bottom", "transform-origin:30px 100%")
+  ,("transform-origin:top 30px",    "transform-origin:30px 0")
+  ,("transform-origin:30px top",    "transform-origin:30px 0")
+  ,("transform-origin:center 30px", "transform-origin:50% 30px")
+  ,("transform-origin:30px center", "transform-origin:30px")
+
+  -- 3 value syntax
+  ,("transform-origin:left top 1px",        "transform-origin:0 0 1px")
+  ,("transform-origin:top left 1px",        "transform-origin:0 0 1px")
+  ,("transform-origin:left bottom 1px",     "transform-origin:0 100% 1px")
+  ,("transform-origin:bottom left 1px",     "transform-origin:0 100% 1px")
+  ,("transform-origin:right top 1px",       "transform-origin:100% 0 1px")
+  ,("transform-origin:top right 1px",       "transform-origin:100% 0 1px")
+  ,("transform-origin:right bottom 1px",    "transform-origin:100% 100% 1px")
+  ,("transform-origin:bottom right 1px",    "transform-origin:100% 100% 1px")
+  ,("transform-origin:center bottom 1px",   "transform-origin:50% 100% 1px")
+  ,("transform-origin:bottom center 1px",   "transform-origin:50% 100% 1px")
+  ,("transform-origin:center top 1px",      "transform-origin:50% 0 1px")
+  ,("transform-origin:top center 1px",      "transform-origin:50% 0 1px")
+  ,("transform-origin:center right 1px",    "transform-origin:100% 50% 1px")
+  ,("transform-origin:right center 1px",    "transform-origin:100% 50% 1px")
+  ,("transform-origin:center left 1px",     "transform-origin:0 50% 1px")
+  ,("transform-origin:left center 1px",     "transform-origin:0 50% 1px")
+  ]
+
+
+declarationTestsInfo :: [(Text, Text)]
+declarationTestsInfo = 
+  [("box-shadow:0 7px 0 #fefefe, 0 14px 0 #fefefe",
+    "box-shadow:0 7px 0 #fefefe,0 14px 0 #fefefe")
+  ]
+
+cleaningTests :: Spec
+cleaningTests = 
+    describe "cleaning function" $
+      mapM_ (matchSpecWithDesc f) cleaningTestsInfo
+  where f = (Declarations . clean) <$> declarations
+
+cleaningTestsInfo :: [(String, Text, Text)]
+cleaningTestsInfo =
+  [("Merge margin with later margin-top",
+        "margin:0;margin-top:5px",
+        "margin:5px 0 0")
+  ,("Merge margin with later margin-right",
+        "margin:2px;margin-right:1in",
+        "margin:2px 1in 2px 2px")
+  ,("Merge margin with later margin-bottom",
+        "margin:auto;margin-bottom:.2em",
+        "margin:auto auto .2em")
+  ,("Merge margin with later margin-left",
+        "margin:fill;margin-left:9px",
+        "margin:fill fill fill 9px")
+  ,("Merge margin with later margin-top, with declaration in-between",
+        "margin:0;color:red;margin-top:5px",
+        "margin:5px 0 0;color:red")
+  ,("Merge margin with later margin-right, with declaration in-between",
+        "margin:2px;color:red;margin-right:1in",
+        "margin:2px 1in 2px 2px;color:red")
+  ,("Merge margin with later margin-bottom, with declaration in-between",
+        "margin:auto;color:red;margin-bottom:.2em",
+        "margin:auto auto .2em;color:red")
+  ,("Merge margin with later margin-left, with declaration in-between",
+        "margin:fill;color:red;margin-left:9px",
+        "margin:fill fill fill 9px;color:red")
+  ,("Drop a margin-top overwritten by a following margin",
+        "margin-top:5px;margin:0",
+        "margin:0")
+  ,("Drop a margin-right overwritten by a following margin",
+        "margin-right:1px;margin:2px",
+        "margin:2px")
+  ,("Drop a margin-bottom overwritten by a following margin",
+        "margin-bottom:.2em;margin:auto",
+        "margin:auto")
+  ,("Drop a margin-left overwritten by a following margin",
+        "margin-left:9px;margin:fill",
+        "margin:fill")
+  ,("Drop a margin-top overwritten by later margin, with a declaration in-between",
+        "margin-top:5px;padding:0;margin:0",
+        "padding:0;margin:0")
+  ,("Drop a margin-right overwritten by later margin, with a declaration in-between",
+        "margin-right:1px;padding:0;margin:2px",
+        "padding:0;margin:2px")
+  ,("Drop a margin-bottom overwritten by later margin, with a declaration in-between",
+        "margin-bottom:.2em;padding:0;margin:auto",
+        "padding:0;margin:auto")
+  ,("Drop a margin-left overwritten by later margin, with a declaration in-between",
+        "margin-left:9px;padding:0;margin:fill",
+        "padding:0;margin:fill")
+  ,("Drop a margin-top overwritten by a later margin-top, with a declaration in-between",
+        "margin-top:99px;color:red;margin-top:0",
+        "color:red;margin-top:0")
+  ,("Drop a margin-right overwritten by a later margin-right, with a declaration in-between",
+        "margin-right:99px;color:red;margin-right:0",
+        "color:red;margin-right:0")
+  ,("Drop a margin-bottom overwritten by a later margin-bottom, with a declaration in-between",
+        "margin-bottom:99px;color:red;margin-bottom:0",
+        "color:red;margin-bottom:0")
+  ,("Drop a margin-left overwritten by a later margin-left, with a declaration in-between",
+        "margin-left:99px;color:red;margin-left:0",
+        "color:red;margin-left:0")
+  ,("Drop a margin-top overwritten by a previous margin-top with !important",
+        "margin-top:99px!important;margin-top:0",
+        "margin-top:99px!important")
+  ,("Drop a margin-right overwritten by a previous margin-right with !important",
+        "margin-right:99px!important;margin-right:0",
+        "margin-right:99px!important")
+  ,("Drop a margin-bottom overwritten by a previous margin-bottom with !important",
+        "margin-bottom:99px!important;margin-bottom:0",
+        "margin-bottom:99px!important")
+  ,("Drop a margin-left overwritten by a previous margin-left with !important",
+        "margin-left:99px!important;margin-left:0",
+        "margin-left:99px!important")
+  ,("Drop a margin-top overwritten by a previous margin-top with !important, with a declaration in-between",
+        "margin-top:99px!important;margin:auto;margin-top:0",
+        "margin-top:99px!important;margin:auto")
+  ,("Drop a margin-right overwritten by a previous margin-right with !important, with a declaration in-between",
+        "margin-right:99px!important;margin:auto;margin-right:0",
+        "margin-right:99px!important;margin:auto")
+  ,("Drop a margin-bottom overwritten by a previous margin-bottom with !important, with a declaration in-between",
+        "margin-bottom:99px!important;margin:auto;margin-bottom:0",
+        "margin-bottom:99px!important;margin:auto")
+  ,("Drop a margin-left overwritten by a previous margin-left with !important, with a declaration in-between",
+        "margin-left:99px!important;margin:auto;margin-left:0",
+        "margin-left:99px!important;margin:auto")
+  -- ,("When merging, translate initial keyword to the initial value",
+        -- "margin:0;margin-top:initial",
+        -- "margin:0")
+  -- ,("When merging, translate unset keyword of a property that inherits to the initial value",
+        -- "margin:0;margin-top:unset",
+        -- "margin:0")
+  -- ,("Don't merge properties with inherit keyword",
+        -- "margin:0;margin-top:inherit",
+        -- "margin:0;margin-top:inherit")
+  ]
+
+minifyDecTests :: Spec
+minifyDecTests = do
+    describe "minifyDec function" $
+      mapM_ (matchSpec f) minifyDecTestsInfo
+  where f = minify <$> declaration
+
+shorthandInitialValuesTests :: Spec
+shorthandInitialValuesTests = do
+    testMatches "background minification" backgroundTestsInfo
+    testMatches "transition minification" transitionTestsInfo
+    testMatches "animation minification" animationTestsInfo
+    testMatches "font minification" fontTestsInfo
+    testMatches "outline minification" outlineTestsInfo
+    anyOrderShorthandTests 
+  where testMatches a i = describe a $ mapM_ (matchSpecWithDesc (minify <$> declaration)) i
+
+backgroundTestsInfo :: [(String, Text, Text)]
+backgroundTestsInfo = 
+  [("Does not remove <position> from background when the <bg-size> cannot be removed",
+    "background: 0 0 / 3px", "background:0 0/3px")
+  ,("Removes <position> when it is the default and no <bg-size> is present",
+    "background: #fff 0 0", "background:#fff")
+  ,("Removes both <position> and <bg-size> when they are the default",
+    "background: #fff 0 0/auto", "background:#fff")
+  ,("Removes background-clip keyword when it equals the background-origin keyword",
+    "background: border-box #fff border-box", "background:border-box #fff")
+  ,("Removes the keywords for background-origin and background-clip when both are the initial ones",
+    "background: #fff padding-box border-box", "background:#fff")
+  ,("Removes the background-attachment keyword when it is the initial one",
+    "background: #fff scroll", "background:#fff")
+  ,("Removes the background-color value when it is the initial one",
+    "background: url(img.png) transparent", "background:url(img.png)")
+  ,("Removes background-image when it is the default (i.e. none)",
+    "background: none #fff", "background:#fff")
+  ,("Removes background-repeat when it is the default (i.e. repeat)",
+    "background: repeat #fff", "background:#fff")
+  ]
+
+transitionTestsInfo :: [(String, Text, Text)]
+transitionTestsInfo = 
+  [("Removes transition-duration when it is the default",
+    "transition: ease-out some-property 0s", "transition:ease-out some-property")
+  ,("Removes transition-delay when it is the default",
+    "transition: ease-out 1s some-property 0s", "transition:ease-out 1s some-property")
+  ,("Leaves an initial (i.e. 0s) transition-duration when the transition-delay is not initial (i.e. also 0s)",
+    "transition: all 0s 1s ease", "transition:0s 1s")
+  ,("Removes transition-timing-function when it is the default",
+    "transition: some-property 1s ease", "transition:1s some-property")
+  ,("Removes transition-property when it is the default",
+    "transition: ease-out all 1s", "transition:ease-out 1s")
+  ,("Leaves only 0s when every present value is initial",
+    "transition: all 0s 0s ease", "transition:0s")
+  ]
+
+animationTestsInfo :: [(String, Text, Text)]
+animationTestsInfo = 
+  [("Removes default animation-fill-mode when the animation-name doesn't overlap with its keywords",
+    "animation:none animName", "animation:animName")
+  ,("Leaves default animation-fill-mode when the animation-name overlaps with one of its keywords",
+    "animation:none backwards", "animation:none backwards")
+  ,("Removes default animation-fill-mode and default animation-name when both are used simultaneously",
+    "animation:ease-out none none", "animation:ease-out")
+  ,("Removes default animation-timing-function",
+    "animation:animName ease", "animation:animName")
+  ,("Removes default animation-duration",
+    "animation:0s ease-out", "animation:ease-out")
+  ,("Removes default animation-delay",
+    "animation:1s 0s ease-out", "animation:1s ease-out")
+  ,("Leaves default animation-duration when animation-delay is not the default",
+    "animation:0s 1s ease-out", "animation:0s 1s ease-out")
+  ,("Removes default animation-iteration-count",
+    "animation:1 ease-out", "animation:ease-out")
+  ,("Removes default animation-direction when the animation-name doesn't overlap with its keywords",
+    "animation:normal animName", "animation:animName")
+  ,("Leaves default animation-direction when the animation-name overlaps with one of its keywords",
+    "animation:normal alternate", "animation:normal alternate")
+  ,("Removes default animation-play-state when the animation-name doesn't overlap with its keywords",
+    "animation:running animName", "animation:animName")
+  ,("Leaves default animation-play-state when the animation-name overlaps with one of its keywords",
+    "animation:running paused", "animation:running paused")
+  ]
+
+fontTestsInfo :: [(String, Text, Text)]
+fontTestsInfo = 
+  [("Removes default font-style",
+    "font:normal condensed bolder small-caps 9px/1 sans", "font:small-caps bolder condensed 9px/1 sans")
+  ,("Removes default font-variant",
+    "font:normal italic condensed bolder 9px/1 sans", "font:italic bolder condensed 9px/1 sans")
+  ,("Removes default font-weight",
+    "font:italic normal condensed small-caps 9px/1 sans", "font:italic small-caps condensed 9px/1 sans")
+  ,("Converts bold font-weight into 700",
+    "font:bold 9px sans", "font:700 9px sans")
+  ,("Removes default font-stretch",
+    "font:italic normal small-caps bolder 9px/1 sans", "font:italic small-caps bolder 9px/1 sans")
+  ,("Removes default line-height",
+    "font:9px/normal sans", "font:9px sans")
+  ,("Lowercases and removes quotes from font-family values",
+    "font:9px 'Arial Black', \"sans-serif\", sans", "font:9px arial black,\"sans-serif\",sans")
+  ]
+
+-- Tests for the removal of outline initial values. Almost identical to those
+-- shorthands in the anyOrderShorthandTests, except for the invert color.
+outlineTestsInfo :: [(String, Text, Text)]
+outlineTestsInfo = 
+  [("Removes default outline-color",
+    "outline:invert solid 1px", "outline:solid 1px")
+  ,("Removes default outline-style",
+    "outline:#fff none 1px", "outline:#fff 1px")
+  ,("Removes default outline-width",
+    "outline:#fff solid medium", "outline:#fff solid")
+  ,("Leaves none as the shortest initial value",
+    "outline:invert", "outline:none")
+  ]
+
+-- Tests for shorthands whose initial values are "medium none currentColor",
+-- and can go on any order. 
+anyOrderShorthandTests :: Spec
+anyOrderShorthandTests = do
+    describe "Shorthands that accept values in any order (mostly: medium none currentColor)" $
+      traverse_ (mapM_ (matchSpec (minify <$> declaration))) d
+  where d = fmap f shorthandsToTest 
+        f z = fmap (\(x,y) -> (z <> ":" <> x, z <> ":" <> y)) shorthandEquivalences
+        shorthandsToTest = ["border"
+                           ,"border-top"
+                           ,"border-right"
+                           ,"border-bottom"
+                           ,"border-left"
+                           ,"column-rule"]
+        shorthandEquivalences = [("initial",                   "none")
+                                ,("unset",                     "none")
+                                ,("none",                      "none")
+                                ,("medium",                    "none")
+                                ,("currentColor",              "none")
+                                ,("medium none",               "none")
+                                ,("medium currentColor",       "none")
+                                ,("none currentColor",         "none")
+                                ,("medium none currentColor",  "none")
+                                ,("medium none red",           "red")
+                                ,("thick none currentColor",   "thick")
+                                ,("thick dotted currentColor", "thick dotted")]
+
+minifyDecTestsInfo :: [(Text, Text)]
+minifyDecTestsInfo =
+  [("BORDER-bottom: initial",                   "border-bottom:none")
+  ,("BORDER-bottom: unset",                     "border-bottom:none")
+  ,("BORDER-bottom: none",                      "border-bottom:none")
+  ,("BORDER-bottom: medium",                    "border-bottom:none")
+  ,("BORDER-bottom: currentColor",              "border-bottom:none")
+  ,("BORDER-bottom: medium none",               "border-bottom:none")
+  ,("BORDER-bottom: medium currentColor",       "border-bottom:none")
+  ,("BORDER-bottom: none currentColor",         "border-bottom:none")
+  ,("BORDER-bottom: medium none currentColor",  "border-bottom:none")
+  ,("BORDER-bottom: medium none red",           "border-bottom:red")
+  ,("BORDER-bottom: thick none currentColor",   "border-bottom:thick")
+  ,("BORDER-bottom: thick dotted currentColor", "border-bottom:thick dotted")
+  ,("BORDER-bottom: 12px red",                  "border-bottom:9pt red")
+-- widows inherits, and its initial value is 2
+  ,("widows: unset",   "widows:unset")
+  ,("widows: inherit", "widows:unset")
+  ,("widows: initial", "widows:2")
+-- font-synthesis inherits, and its initial value is "weight style",
+-- which are two keywords, both needed, in any order.
+  ,("font-synthesis: unset",        "font-synthesis:unset")
+  ,("font-synthesis: inherit",      "font-synthesis:unset")
+  ,("font-synthesis: initial",      "font-synthesis:initial")
+  ,("Font-synthesis: weight style", "font-synthesis:initial")
+  ,("font-synthesis: style weight", "font-synthesis:initial")
+  ,("font-synthesis: weight",       "font-synthesis:weight")
+  ,("font-synthesis: style",        "font-synthesis:style")
+-- background-size: The first value gives the width of the corresponding image,
+-- the second value its height. Its initial value is 'auto', and it doesn't
+-- inherit. If only one value is given the second is assumed to be ‘auto’.
+  ,("background-size: 96px",      "background-size:1in")
+  ,("background-size: auto",      "background-size:auto")
+  ,("background-size: unset",     "background-size:auto")
+  ,("background-size: initial",   "background-size:auto")
+  ,("background-size: auto auto", "background-size:auto")
+  ,("background-size: auto 96px", "background-size:auto 1in")
+  ,("background-size: 96px auto", "background-size:1in")
+  ]
+
+spec :: Spec
+spec = do cleaningTests 
+          minifyDecTests
+          shorthandInitialValuesTests 
+          declarationTests
+          propertySpecificTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/DimensionSpec.hs b/tests/Hasmin/Types/DimensionSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/DimensionSpec.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.DimensionSpec where
+
+import Test.Hspec
+--import Test.QuickCheck
+--import Hasmin.Parser.Internal hiding (property)
+
+--import Test.Hspec.Attoparsec (shouldParse, parseSatisfies, (~>))
+import Hasmin.Types.Dimension
+
+spec :: Spec
+spec = 
+  describe "<length> units equivalences" $ do
+    describe "inches conversions" $ do
+      it "1in == 2.54cm" $
+        (Distance 1 IN) `shouldBe` Distance 2.54 CM
+      it "1in == 25.4cm" $
+        (Distance 1 IN) `shouldBe` Distance 25.4 MM
+      it "1in == 101.6q" $
+        (Distance 1 IN) `shouldBe` Distance 101.6 Q
+      it "1in == 72pt" $
+        (Distance 1 IN) `shouldBe` Distance 72 PT
+      it "1in == 6pc" $
+        (Distance 1 IN) `shouldBe` Distance 6 PC
+      it "1in == 96px" $
+        (Distance 1 IN) `shouldBe` Distance 96 PX
+    describe "pixel conversions" $ do
+      it "48px == .5in" $
+        (Distance 48 PX) `shouldBe` Distance 0.5 IN
+      it "72px == 1.905cm" $
+        (Distance 72 PX) `shouldBe` Distance 1.905 CM
+      it "72px == 19.05mm" $
+        (Distance 72 PX) `shouldBe` Distance 19.05 MM 
+      it "60px == 63.5q" $
+        (Distance 60 PX) `shouldBe` Distance 63.5 Q
+      it "12px == 9pt" $
+        (Distance 12 PX) `shouldBe` Distance 9 PT
+      it "16px == 1pc" $
+        (Distance 16 PX) `shouldBe` Distance 1 PC
+    describe "centimeter conversions" $ do
+      it "5.08cm == 2in" $
+        (Distance 5.08 CM) `shouldBe` Distance 2 IN
+      it "1cm == 10mm" $
+        (Distance 1 CM) `shouldBe` Distance 10 MM 
+      it "2cm == 80q" $
+        (Distance 2 CM) `shouldBe` Distance 80 Q
+      it "1.27cm == 36pt" $
+        (Distance 1.27 CM) `shouldBe` Distance 36 PT
+      it "1.27cm == 3pc" $
+        (Distance 1.27 CM) `shouldBe` Distance 3 PC
+      it "1.27cm == 48px" $
+        (Distance 1.27 CM) `shouldBe` Distance 48 PX
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/FilterFunctionSpec.hs b/tests/Hasmin/Types/FilterFunctionSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/FilterFunctionSpec.hs
@@ -0,0 +1,56 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.FilterFunctionSpec where
+
+import Test.Hspec
+
+import Data.Text (Text)
+import Hasmin.Parser.Value
+import Hasmin.Types.Class
+import Hasmin.TestUtils
+
+filterTests :: Spec
+filterTests = 
+    describe "<filter-function> minification tests" $
+      mapM_ (matchSpecWithDesc f) filterTestsInfo
+  where f = minify <$> value
+      
+filterTestsInfo :: [(String, Text, Text)]
+filterTestsInfo =
+  [("Minifies the <color> value in drop-shadow()"
+     ,"drop-shadow(1px 1px 1px #ff0000)"
+     ,"drop-shadow(1px 1px 1px red)")
+  ,("Minifies the <length> values in drop-shadow()"
+     ,"drop-shadow(12px 12px 12px red)"
+     ,"drop-shadow(9pt 9pt 9pt red)")
+  ,("Removes 3rd value in drop-shadow() when it is 0"
+     ,"drop-shadow(1px 1px 0 red)"
+     ,"drop-shadow(1px 1px red)")
+  ,("Minifies <angle> in hue-rotate()"
+     ,"hue-rotate(100grad)"
+     ,"hue-rotate(90deg)")
+  ,("Minifies <length> in blur()"
+     ,"blur(12px)"
+     ,"blur(9pt)")
+  ,("Minifies 100% in contrast()"
+     ,"contrast(100%)"
+     ,"contrast(1)")
+  ,("Minifies 0% in contrast()"
+     ,"contrast(0%)"
+     ,"contrast(0)")
+  ,("Minifies <percentage> multiple of 10 in brightness()"
+     ,"brightness(50%)"
+     ,"brightness(.5)")
+  ,("Normalizes <percentage> to <number> when > 10 in contrast()"
+     ,"contrast(11%)"
+     ,"contrast(.11)")
+  ,("Does not minify <percentage> when > 0 and < 10 in contrast()"
+     ,"contrast(8%)"
+     ,"contrast(8%)")
+  ]
+
+spec :: Spec
+spec = filterTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/GradientSpec.hs b/tests/Hasmin/Types/GradientSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/GradientSpec.hs
@@ -0,0 +1,89 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.GradientSpec where
+
+import Test.Hspec
+-- import Test.QuickCheck
+-- import Hasmin.Parser.Internal
+
+-- import Test.Hspec.Attoparsec (shouldParse, parseSatisfies, (~>))
+import Control.Monad.Reader (runReader)
+import Data.Text (Text)
+-- import Data.Attoparsec.Text (Parser)
+import Hasmin.Parser.Value
+import Hasmin.Types.Class
+import Hasmin.Config
+import Hasmin.TestUtils
+
+gradientTests :: Spec
+gradientTests = 
+    describe "<gradient> minification tests" $
+      mapM_ (matchSpecWithDesc f) gradientTestsInfo
+  where f = (\x -> runReader (minifyWith x) defaultConfig) <$> value
+      
+gradientTestsInfo :: [(String, Text, Text)]
+gradientTestsInfo =
+  [("Converts \"to top\" to 0 (unitless 0 <angle>)"
+     ,"linear-gradient(to top, red, blue)"
+     ,"linear-gradient(0,red,blue)")
+  ,("Converts \"to right\" to 90deg"
+     ,"linear-gradient(to right, green, violet)"
+     ,"linear-gradient(90deg,green,violet)")
+  ,("Converts \"to left\" to 270deg"
+     ,"linear-gradient(to left, pink, peru)"
+     ,"linear-gradient(270deg,pink,peru)")
+  ,("Removes \"to bottom\", since that's the default <side-or-corner> value"
+     ,"linear-gradient(to bottom, green, violet)"
+     ,"linear-gradient(green,violet)")
+  ,("Removes \"180deg\", since that equals \"to bottom\", which is the default value"
+     ,"linear-gradient(180deg, green, violet)"
+     ,"linear-gradient(green,violet)")
+  ,("Should not convert \"to top right\""
+     ,"linear-gradient(to top right, pink, peru)"
+     ,"linear-gradient(to top right,pink,peru)")
+  ,("Should not convert \"to bottom left\""
+     ,"linear-gradient(to bottom left, pink, peru)"
+     ,"linear-gradient(to bottom left,pink,peru)")
+  ,("Reduces a <percentage> value to 0 when it is the same as the previous"
+     ,"linear-gradient(72deg, pink 25%, peru 25%)"
+     ,"linear-gradient(72deg,pink 25%,peru 0)")
+  ,("Reduces a <percentage> value to 0 when it is less than the greatest previous one"
+     ,"linear-gradient(72deg, pink 50%, peru 40%)"
+     ,"linear-gradient(72deg,pink 50%,peru 0)")
+  ,("Reduces a <length> value to 0 when it is the same as the previous"
+     ,"linear-gradient(72deg, pink 3em, peru 3em)"
+     ,"linear-gradient(72deg,pink 3em,peru 0)")
+  ,("Reduces a <length> value to 0 when it is less than the greatest previous one"
+     ,"linear-gradient(72deg, pink 1in, peru 90px)" -- 1in == 96px
+     ,"linear-gradient(72deg,pink 1in,peru 0)")
+  ,("Removes default start and end <percentage> values"
+     ,"linear-gradient(pink 0%, peru 100%)"
+     ,"linear-gradient(pink,peru)")
+  ,("Should not remove trailing zero when it is the last stop"
+     ,"linear-gradient(pink,peru 0)"
+     ,"linear-gradient(pink,peru 0)")
+  ,("Reduce complex combination of linear and non linear color hints"
+     ,"linear-gradient(pink 1%, peru 10%, green 20%, red 30%, violet 50%, purple 55%, cyan 60%, #ccc 80%, #000 100%)"
+     ,"linear-gradient(pink 1%,peru 10%,green,red 30%,violet 50%,purple,cyan 60%,#ccc,#000)")
+  ,("Removes circle in radial-gradient() when using a single <length>"
+     ,"radial-gradient(circle 1px, pink, peru)"
+     ,"radial-gradient(1px,pink,peru)")
+  ,("Removes ellipse in radial-gradient() when using two <length-percentage>"
+     ,"radial-gradient(ellipse 1px 1px, pink, peru)"
+     ,"radial-gradient(1px 1px,pink,peru)")
+  ,("Removes ellipse in radial-gradient() when using an <extent-keyword> in second place"
+     ,"radial-gradient(ellipse closest-side, pink, peru)"
+     ,"radial-gradient(closest-side,pink,peru)")
+  ,("Removes ellipse in radial-gradient() when using an <extent-keyword> first"
+     ,"radial-gradient(farthest-side ellipse, pink, peru)"
+     ,"radial-gradient(farthest-side,pink,peru)")
+  ,("Removes 'ellipse farthest-corner' in radial-gradient()"
+     ,"radial-gradient(ellipse farthest-corner, pink, peru)"
+     ,"radial-gradient(pink,peru)")
+  ]
+
+spec :: Spec
+spec = gradientTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/PositionSpec.hs b/tests/Hasmin/Types/PositionSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/PositionSpec.hs
@@ -0,0 +1,138 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.PositionSpec where
+
+import Test.Hspec
+import Hasmin.Parser.Value
+import Hasmin.TestUtils
+
+import Data.Text (Text)
+import Hasmin.Types.Class
+
+positionMinificationTests :: Spec
+positionMinificationTests =
+    describe "position minification" $
+      mapM_ (matchSpec f) positionMinificationTestsInfo
+  where f = minify <$> position
+
+positionMinificationTestsInfo :: [(Text, Text)]
+positionMinificationTestsInfo =
+  [("50%",              "50%")
+  ,("50% 50%",          "50%")
+  ,("center",           "50%")
+  ,("center top 50%",   "50%")
+  ,("top 50% center",   "50%")
+  ,("center left 50%",  "50%")
+  ,("top 50% left 50%", "50%")
+  ,("left 50% top 50%", "50%")
+  ,("left 50% center",  "50%")
+  ,("center center",    "50%")
+
+  ,("0% 50%",          "0")
+  ,("left",            "0")
+  ,("left center",     "0")
+  ,("left top 50%",    "0")
+  ,("left 0% center",  "0")
+  ,("left 0% top 50%", "0")
+  ,("top 50% left 0%", "0")
+  ,("center left 0%",  "0")
+  ,("top 50% left",    "0")
+  ,("center left",     "0")
+
+  ,("0% 0%",          "0 0")
+  ,("left 0%",        "0 0")
+  ,("0% top",         "0 0")
+  ,("left top",       "0 0")
+  ,("left top 0%",    "0 0")
+  ,("left 0% top",    "0 0")
+  ,("left 0% top 0%", "0 0")
+  ,("top 0% left 0%", "0 0")
+  ,("top left 0%",    "0 0")
+  ,("top 0% left",    "0 0")
+  ,("top left",       "0 0")
+
+  ,("50% 0%",          "top")
+  ,("top",             "top")
+  ,("top center",      "top")
+  ,("top left 50%",    "top")
+  ,("top 0% left 50%", "top")
+  ,("left 50% top 0%", "top")
+  ,("left 50% top",    "top")
+  ,("center top",      "top")
+
+  ,("100% 0%",         "100% 0")
+  ,("right top",       "100% 0")
+  ,("right 0% top",    "100% 0")
+  ,("right top 0%",    "100% 0")
+  ,("right 0% top 0%", "100% 0")
+  ,("top 0% right 0%", "100% 0")
+  ,("top 0% right",    "100% 0")
+  ,("top right 0%",    "100% 0")
+  ,("top right",       "100% 0")
+
+  ,("100%",             "100%")
+  ,("100% 50%",         "100%")
+  ,("right",            "100%")
+  ,("right center",     "100%")
+  ,("right top 50%",    "100%")
+  ,("right 0% center",  "100%")
+  ,("right 0% top 50%", "100%")
+  ,("top 50% right 0%", "100%")
+  ,("center right 0%",  "100%")
+  ,("top 50% right",    "100%")
+  ,("center right",     "100%")
+
+  ,("0% 100%",         "0 100%")
+  ,("left bottom",     "0 100%")
+  ,("left 0 bottom",   "0 100%")
+  ,("left bottom 0",   "0 100%")
+  ,("left 0 bottom 0", "0 100%")
+  ,("bottom 0 left 0", "0 100%")
+  ,("bottom left 0",   "0 100%")
+  ,("bottom 0 left",   "0 100%")
+  ,("bottom left",     "0 100%")
+
+  ,("50% 100%",           "bottom")
+  ,("bottom",             "bottom")
+  ,("center bottom",      "bottom")
+  ,("center bottom 0%",   "bottom")
+  ,("left 50% bottom",    "bottom")
+  ,("left 50% bottom 0%", "bottom")
+  ,("bottom 0% left 50%", "bottom")
+  ,("bottom left 50%",    "bottom")
+  ,("bottom 0% center",   "bottom")
+  ,("bottom center",      "bottom")
+
+  ,("100% 100%",          "100% 100%")
+  ,("100% bottom",        "100% 100%")
+  ,("right bottom",       "100% 100%")
+  ,("right 0% bottom",    "100% 100%")
+  ,("right bottom 0%",    "100% 100%")
+  ,("right 0% bottom 0%", "100% 100%")
+  ,("bottom 0% right 0%", "100% 100%")
+  ,("bottom 0% right",    "100% 100%")
+  ,("bottom right 0%",    "100% 100%")
+  ,("bottom right",       "100% 100%")
+
+  -- Other random tests
+  ,("10px 15px",          "10px 15px")
+  ,("30% 0",              "30% 0")
+  ,("27% 50%",            "27%")
+  ,("left 0 top 30px",    "0 30px")
+  ,("left 10px top 15px", "10px 15px")
+  ,("left 15px",          "0 15px")
+  ,("10px top",           "10px 0")
+  ,("left top 15px",      "0 15px")
+  ,("left 10px top",      "10px 0")
+
+  ,("right 0 top 30px",    "100% 30px")
+  ,("20px bottom",         "20px 100%")
+  ,("right 20px",          "100% 20px")
+  ,("top 30px right 0",    "100% 30px")
+  ]
+
+spec :: Spec
+spec = do positionMinificationTests 
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/RepeatStyleSpec.hs b/tests/Hasmin/Types/RepeatStyleSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/RepeatStyleSpec.hs
@@ -0,0 +1,32 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.RepeatStyleSpec where
+
+import Test.Hspec
+
+import Data.Text (Text)
+import Hasmin.Parser.Value
+import Hasmin.Types.Class
+import Hasmin.TestUtils
+
+repeatStyleTests :: Spec
+repeatStyleTests = 
+    describe "<repeat-style> minification tests" $
+      mapM_ (matchSpec f) repeatStyleTestsInfo
+  where f = minify <$> repeatStyle
+      
+repeatStyleTestsInfo :: [(Text, Text)]
+repeatStyleTestsInfo =
+  [("repeat no-repeat", "repeat-x")
+  ,("no-repeat repeat", "repeat-y")
+  ,("no-repeat no-repeat", "no-repeat")
+  ,("repeat repeat", "repeat")
+  ,("space space", "space")
+  ,("round round", "round")
+  ]
+
+spec :: Spec
+spec = repeatStyleTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/ShadowSpec.hs b/tests/Hasmin/Types/ShadowSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/ShadowSpec.hs
@@ -0,0 +1,45 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.ShadowSpec where
+
+import Test.Hspec
+
+import Data.Text (Text)
+import Hasmin.Parser.Value
+import Hasmin.Types.Class
+import Hasmin.TestUtils
+
+shadowTests :: Spec
+shadowTests = 
+    describe "<shadow> minification tests" $
+      mapM_ (matchSpecWithDesc f) shadowTestsInfo
+  where f = minify <$> shadowList
+      
+shadowTestsInfo :: [(String, Text, Text)]
+shadowTestsInfo =
+  [("Removes 4th <length> value when it is null"
+     ,"1px 1px 1px 0px red"
+     ,"1px 1px 1px red")
+  ,("Removes 3rd <length> value when there is no 4th, and it is zero"
+     ,"1px 1px 0px red"
+     ,"1px 1px red")
+  ,("Minifies permuted value"
+     ,"red 1px 1px 0px 0px inset"
+     ,"inset 1px 1px red")
+  ,("Minifies the <color> value"
+     ,"inset 1px 1px #ff0000"
+     ,"inset 1px 1px red")
+  ,("Minifies the <length> values"
+     ,"inset 12px 12px 12px 12px blue"
+     ,"inset 9pt 9pt 9pt 9pt blue")
+  ,("Minifies list of <shadow> values"
+     ,"inset 12px 12px 0 0 blue, 12px 12px 3px 0 red"
+     ,"inset 9pt 9pt blue,9pt 9pt 3px red")
+  ]
+
+spec :: Spec
+spec = shadowTests
+
+main :: IO ()
+main = hspec spec
+
diff --git a/tests/Hasmin/Types/StringSpec.hs b/tests/Hasmin/Types/StringSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/StringSpec.hs
@@ -0,0 +1,55 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.StringSpec where
+
+import Test.Hspec
+import Hasmin.Parser.Value
+import Hasmin.TestUtils
+
+import Data.Text (Text)
+import Hasmin.Types.Class
+
+
+quotesNormalizationTests :: Spec
+quotesNormalizationTests = do
+    describe "Quotes Normalization" $ do
+      describe "normalizes <string>s quotes in general" $
+        mapM_ (matchSpecWithDesc f) quotesNormalizationTestsInfo
+      describe "normalizes <strings>s quotes inside format()" $
+        mapM_ (matchSpec g) unquotingFormatTestsInfo
+      describe "unquotes url() <string>s" $
+        mapM_ (matchSpec g) unquotingUrlsTestsInfo
+  where f = minify <$> stringvalue
+        g = minify <$> textualvalue
+
+quotesNormalizationTestsInfo :: [(String, Text, Text)]
+quotesNormalizationTestsInfo =
+  [("Convert single quotes into double quotes",
+     "'x'", "\"x\"")
+  ,("Do not convert single quotes when they enclose a double quote",
+     "'\"'", "'\"'")
+  ,("Convert escaped double quotes into double quotes, but don't convert the enclosing single quotes into double",
+     "'\\22'", "'\"'")
+  ,("Don't convert escaped double quotes when enclosed in double quotes",
+     "\"\\22\"", "\"\\22\"")
+  ]
+
+unquotingUrlsTestsInfo :: [(Text, Text)] 
+unquotingUrlsTestsInfo = 
+  [("url(\"validUrl\")", "url(validUrl)")
+  ,("url('validUrl')", "url(validUrl)")
+  ,("url('a b')", "url('a b')")
+  ,("url(\"a'b\")", "url(\"a'b\")")
+  ,("url('a\"b')", "url('a\"b')")
+  ]
+
+-- TODO rename this, maybe combine it with some of the other tests
+unquotingFormatTestsInfo :: [(Text, Text)]
+unquotingFormatTestsInfo =
+  [("format('woff')", "format(\"woff\")")]
+
+spec :: Spec
+spec = quotesNormalizationTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/StylesheetSpec.hs b/tests/Hasmin/Types/StylesheetSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/StylesheetSpec.hs
@@ -0,0 +1,66 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.StylesheetSpec where
+
+import Test.Hspec
+import Hasmin.Parser.Internal
+import Hasmin.TestUtils
+
+import Data.Text (Text)
+
+atRuleTests :: Spec
+atRuleTests =
+    describe "at rules parsing and printing" $
+      mapM_ (matchSpec atRule) atRuleTestsInfo
+
+atRuleTestsInfo :: [(Text, Text)]
+atRuleTestsInfo =
+  [("@charset \"UTF-8\";",
+      "@charset \"UTF-8\";")
+  ,("@import/**/ 'custom.css' ;",
+      "@import 'custom.css';")
+  ,("@import  \"common.css\" screen , projection;",
+      "@import \"common.css\" screen,projection;")
+  ,("@import  url(\'landscape.css\')  screen  and  (orientation: landscape);",
+      "@import url(\'landscape.css\') screen and (orientation:landscape);")
+  ,("@namespace /**/ prefix url(XML-namespace-URL);",
+      "@namespace prefix url(XML-namespace-URL);")
+  ,("@namespace  prefix /**/  \"XML-namespace-URL\";",
+      "@namespace prefix \"XML-namespace-URL\";")
+  ,("@media screen {s{a:a}}",
+      "@media screen{s{a:a}}")
+  ,("@media screen and (min-width: 768px){s{a:a}}",
+      "@media screen and (min-width:768px){s{a:a}}")
+  ,("@keyframes p { from { background-position: 40px 0 } to { background-position: 0 0 } }",
+      "@keyframes p{from{background-position:40px 0}to{background-position:0 0}}")
+  ,("@font-face  /**/ {a:a;}",
+      "@font-face{a:a}")
+  -- ,("@supports (--foo: green) { body { color: green; } }",
+   --  "@supports (--foo:green){body{color:green}}")
+  -- ,("@supports ( transform-style: preserve ) or ( -moz-transform-style: preserve )",
+   --  "@supports (transform-style:preserve) or (-moz-transform-style:preserve)")
+  -- ,("@supports ( display : table-cell ) and ( not ( display : list-item ) )",
+   --  "@supports (display:table-cell) and (not (display:list-item))")
+  -- ,("@document url(http://www.w3.org/) , url-prefix(http://www.w3.org/Style/),\
+    --            \ domain(mozilla.org), regexp(\"https:.*\")"
+  -- ,("@document url(http://www.w3.org/),url-prefix(http://www.w3.org/Style/),\
+    --            \domain(mozilla.org),regexp(\"https:.*\")"
+  -- ,("@page { margin: 1in }",
+    -- "@page{margin:1in}")
+  -- ,("@page :left { font-size: 20pt; }",
+    -- "@page:left{font-size:20pt}")
+  -- ,("@page toc, index { size:8.5in 11in; }",
+    -- "@page toc,index{size:8.5in 11in}")
+  -- ,("@viewport { min-width: 640px; max-width: 800px; }",
+    -- "@viewport{min-width:640px;max-width:800px}")
+  -- ,("@counter-style circled-alpha { system: fixed; symbols: Ⓐ Ⓑ Ⓒ; suffix: " "; }",
+    -- "@counter-style circled-alpha{system:fixed;symbols:Ⓐ Ⓑ Ⓒ;suffix:" "}")
+  -- ,("@font-feature-values Jupiter Sans { @swash { delicate: 1; flowing: 2; } }",
+    -- "@font-feature-values Jupiter Sans{@swash{delicate:1;flowing:2}}",
+  ] 
+
+spec :: Spec
+spec = atRuleTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Hasmin/Types/TransformFunctionSpec.hs b/tests/Hasmin/Types/TransformFunctionSpec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Hasmin/Types/TransformFunctionSpec.hs
@@ -0,0 +1,69 @@
+{-# LANGUAGE OverloadedStrings #-}
+
+module Hasmin.Types.TransformFunctionSpec where
+
+import Test.Hspec
+import Hasmin.Parser.Value
+import Hasmin.TestUtils
+
+import Control.Monad.Reader (runReader)
+import Data.Text (Text)
+import Hasmin.Types.Class
+import Hasmin.Types.TransformFunction
+import Hasmin.Types.Value
+import Hasmin.Config
+
+transformTests :: Spec
+transformTests =
+    describe "transform function conversion" $
+      mapM_ (matchSpec (f <$> textualvalue)) transformTestsInfo
+  where f x = runReader (minifyWith x) defaultConfig
+
+combinationTests :: Spec
+combinationTests = 
+    describe "transform function combination" $
+      mapM_ (matchSpecWithDesc (g <$> values "transform")) functionCombinationTestsInfo
+  where g = mkValues . fmap TransformV . f . fmap (\(TransformV t) -> t) . valuesToList
+        f x = runReader (combine x) defaultConfig
+
+transformTestsInfo :: [(Text, Text)]
+transformTestsInfo = 
+  [("translate(0)",             "skew(0)")
+  ,("translateX(0)",            "skew(0)")
+  ,("translateY(0)",            "skew(0)")
+  ,("translate3d(0,0,0)",       "skew(0)")
+  ,("translate3d(0%,0,0)",      "skew(0)")
+  ,("translate3d(-100%,0,0)",   "translate(-100%)")
+  ,("translate3d(0,-100%,0)",   "translatey(-100%)")
+  ,("translate3d(0%,0%,5px)",   "translatez(5px)")
+  ,("translate3d(2em,0,0)",     "translate(2em)")
+  ,("translate3d(0,3em,0)",     "translatey(3em)")
+  ,("translate3d(0,0,4em)",     "translatez(4em)")
+  ,("scale3d(4, 1, 1)",         "scale(4)")
+  ,("scale3d(1, 5, 1)",         "scaley(5)")
+  ,("scale3d(1, 1, 6)",         "scalez(6)")
+  ,("matrix(1,0,0,0,0,/**/ 0)", "scaley(0)")
+  ,("matrix(7,0,0,8,0, 0)",     "scale(7,8)")
+  ]
+
+functionCombinationTestsInfo :: [(String, Text, Text)]
+functionCombinationTestsInfo = 
+  [("Combines consecutive absolute value translate() functions into one",
+    "translate(10px) translate(0) translate(10px)", "translate(20px)")
+  ,("Don't combine an skew(90deg), since tan(90deg) = ∞",
+    "skew(90deg) translate(1px)", "skew(90deg) translate(1px)")
+  ,("Remove identity matrices from lists (e.g. translate(0))",
+    "skew(90deg) translate(0px)", "skew(90deg)")
+  -- ,("Remove identity matrices at the end of a list"
+    -- "skew(90deg) translate(100%) skew(0)", "skew(90deg) translate(100%)")
+  -- ,("Remove identity matrices at the beginning of a list"
+    -- "skew(0) translate(100%) skew(90deg)", "translate(100%) skew(90deg)")
+  ]
+
+
+spec :: Spec
+spec = do transformTests 
+          combinationTests
+
+main :: IO ()
+main = hspec spec
diff --git a/tests/Spec.hs b/tests/Spec.hs
new file mode 100644
--- /dev/null
+++ b/tests/Spec.hs
@@ -0,0 +1,1 @@
+{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
