content: --------------------------------------------------------------------------------
  -- | 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 `compressExpression` on all parenthesised calc and 
  -- clamp 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 compressExpression (drop 4 str)
                -- See issue #1021
                | "clamp(" `isPrefixOf` str = f "clamp" ++ nested 0 compressExpression (drop 5 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() and clamp() expressions
  compressExpression :: String -> String
  compressExpression =
      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