/** * Processes incoming CSS optimising it and then returning it. * * @param string $css The raw CSS to optimise * @return string The optimised CSS */ public function process($css) { global $CFG; $this->reset_stats(); $this->timestart = microtime(true); $this->rawstrlen = strlen($css); // First up we need to remove all line breaks - this allows us to instantly // reduce our processing requirements and as we will process everything // into a new structure there's really nothing lost. $css = preg_replace('#\\r?\\n#', ' ', $css); // Next remove the comments... no need to them in an optimised world and // knowing they're all gone allows us to REALLY make our processing simpler $css = preg_replace('#/\\*(.*?)\\*/#m', '', $css, -1, $this->commentsincss); $medias = array('all' => new css_media()); $imports = array(); $charset = false; $currentprocess = self::PROCESSING_START; $currentrule = css_rule::init(); $currentselector = css_selector::init(); $inquotes = false; // ' or " $inbraces = false; // { $inbrackets = false; // [ $inparenthesis = false; // ( $currentmedia = $medias['all']; $currentatrule = null; $suspectatrule = false; $buffer = ''; $char = null; // Next we are going to iterate over every single character in $css. // This is why we removed line breaks and comments! for ($i = 0; $i < $this->rawstrlen; $i++) { $lastchar = $char; $char = substr($css, $i, 1); if ($char == '@' && $buffer == '') { $suspectatrule = true; } switch ($currentprocess) { // Start processing an at rule e.g. @media, @page case self::PROCESSING_ATRULE: switch ($char) { case ';': if (!$inbraces) { $buffer .= $char; if ($currentatrule == 'import') { $imports[] = $buffer; $currentprocess = self::PROCESSING_SELECTORS; } else { if ($currentatrule == 'charset') { $charset = $buffer; $currentprocess = self::PROCESSING_SELECTORS; } } } $buffer = ''; $currentatrule = false; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '{': if ($currentatrule == 'media' && preg_match('#\\s*@media\\s*([a-zA-Z0-9]+(\\s*,\\s*[a-zA-Z0-9]+)*)#', $buffer, $matches)) { $mediatypes = str_replace(' ', '', $matches[1]); if (!array_key_exists($mediatypes, $medias)) { $medias[$mediatypes] = new css_media($mediatypes); } $currentmedia = $medias[$mediatypes]; $currentprocess = self::PROCESSING_SELECTORS; $buffer = ''; } // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } break; // Start processing selectors // Start processing selectors // Start processing selectors // Start processing selectors case self::PROCESSING_START: case self::PROCESSING_SELECTORS: switch ($char) { case '[': $inbrackets++; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ']': $inbrackets--; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ' ': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } if (!empty($buffer)) { if ($suspectatrule && preg_match('#@(media|import|charset)\\s*#', $buffer, $matches)) { $currentatrule = $matches[1]; $currentprocess = self::PROCESSING_ATRULE; $buffer .= $char; } else { $currentselector->add($buffer); $buffer = ''; } } $suspectatrule = false; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '{': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } $currentselector->add($buffer); $currentrule->add_selector($currentselector); $currentselector = css_selector::init(); $currentprocess = self::PROCESSING_STYLES; $buffer = ''; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '}': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } if ($currentatrule == 'media') { $currentmedia = $medias['all']; $currentatrule = false; $buffer = ''; } // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ',': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } $currentselector->add($buffer); $currentrule->add_selector($currentselector); $currentselector = css_selector::init(); $buffer = ''; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } break; // Start processing styles // Start processing styles // Start processing styles // Start processing styles case self::PROCESSING_STYLES: if ($char == '"' || $char == "'") { if ($inquotes === false) { $inquotes = $char; } if ($inquotes === $char && $lastchar !== '\\') { $inquotes = false; } } if ($inquotes) { $buffer .= $char; continue 2; } switch ($char) { case ';': if ($inparenthesis) { $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } $currentrule->add_style($buffer); $buffer = ''; $inquotes = false; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '}': $currentrule->add_style($buffer); $this->rawselectors += $currentrule->get_selector_count(); $currentmedia->add_rule($currentrule); $currentrule = css_rule::init(); $currentprocess = self::PROCESSING_SELECTORS; $this->rawrules++; $buffer = ''; $inquotes = false; $inparenthesis = false; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '(': $inparenthesis = true; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ')': $inparenthesis = false; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } break; } $buffer .= $char; } $css = ''; if (!empty($charset)) { $imports[] = $charset; } if (!empty($imports)) { $css .= implode("\n", $imports); $css .= "\n\n"; } foreach ($medias as $media) { $media->organise_rules_by_selectors(); $this->optimisedrules += $media->count_rules(); $this->optimisedselectors += $media->count_selectors(); if ($media->has_errors()) { $this->errors[] = $media->get_errors(); } $css .= $media->out(); } $this->optimisedstrlen = strlen($css); $this->timecomplete = microtime(true); return trim($css); }
/** * Processes incoming CSS optimising it and then returning it. * * @param string $css The raw CSS to optimise * @return string The optimised CSS */ public function process($css) { global $CFG; // Easiest win there is $css = trim($css); $this->reset_stats(); $this->timestart = microtime(true); $this->rawstrlen = strlen($css); // Don't try to process files with no content... it just doesn't make sense. // But we should produce an error for them, an empty CSS file will lead to a // useless request for those running theme designer mode. if ($this->rawstrlen === 0) { $this->errors[] = 'Skipping file as it has no content.'; return ''; } // First up we need to remove all line breaks - this allows us to instantly // reduce our processing requirements and as we will process everything // into a new structure there's really nothing lost. $css = preg_replace('#\\r?\\n#', ' ', $css); // Next remove the comments... no need to them in an optimised world and // knowing they're all gone allows us to REALLY make our processing simpler $css = preg_replace('#/\\*(.*?)\\*/#m', '', $css, -1, $this->commentsincss); $medias = array('all' => new css_media()); $imports = array(); $charset = false; // Keyframes are used for CSS animation they will be processed right at the very end. $keyframes = array(); $currentprocess = self::PROCESSING_START; $currentrule = css_rule::init(); $currentselector = css_selector::init(); $inquotes = false; // ' or " $inbraces = false; // { $inbrackets = false; // [ $inparenthesis = false; // ( $currentmedia = $medias['all']; $currentatrule = null; $suspectatrule = false; $buffer = ''; $char = null; // Next we are going to iterate over every single character in $css. // This is why we removed line breaks and comments! for ($i = 0; $i < $this->rawstrlen; $i++) { $lastchar = $char; $char = substr($css, $i, 1); if ($char == '@' && $buffer == '') { $suspectatrule = true; } switch ($currentprocess) { // Start processing an @ rule e.g. @media, @page, @keyframes case self::PROCESSING_ATRULE: switch ($char) { case ';': if (!$inbraces) { $buffer .= $char; if ($currentatrule == 'import') { $imports[] = $buffer; $currentprocess = self::PROCESSING_SELECTORS; } else { if ($currentatrule == 'charset') { $charset = $buffer; $currentprocess = self::PROCESSING_SELECTORS; } } } if ($currentatrule !== 'media') { $buffer = ''; $currentatrule = false; } // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '{': if ($currentatrule == 'media' && preg_match('#\\s*@media\\s*([a-zA-Z0-9]+(\\s*,\\s*[a-zA-Z0-9]+)*)\\s*{#', $buffer, $matches)) { // Basic media declaration $mediatypes = str_replace(' ', '', $matches[1]); if (!array_key_exists($mediatypes, $medias)) { $medias[$mediatypes] = new css_media($mediatypes); } $currentmedia = $medias[$mediatypes]; $currentprocess = self::PROCESSING_SELECTORS; $buffer = ''; } else { if ($currentatrule == 'media' && preg_match('#\\s*@media\\s*([^{]+)#', $buffer, $matches)) { // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/ $mediatypes = $matches[1]; $hash = md5($mediatypes); $medias[$hash] = new css_media($mediatypes); $currentmedia = $medias[$hash]; $currentprocess = self::PROCESSING_SELECTORS; $buffer = ''; } else { if ($currentatrule == 'keyframes' && preg_match('#@((\\-moz\\-|\\-webkit\\-)?keyframes)\\s*([^\\s]+)#', $buffer, $matches)) { // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow // them to be overridden to ensure we don't mess anything up. (means we keep everything in order) $keyframefor = $matches[1]; $keyframename = $matches[3]; $keyframe = new css_keyframe($keyframefor, $keyframename); $keyframes[] = $keyframe; $currentmedia = $keyframe; $currentprocess = self::PROCESSING_SELECTORS; $buffer = ''; } } } // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } break; // Start processing selectors // Start processing selectors case self::PROCESSING_START: case self::PROCESSING_SELECTORS: switch ($char) { case '[': $inbrackets++; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ']': $inbrackets--; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ' ': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } if (!empty($buffer)) { // Check for known @ rules if ($suspectatrule && preg_match('#@(media|import|charset|(\\-moz\\-|\\-webkit\\-)?(keyframes))\\s*#', $buffer, $matches)) { $currentatrule = !empty($matches[3]) ? $matches[3] : $matches[1]; $currentprocess = self::PROCESSING_ATRULE; $buffer .= $char; } else { $currentselector->add($buffer); $buffer = ''; } } $suspectatrule = false; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '{': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } if ($buffer !== '') { $currentselector->add($buffer); } $currentrule->add_selector($currentselector); $currentselector = css_selector::init(); $currentprocess = self::PROCESSING_STYLES; $buffer = ''; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '}': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } if ($currentatrule == 'media') { $currentmedia = $medias['all']; $currentatrule = false; $buffer = ''; } else { if (strpos($currentatrule, 'keyframes') !== false) { $currentmedia = $medias['all']; $currentatrule = false; $buffer = ''; } } // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ',': if ($inbrackets) { // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } $currentselector->add($buffer); $currentrule->add_selector($currentselector); $currentselector = css_selector::init(); $buffer = ''; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } break; // Start processing styles // Start processing styles case self::PROCESSING_STYLES: if ($char == '"' || $char == "'") { if ($inquotes === false) { $inquotes = $char; } if ($inquotes === $char && $lastchar !== '\\') { $inquotes = false; } } if ($inquotes) { $buffer .= $char; continue 2; } switch ($char) { case ';': if ($inparenthesis) { $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } $currentrule->add_style($buffer); $buffer = ''; $inquotes = false; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '}': $currentrule->add_style($buffer); $this->rawselectors += $currentrule->get_selector_count(); $currentmedia->add_rule($currentrule); $currentrule = css_rule::init(); $currentprocess = self::PROCESSING_SELECTORS; $this->rawrules++; $buffer = ''; $inquotes = false; $inparenthesis = false; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case '(': $inparenthesis = true; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; case ')': $inparenthesis = false; $buffer .= $char; // continue 1: The switch processing chars // continue 2: The switch processing the state // continue 3: The for loop continue 3; } break; } $buffer .= $char; } foreach ($medias as $media) { $this->optimise($media); } $css = $this->produce_css($charset, $imports, $medias, $keyframes); $this->timecomplete = microtime(true); return trim($css); }