3
\$\begingroup\$

On part of a project, I have some CSS being read using twig's source() function.
But sending a huge chunk of CSS is not optimal.
To reduce the size of the HTML, I've written this twig filter.

However, I've noticed that some big files would take a bit more to be minimified.
To reduce the time to re-minify the CSS, I've added a crude caching system.

Assuming that you have the following:

$twig = new Twig_Environment(new Twig_Loader_Filesystem( ... ), array( ... ));

You have the following code:

$twig->addFilter(new Twig_SimpleFilter('minify_css', function($css, $cache = true){
        $path = $_SERVER['DOCUMENT_ROOT'] . '/cache/minify_css/';
        $min_length = 1024;

        if(!is_dir($path))
        {
            mkdir($path, 0755, true);
        }
        $length = strlen($css);
        $result = '';

        if($cache && $length > $min_length && @is_file($file = $path . ($hash = md5($css)) . '.css' ))
        {
            $result = file_get_contents($file);
        }
        else if($length >= 9)
        {
            $result = preg_replace(
                array(
                    '@/\*[^*]*\*+([^/][^*]*\*+)*\/@', // /*abc*/ -> 
                    '@([\-;\{]\w*)\s*:\s*@', //a : b -> a:b
                    '@\s*;\s*\}\s*@', //a:b ; } -> a:b}
                    '@\s*\{\s*@', // a { b:c} -> a{b:c}
                    '@\s*!important\s*@i', // a:b !important } -> a:b!important}
                    '@([,;}!:]|^)\s*0+\s*px@i', // a: 0px -> a:0
                    '@([,;:!.>~\+=\[\{\}\(]|^)[\s\r\n]+@', // a, b -> a,b
                    '@[\s\r\n]+([,;!>~\+=\]\{\}\)]|$)@', // a ,b -> a,b
                    '@([,:\( ])\s*#([\da-f])\2([\da-f])\3([\da-f])\4@i' // a: #FF00CC -> a:#F0C
                ),
                array(
                    '',
                    '$1:',
                    '}',
                    '{',
                    '!important',
                    '${1}0',
                    '$1',
                    '$1',
                    '$1#$2$3$4'
                ),
                str_replace(array("\r\n", "\r", "\n", "\t", '  ', '    ', '    '), '', $css)
            );

            if($cache && $length > $min_length)
            {
                file_put_contents($file, $result);
            }
        }
        return $result;
    }), array('is_safe' => array('html', 'css', 'js'))
);

This filter takes 1 parameter, to indicate if there's or not caching, and returns the minified CSS when applicable.

An example of it's usage:

{% for file in css_files %}
    <style>{{ source('assets/css/' ~ file ~ '.css') | minify_css }}</style>
{% endfor %}

This assumes that the template has access to the variable css_files, containing the name of those files.


What else can I improve here?
Any speed improvements or readability improvements?

\$\endgroup\$

1 Answer 1

1
\$\begingroup\$

The very obvious thing is that str_replace.

Here, there are 2 things to change:

  1. Completely remove this line.
    All that replace can be made into the following regular expression: @(?:\s{2,}|[\r\n\t])+@
    This should be the very first regular expression.
  2. Since all the other rules handle whitespace, one only needs to normalize it. The rest of the replacements will dictate if it needs to be replaced or not.
    If the whitespace is simply removed, it will lead to removing needed whitespace.
    Here's an example CSS:

    #services  img{
        max-width: 100%;
    }
    
    #slide .carousel-caption {
    display: none
    }  
    

    With the replacement as an empty string, you would remove the double-space in #services img. Normalizing it to a single space will return #services img instead of minifying into #servicesimg.

With this said, and a bug fixed, the code will look like the following:

    $twig->addFilter(new Twig_SimpleFilter('minify_css', function($css, $cache = true){
        $path = $_SERVER['DOCUMENT_ROOT'] . '/cache/minify_css/';
        $min_length = 1024;

        if(!is_dir($path))
        {
            mkdir($path, 0755, true);
        }
        $length = strlen($css);
        $result = '';

        if($cache && $length > $min_length && @is_file($file = $path . ($hash = md5($css)) . '.css' ))
        {
            $result = file_get_contents($file);
        }
        else if($length >= 9)
        {
            $result = preg_replace(
                array(
                    '@(?:\s{2,}|[\r\n\t])+@', // normalize whitespace
                    '@/\*[^*]*\*+([^/][^*]*\*+)*\/@', // /*abc*/ -> 
                    '@([\-;\{]\w*)\s*:\s*@', //a : b -> a:b
                    '@\s*;\s*\}\s*@', //a:b ; } -> a:b}
                    '@\s*\{\s*@', // a { b:c} -> a{b:c}
                    '@\s*!important\s*@i', // a:b !important } -> a:b!important}
                    '@([,;}!:]|^)\s*0+\s*px@i', // a: 0px -> a:0
                    '@([,;:!.>~\+=\[\{\}\(]|^)[\s\r\n]+@', // a, b -> a,b
                    '@[\s\r\n]+([,;!>~\+=\]\{\}\)]|$)@', // a ,b -> a,b
                    '@([,:\( ])\s*#([\da-f])\2([\da-f])\3([\da-f])\4@i' // a: #FF00CC -> a:#F0C
                ),
                array(
                    ' ',
                    '',
                    '$1:',
                    '}',
                    '{',
                    '!important',
                    '${1}0',
                    '$1',
                    '$1',
                    '$1#$2$3$4'
                ),
                $css
            );

            if($cache && $length > $min_length)
            {
                file_put_contents($file, $result);
            }
        }
        return $result;
    }), array('is_safe' => array('html', 'css', 'js'))
);
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Not the answer you're looking for? Browse other questions tagged or ask your own question.