FMCorz
4/7/2014 - 5:00 AM

Chunk CSS into smaller chunks

Chunk CSS into smaller chunks

<?php

function chunk($css, $every = 4095) {
    $chunks = array();
    $offsets = array();
    $offset = 0;
    $selectorcount = 0;
    $lastvalidoffset = 0;
    $lastvalidoffsetselectorcount = 0;
    $inrule = 0;
    $inmedia = false;
    $mediacoming = false;

    // Remove the comments. Because it's easier, safer and probably a lot of other good reasons.
    $css = preg_replace('#/\*(.*?)\*/#m', '', $css);
    $strlen = strlen($css);

    // Walk through the CSS content character by character.
    for ($i = 1; $i <= $strlen; $i++) {
        $char = $css[$i - 1];
        $offset = $i;

        // Is that a media query that I see coming towards us?
        if ($char === '@') {
            if (!$inmedia && substr($css, $offset, 5) === 'media') {
                $mediacoming = true;
            }
        }

        // So we are entering a rule or a media query...
        if ($char === '{') {
            if ($mediacoming) {
                $inmedia = true;
                $mediacoming = false;
            } else {
                $inrule++;
                $selectorcount++;
            }
        }

        // Let's count the number of selectors, but only if we are not in a rule as they
        // can contain commas too.
        if (!$inrule && $char === ',') {
            $selectorcount++;
        }

        // We reached the end of something.
        if ($char === '}') {
            // Oh, we are in a media query.
            if ($inmedia) {
                if (!$inrule) {
                    // This is the end of the media query, let's note that it is safe to split here.
                    $inmedia = false;
                    $lastvalidoffset = $offset;
                    $lastvalidoffsetselectorcount = $selectorcount;
                } else {
                    // We were in a rule, in the media query.
                    $inrule--;
                }
            } else {
                $inrule--;
                if (!$inrule) {
                    // We exited the rule, it is safe to split here.
                    $lastvalidoffset = $offset;
                    $lastvalidoffsetselectorcount = $selectorcount;
                }
            }
        }

        // Alright, this is splitting time...
        if ($selectorcount >= $every) {
            if (!$lastvalidoffset) {
                // We must have reached more selectors into one set than we were allowed. That means that either
                // the chunk size value is too small, or that we have a gigantic group of selectors, or that a media
                // query contains more selectors than the chunk size. We have to ignore this because we do not
                // support split inside a group of selectors or media query.
                debugging('Could not find a safe place to split at ' . $offset . ', ignoring...', DEBUG_DEVELOPER);
            } else {
                // We identify the offset to split at and reset the number of selectors found from there.
                $offsets[] = $lastvalidoffset;
                $selectorcount = $selectorcount - $lastvalidoffsetselectorcount;
                $lastvalidoffset = 0;
            }
        }

    }

    // Now that we have got the offets, we can chunk the CSS.
    $offsetcount = count($offsets);
    foreach ($offsets as $key => $index) {
        $start = 0;
        if ($key > 0) {
            $start = $offsets[$key - 1];
        }
        // From somewhere up to the offset.
        $chunks[] = substr($css, $start, $index - $start);
    }
    // Add the last chunk, from the last offset to the end of the string.
    $chunks[] = substr($css, end($offsets));

    return $chunks;

}