Go crazy? Well, maybe not..

I had another fun encounter with Internet Explorer this week. Apparently IE (6-9) enforces a hard limit for the number of CSS rules that it will accept in a single style sheet. Yup, you read that correctly. Just when I started to forget how much I disliked IE, it goes and proves to me once again just how worthy of my disdain it truly is.

I also feel the need to mention that the only browser that had this problem was IE. But, anyways.. I'm not bitter.

How on Earth Do You End Up With 4095+ CSS Rules?

Now, I feel I should start with a little back story to get you up to speed with how I got here. So, I've been using an application called pdf2htmlEX to convert gigantic PDF files (500MB+, 700+ pages) to HTML. To get the HTML to look almost exactly like the original PDF, the application uses a lot of absolute positioning of text; sometimes even individual letters are positioned absolutely by themselves. Needless to say, this leads to quite a lot of CSS rules. All in all, after converting one of these giant PDF files, I ended up with a style sheet with almost 12,000 CSS rules.

Before I went crazy with writing some code to chop up my giant CSS files into something IE will digest, I decided to test to make sure this was actually the problem. Using my IDE, I manually chopped up the CSS file into chunks of no more than 4095 rules each. Sure enough once I did this, IE decided it would render everything just fine. Amazing.

Great, Now What to Do About It?

Now that I knew for sure what the problem was, and how to fix it, I just had to code it. Here's a mini-library for CodeIgniter that I used to remedy this situation:

<?php  if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Css
{

    /*
        Chop up the given CSS into chunks with no more than the given # of rules each.
    */
    public function chop($css, $max_num_rules)
    {
        $chunks = array();

        $offset = 0;

        /*
            Find all the rules in the given CSS.
        */
        while (
            preg_match_all('~\{[^\{\}]+\}~', $css, $matches, PREG_OFFSET_CAPTURE, $offset) > 0 &&
            isset($matches[0][$max_num_rules])
        )
        {
            $pos1 = $matches[0][$max_num_rules - 1][1] + strlen($matches[0][$max_num_rules - 1][0]);
            $pos2 = $matches[0][$max_num_rules][1];

            $chunks[] = substr($css, $offset, $pos1 - $offset);

            $offset = $pos1;
        }

        // The left-overs.
        $chunks[] = substr($css, $offset);

        if (count($chunks) > 1)
            // We may have broken media queries in the CSS, let's fix them.
            $chunks = $this->fix_broken_media_queries($chunks);

        return $chunks;
    }

    /*
        Looks for and closes any open media queries in the given CSS.
    */
    protected function fix_broken_media_queries($chunks)
    {
        $open_media_queries = array();

        foreach ($chunks as $i => $css)
        {
            if (count($open_media_queries) > 0)
                foreach ($open_media_queries as $media_query)
                    $css = $media_query . $css;

            $n = 0;
            $offset = 0;

            while (preg_match('~@media [^\{]+\{~', $css, $matches, PREG_OFFSET_CAPTURE, $offset) === 1)
            {
                // Add to the array of open media queries.
                $open_media_queries[$n] = $matches[0][0];

                /*
                    Set 'pos1' to just after the media query declaration.
                */
                $pos1 = $matches[0][1] + strlen($matches[0][0]);

                $offset = $pos1;

                // Get the closing bracket of the media query.
                if (($pos2 = $this->find_next_closing_bracket($css, $offset)) !== false)
                {
                    // Found it!

                    $offset = ($pos2 + 1);

                    // Remove this media query from the array of open media queries.
                    if (isset($open_media_queries[$n]))
                        unset($open_media_queries[$n]);
                }
                else
                {
                    // Couldn't find the closing bracket for this media query.

                    // Close the open media query for this chunk.
                    $css .= '}';
                }
                
                $n++;
            }

            $chunks[$i] = $css;
        }

        return $chunks;
    }

    /*
        Finds the next closing bracket in the given CSS, starting at the given offset.
    */
    protected function find_next_closing_bracket($css, &$offset)
    {
        while (
            ($pos2 = strpos($css, '}', $offset)) !== false &&
            ($pos3 = strpos($css, '{', $offset)) !== false &&
            $pos3 < $pos2
        )
            $offset = ($pos2 + 1);

        return $pos2;
    }

}


/* End of file Css.php */
/* Location: ./application/libraries/Css.php */

And an example of the fix in action:

<?php if ( ! defined('BASEPATH')) exit('No direct script access allowed');

class Test extends CI_Controller
{

    function __construct()
    {
        parent::__construct();
    }

    public function index()
    {
        $this->load->library('Css');

        $chunks = $this->css->chop($css, 4095);

        foreach ($chunks as $i => $chunk)
        {
            $file = '/file/path/to/css/directory/chunk-' . ($i + 1) . '.css';

            file_put_contents($file, $chunk);
        }
    }
    
}