    --------------------------------------------------------------------------------
    -- | Module used for CSS compression. The compression is currently in a simple
    -- state, but would typically reduce the number of bytes by about 25%.
    module Hakyll.Web.CompressCss
        ( compressCssCompiler
        , compressCss
        ) where

    --------------------------------------------------------------------------------
    import           Data.Char               (isSpace)
    import           Data.List               (dropWhileEnd, isPrefixOf)

    --------------------------------------------------------------------------------
    import           Hakyll.Core.Compiler
    import           Hakyll.Core.Item
    import           Hakyll.Core.Util.String

    --------------------------------------------------------------------------------
    -- | Compiler form of 'compressCss'
    compressCssCompiler :: Compiler (Item String)
    compressCssCompiler = fmap compressCss <$> getResourceString

    --------------------------------------------------------------------------------
    -- | Compress CSS to speed up your site.
    compressCss :: String -> String
    compressCss = withoutStrings (handleCalcExpressions compressSeparators . compressWhitespace)
                . dropWhileEnd isSpace
                . dropWhile isSpace
                . stripComments

    --------------------------------------------------------------------------------
    -- | Compresses certain forms of separators.
    compressSeparators :: String -> String
    compressSeparators =
        replaceAll "; *}" (const "}") .
        replaceAll ";+" (const ";") .
        replaceAll " *[{};,>+~!] *" (take 1 . dropWhile isSpace) .
        replaceAll ": *" (take 1) -- not destroying pseudo selectors (#323)

    -- | Uses `compressCalcExpression` on all parenthesised calc expressions
    -- and applies `transform` to all parts outside of them
    handleCalcExpressions :: (String -> String) -> String -> String
    handleCalcExpressions transform = top transform
      where
        top f ""                             = f ""
        top f str | "calc(" `isPrefixOf` str = f "calc" ++ nested 0 compressCalcExpression (drop 4 str)
        top f (x:xs)                         = top (f . (x:)) xs

        -- when called with depth=0, the first character must be a '('
        nested :: Int -> (String -> String) -> String -> String
        nested _     f ""                             = f "" -- shouldn't happen, mismatched nesting
        nested depth f str | "calc(" `isPrefixOf` str = nested depth f (drop 4 str)
        nested 1     f (')':xs)                       = f ")" ++ top transform xs
        nested depth f (x:xs)                         = nested (case x of
                                                          '(' -> depth + 1
                                                          ')' -> depth - 1 -- assert: depth > 1
                                                          _   -> depth
                                                        ) (f . (x:)) xs

    -- | does not remove whitespace around + and -, which is important in calc() expressions
    compressCalcExpression :: String -> String
    compressCalcExpression =
        replaceAll " *[*/] *| *\\)|\\( *" (take 1 . dropWhile isSpace)

    --------------------------------------------------------------------------------
    -- | Compresses all whitespace.
    compressWhitespace :: String -> String
    compressWhitespace = replaceAll "[ \t\n\r]+" (const " ")

    --------------------------------------------------------------------------------
    -- | Function that strips CSS comments away (outside of strings).
    stripComments :: String -> String
    stripComments ""                       = ""
    stripComments ('/':'*':str)            = stripComments $ eatComment str
    stripComments (x:xs) | x `elem` "\"'"  = retainString x xs stripComments
                         | otherwise       = x : stripComments xs

    eatComment :: String -> String
    eatComment "" = ""
    eatComment ('*':'/':str) = str
    eatComment (_:str) = eatComment str

    --------------------------------------------------------------------------------
    -- | Helper functions to handle string tokens correctly.

    -- TODO: handle backslash escapes
    withoutStrings :: (String -> String) -> String -> String
    withoutStrings f str = case span (`notElem` "\"'") str of
        (text, "")     -> f text
        (text, d:rest) -> f text ++ retainString d rest (withoutStrings f)

    retainString :: Char -> String -> (String -> String) -> String
    retainString delim str cont = case span (/= delim) str of
        (val, "")     -> delim : val
        (val, _:rest) -> delim : val ++ delim : cont rest