. /** * This file contains CSS related class, and function for the CSS optimiser. * * Please see the {@link css_optimiser} class for greater detail. * * NOTE: these functions are not expected to be used from any addons. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ defined('MOODLE_INTERNAL') || die(); if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) { // This can be also set in config.php file, // it needs to be higher than the time it takes to generate all CSS content. define('THEME_DESIGNER_CACHE_LIFETIME', 10); } /** * Stores CSS in a file at the given path. * * This function either succeeds or throws an exception. * * @param theme_config $theme The theme that the CSS belongs to. * @param string $csspath The path to store the CSS at. * @param string $csscontent the complete CSS in one string * @param bool $chunk If set to true these files will be chunked to ensure * that no one file contains more than 4095 selectors. * @param string $chunkurl If the CSS is be chunked then we need to know the URL * to use for the chunked files. */ function css_store_css(theme_config $theme, $csspath, $csscontent, $chunk = false, $chunkurl = null) { global $CFG; clearstatcache(); if (!file_exists(dirname($csspath))) { @mkdir(dirname($csspath), $CFG->directorypermissions, true); } // Prevent serving of incomplete file from concurrent request, // the rename() should be more atomic than fwrite(). ignore_user_abort(true); // First up write out the single file for all those using decent browsers. css_write_file($csspath, $csscontent); if ($chunk) { // If we need to chunk the CSS for browsers that are sub-par. $css = css_chunk_by_selector_count($csscontent, $chunkurl); $files = count($css); $count = 1; foreach ($css as $content) { if ($count === $files) { // If there is more than one file and this IS the last file. $filename = preg_replace('#\.css$#', '.0.css', $csspath); } else { // If there is more than one file and this is not the last file. $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath); } $count++; css_write_file($filename, $content); } } ignore_user_abort(false); if (connection_aborted()) { die; } } /** * Writes a CSS file. * * @param string $filename * @param string $content */ function css_write_file($filename, $content) { global $CFG; if ($fp = fopen($filename.'.tmp', 'xb')) { fwrite($fp, $content); fclose($fp); rename($filename.'.tmp', $filename); @chmod($filename, $CFG->filepermissions); @unlink($filename.'.tmp'); // Just in case anything fails. } } /** * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors. * * The chunking will not split a group of selectors, or a media query. That means that * if n > $maxselectors and there are n selectors grouped together, * they will not be chunked and you could end up with more selectors than desired. * The same applies for a media query that has more than n selectors. * * Also, as we do not split group of selectors or media queries, the chunking might * not be as optimal as it could be, having files with less selectors than it could * potentially contain. * * String functions used here are not compliant with unicode characters. But that is * not an issue as the syntax of CSS is using ASCII codes. Even if we have unicode * characters in comments, or in the property 'content: ""', it will behave correcly. * * Please note that this strips out the comments if chunking happens. * * @param string $css The CSS to chunk. * @param string $importurl The URL to use for import statements. * @param int $maxselectors The number of selectors to limit a chunk to. * @param int $buffer Not used any more. * @return array An array of CSS chunks. */ function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) { // Check if we need to chunk this CSS file. $count = substr_count($css, ',') + substr_count($css, '{'); if ($count < $maxselectors) { // The number of selectors is less then the max - we're fine. return array($css); } $chunks = array(); // The final chunks. $offsets = array(); // The indexes to chunk at. $offset = 0; // The current offset. $selectorcount = 0; // The number of selectors since the last split. $lastvalidoffset = 0; // The last valid index to split at. $lastvalidoffsetselectorcount = 0; // The number of selectors used at the time were could split. $inrule = 0; // The number of rules we are in, should not be greater than 1. $inmedia = false; // Whether or not we are in a media query. $mediacoming = false; // Whether or not we are expeting a media query. $currentoffseterror = null; // Not null when we have recorded an error for the current split. $offseterrors = array(); // The offsets where we found errors. // Remove the comments. Because it's easier, safer and probably a lot of other good reasons. $css = preg_replace('#/\*(.*?)\*/#s', '', $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, or in // the definition of a media query, as they can contain commas too. if (!$mediacoming && !$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. $inmedia = false; } else { // We were in a rule, in the media query. $inrule--; } } else { $inrule--; // Handle stupid broken CSS where there are too many } brackets, // as this can cause it to break (with chunking) where it would // coincidentally have worked otherwise. if ($inrule < 0) { $inrule = 0; } } // We are not in a media query, and there is no pending rule, it is safe to split here. if (!$inmedia && !$inrule) { $lastvalidoffset = $offset; $lastvalidoffsetselectorcount = $selectorcount; } } // Alright, this is splitting time... if ($selectorcount > $maxselectors) { 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. if ($currentoffseterror === null) { $currentoffseterror = $offset; $offseterrors[] = $currentoffseterror; } } else { // We identify the offset to split at and reset the number of selectors found from there. $offsets[] = $lastvalidoffset; $selectorcount = $selectorcount - $lastvalidoffsetselectorcount; $lastvalidoffset = 0; $currentoffseterror = null; } } } // Report offset errors. if (!empty($offseterrors)) { debugging('Could not find a safe place to split at offset(s): ' . implode(', ', $offseterrors) . '. Those were ignored.', DEBUG_DEVELOPER); } // 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 (if there is one), from the last offset to the end of the string. if (end($offsets) != $strlen) { $chunks[] = substr($css, end($offsets)); } // The array $chunks now contains CSS split into perfect sized chunks. // Import statements can only appear at the very top of a CSS file. // Imported sheets are applied in the the order they are imported and // are followed by the contents of the CSS. // This is terrible for performance. // It means we must put the import statements at the top of the last chunk // to ensure that things are always applied in the correct order. // This way the chunked files are included in the order they were chunked // followed by the contents of the final chunk in the actual sheet. $importcss = ''; $slashargs = strpos($importurl, '.php?') === false; $parts = count($chunks); for ($i = 1; $i < $parts; $i++) { if ($slashargs) { $importcss .= "@import url({$importurl}/chunk{$i});\n"; } else { $importcss .= "@import url({$importurl}&chunk={$i});\n"; } } $importcss .= end($chunks); $chunks[key($chunks)] = $importcss; return $chunks; } /** * Sends a cached CSS file * * This function sends the cached CSS file. Remember it is generated on the first * request, then optimised/minified, and finally cached for serving. * * @param string $csspath The path to the CSS file we want to serve. * @param string $etag The revision to make sure we utilise any caches. */ function css_send_cached_css($csspath, $etag) { // 60 days only - the revision may get incremented quite often. $lifetime = 60*60*24*60; header('Etag: "'.$etag.'"'); header('Content-Disposition: inline; filename="styles.php"'); header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); header('Cache-Control: public, max-age='.$lifetime); header('Accept-Ranges: none'); header('Content-Type: text/css; charset=utf-8'); if (!min_enable_zlib_compression()) { header('Content-Length: '.filesize($csspath)); } readfile($csspath); die; } /** * Sends a cached CSS content * * @param string $csscontent The actual CSS markup. * @param string $etag The revision to make sure we utilise any caches. */ function css_send_cached_css_content($csscontent, $etag) { // 60 days only - the revision may get incremented quite often. $lifetime = 60*60*24*60; header('Etag: "'.$etag.'"'); header('Content-Disposition: inline; filename="styles.php"'); header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Pragma: '); header('Cache-Control: public, max-age='.$lifetime); header('Accept-Ranges: none'); header('Content-Type: text/css; charset=utf-8'); if (!min_enable_zlib_compression()) { header('Content-Length: '.strlen($csscontent)); } echo($csscontent); die; } /** * Sends CSS directly without caching it. * * This function takes a raw CSS string, optimises it if required, and then * serves it. * Turning both themedesignermode and CSS optimiser on at the same time is awful * for performance because of the optimiser running here. However it was done so * that theme designers could utilise the optimised output during development to * help them optimise their CSS... not that they should write lazy CSS. * * @param string $css */ function css_send_uncached_css($css) { header('Content-Disposition: inline; filename="styles_debug.php"'); header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT'); header('Pragma: '); header('Accept-Ranges: none'); header('Content-Type: text/css; charset=utf-8'); if (is_array($css)) { $css = implode("\n\n", $css); } echo $css; die; } /** * Send file not modified headers * * @param int $lastmodified * @param string $etag */ function css_send_unmodified($lastmodified, $etag) { // 60 days only - the revision may get incremented quite often. $lifetime = 60*60*24*60; header('HTTP/1.1 304 Not Modified'); header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT'); header('Cache-Control: public, max-age='.$lifetime); header('Content-Type: text/css; charset=utf-8'); header('Etag: "'.$etag.'"'); if ($lastmodified) { header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT'); } die; } /** * Sends a 404 message about CSS not being found. */ function css_send_css_not_found() { header('HTTP/1.0 404 not found'); die('CSS was not found, sorry.'); } /** * Determines if the given value is a valid CSS colour. * * A CSS colour can be one of the following: * - Hex colour: #AA66BB * - RGB colour: rgb(0-255, 0-255, 0-255) * - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1) * - HSL colour: hsl(0-360, 0-100%, 0-100%) * - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1) * * Or a recognised browser colour mapping {@link css_optimiser::$htmlcolours} * * @param string $value The colour value to check * @return bool */ function css_is_colour($value) { $value = trim($value); $hex = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/'; $rgb = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i'; $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i'; $hsl = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i'; $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i'; if (in_array(strtolower($value), array('inherit'))) { return true; } else if (preg_match($hex, $value)) { return true; } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) { return true; } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) { // It is an RGB colour. return true; } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) { // It is an RGBA colour. return true; } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) { // It is an HSL colour. return true; } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) { // It is an HSLA colour. return true; } // Doesn't look like a colour. return false; } /** * Returns true is the passed value looks like a CSS width. * In order to pass this test the value must be purely numerical or end with a * valid CSS unit term. * * @param string|int $value * @return boolean */ function css_is_width($value) { $value = trim($value); if (in_array(strtolower($value), array('auto', 'inherit'))) { return true; } if ((string)$value === '0' || preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) { return true; } return false; } /** * A simple sorting function to sort two array values on the number of items they contain * * @param array $a * @param array $b * @return int */ function css_sort_by_count(array $a, array $b) { $a = count($a); $b = count($b); if ($a == $b) { return 0; } return ($a > $b) ? -1 : 1; } /** * A basic CSS optimiser that strips out unwanted things and then processes CSS organising and cleaning styles. * * This CSS optimiser works by reading through a CSS string one character at a * time and building an object structure of the CSS. * As part of that processing styles are expanded out as much as they can be to * ensure we collect all mappings, at the end of the processing those styles are * then combined into an optimised form to keep them as short as possible. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_optimiser { /** * Used when the processor is about to start processing. * Processing states. Used internally. */ const PROCESSING_START = 0; /** * Used when the processor is currently processing a selector. * Processing states. Used internally. */ const PROCESSING_SELECTORS = 0; /** * Used when the processor is currently processing a style. * Processing states. Used internally. */ const PROCESSING_STYLES = 1; /** * Used when the processor is currently processing a comment. * Processing states. Used internally. */ const PROCESSING_COMMENT = 2; /** * Used when the processor is currently processing an @ rule. * Processing states. Used internally. */ const PROCESSING_ATRULE = 3; /** * The raw string length before optimisation. * Stats variables set during and after processing * @var int */ protected $rawstrlen = 0; /** * The number of comments that were removed during optimisation. * Stats variables set during and after processing * @var int */ protected $commentsincss = 0; /** * The number of rules in the CSS before optimisation. * Stats variables set during and after processing * @var int */ protected $rawrules = 0; /** * The number of selectors using in CSS rules before optimisation. * Stats variables set during and after processing * @var int */ protected $rawselectors = 0; /** * The string length after optimisation. * Stats variables set during and after processing * @var int */ protected $optimisedstrlen = 0; /** * The number of rules after optimisation. * Stats variables set during and after processing * @var int */ protected $optimisedrules = 0; /** * The number of selectors used in rules after optimisation. * Stats variables set during and after processing * @var int */ protected $optimisedselectors = 0; /** * The start time of the optimisation. * Stats variables set during and after processing * @var int */ protected $timestart = 0; /** * The end time of the optimisation. * Stats variables set during and after processing * @var int */ protected $timecomplete = 0; /** * Will be set to any errors that may have occured during processing. * This is updated only at the end of processing NOT during. * * @var array */ protected $errors = array(); /** * 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) { // 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; // ( /* @var css_media $currentmedia */ $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 '{': $regexmediabasic = '#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#'; $regexadvmedia = '#\s*@media\s*([^{]+)#'; $regexkeyframes = '#@((\-moz\-|\-webkit\-|\-ms\-|\-o\-)?keyframes)\s*([^\s]+)#'; if ($currentatrule == 'media' && preg_match($regexmediabasic, $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($regexadvmedia, $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($regexkeyframes, $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. case self::PROCESSING_START: case self::PROCESSING_SELECTORS: $regexatrule = '#@(media|import|charset|(\-moz\-|\-webkit\-|\-ms\-|\-o\-)?(keyframes))\s*#'; 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($regexatrule, $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; } // Check for known @ rules. if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) { // Ahh we've been in an @rule, lets rewind one and have the @rule case process this. $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1]; $currentprocess = self::PROCESSING_ATRULE; $i--; $suspectatrule = false; // 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. 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); } /** * Produces CSS for the given charset, imports, media, and keyframes * @param string $charset * @param array $imports * @param css_media[] $medias * @param css_keyframe[] $keyframes * @return string */ protected function produce_css($charset, array $imports, array $medias, array $keyframes) { $css = ''; if (!empty($charset)) { $imports[] = $charset; } if (!empty($imports)) { $css .= implode("\n", $imports); $css .= "\n\n"; } $cssreset = array(); $cssstandard = array(); $csskeyframes = array(); // Process each media declaration individually. foreach ($medias as $media) { // If this declaration applies to all media types. if (in_array('all', $media->get_types())) { // Collect all rules that represet reset rules and remove them from the media object at the same time. // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they // can't end up out of order because of optimisation. $resetrules = $media->get_reset_rules(true); if (!empty($resetrules)) { $cssreset[] = css_writer::media('all', $resetrules); } } // Get the standard cSS. $cssstandard[] = $media->out(); } // Finally if there are any keyframe declarations process them now. if (count($keyframes) > 0) { foreach ($keyframes as $keyframe) { $this->optimisedrules += $keyframe->count_rules(); $this->optimisedselectors += $keyframe->count_selectors(); if ($keyframe->has_errors()) { $this->errors += $keyframe->get_errors(); } $csskeyframes[] = $keyframe->out(); } } // Join it all together. $css .= join('', $cssreset); $css .= join('', $cssstandard); $css .= join('', $csskeyframes); // Record the strlenght of the now optimised CSS. $this->optimisedstrlen = strlen($css); // Return the now produced CSS. return $css; } /** * Optimises the CSS rules within a rule collection of one form or another * * @param css_rule_collection $media * @return void This function acts in reference */ protected function optimise(css_rule_collection $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(); } } /** * Returns an array of stats from the last processing run * @return string */ public function get_stats() { $stats = array( 'timestart' => $this->timestart, 'timecomplete' => $this->timecomplete, 'timetaken' => round($this->timecomplete - $this->timestart, 4), 'commentsincss' => $this->commentsincss, 'rawstrlen' => $this->rawstrlen, 'rawselectors' => $this->rawselectors, 'rawrules' => $this->rawrules, 'optimisedstrlen' => $this->optimisedstrlen, 'optimisedrules' => $this->optimisedrules, 'optimisedselectors' => $this->optimisedselectors, 'improvementstrlen' => '-', 'improvementrules' => '-', 'improvementselectors' => '-', ); // Avoid division by 0 errors by checking we have valid raw values. if ($this->rawstrlen > 0) { $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%'; } if ($this->rawrules > 0) { $stats['improvementrules'] = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%'; } if ($this->rawselectors > 0) { $stats['improvementselectors'] = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%'; } return $stats; } /** * Returns true if any errors have occured during processing * * @return bool */ public function has_errors() { return !empty($this->errors); } /** * Returns an array of errors that have occured * * @param bool $clear If set to true the errors will be cleared after being returned. * @return array */ public function get_errors($clear = false) { $errors = $this->errors; if ($clear) { // Reset the error array. $this->errors = array(); } return $errors; } /** * Returns any errors as a string that can be included in CSS. * * @return string */ public function output_errors_css() { $computedcss = "/****************************************\n"; $computedcss .= " *--- Errors found during processing ----\n"; foreach ($this->errors as $error) { $computedcss .= preg_replace('#^#m', '* ', $error); } $computedcss .= " ****************************************/\n\n"; return $computedcss; } /** * Returns a string to display stats about the last generation within CSS output * * @return string */ public function output_stats_css() { $computedcss = "/****************************************\n"; $computedcss .= " *------- CSS Optimisation stats --------\n"; if ($this->rawstrlen === 0) { $computedcss .= " File not processed as it has no content /\n\n"; $computedcss .= " ****************************************/\n\n"; return $computedcss; } else if ($this->rawrules === 0) { $computedcss .= " File contained no rules to be processed /\n\n"; $computedcss .= " ****************************************/\n\n"; return $computedcss; } $stats = $this->get_stats(); $computedcss .= " * ".date('r')."\n"; $computedcss .= " * {$stats['commentsincss']} \t comments removed\n"; $computedcss .= " * Optimisation took {$stats['timetaken']} seconds\n"; $computedcss .= " *--------------- before ----------------\n"; $computedcss .= " * {$stats['rawstrlen']} \t chars read in\n"; $computedcss .= " * {$stats['rawrules']} \t rules read in\n"; $computedcss .= " * {$stats['rawselectors']} \t total selectors\n"; $computedcss .= " *---------------- after ----------------\n"; $computedcss .= " * {$stats['optimisedstrlen']} \t chars once optimized\n"; $computedcss .= " * {$stats['optimisedrules']} \t optimized rules\n"; $computedcss .= " * {$stats['optimisedselectors']} \t total selectors once optimized\n"; $computedcss .= " *---------------- stats ----------------\n"; $computedcss .= " * {$stats['improvementstrlen']} \t reduction in chars\n"; $computedcss .= " * {$stats['improvementrules']} \t reduction in rules\n"; $computedcss .= " * {$stats['improvementselectors']} \t reduction in selectors\n"; $computedcss .= " ****************************************/\n\n"; return $computedcss; } /** * Resets the stats ready for another fresh processing */ public function reset_stats() { $this->commentsincss = 0; $this->optimisedrules = 0; $this->optimisedselectors = 0; $this->optimisedstrlen = 0; $this->rawrules = 0; $this->rawselectors = 0; $this->rawstrlen = 0; $this->timecomplete = 0; $this->timestart = 0; } /** * An array of the common HTML colours that are supported by most browsers. * * This reference table is used to allow us to unify colours, and will aid * us in identifying buggy CSS using unsupported colours. * * @var string[] */ public static $htmlcolours = array( 'aliceblue' => '#F0F8FF', 'antiquewhite' => '#FAEBD7', 'aqua' => '#00FFFF', 'aquamarine' => '#7FFFD4', 'azure' => '#F0FFFF', 'beige' => '#F5F5DC', 'bisque' => '#FFE4C4', 'black' => '#000000', 'blanchedalmond' => '#FFEBCD', 'blue' => '#0000FF', 'blueviolet' => '#8A2BE2', 'brown' => '#A52A2A', 'burlywood' => '#DEB887', 'cadetblue' => '#5F9EA0', 'chartreuse' => '#7FFF00', 'chocolate' => '#D2691E', 'coral' => '#FF7F50', 'cornflowerblue' => '#6495ED', 'cornsilk' => '#FFF8DC', 'crimson' => '#DC143C', 'cyan' => '#00FFFF', 'darkblue' => '#00008B', 'darkcyan' => '#008B8B', 'darkgoldenrod' => '#B8860B', 'darkgray' => '#A9A9A9', 'darkgrey' => '#A9A9A9', 'darkgreen' => '#006400', 'darkKhaki' => '#BDB76B', 'darkmagenta' => '#8B008B', 'darkolivegreen' => '#556B2F', 'arkorange' => '#FF8C00', 'darkorchid' => '#9932CC', 'darkred' => '#8B0000', 'darksalmon' => '#E9967A', 'darkseagreen' => '#8FBC8F', 'darkslateblue' => '#483D8B', 'darkslategray' => '#2F4F4F', 'darkslategrey' => '#2F4F4F', 'darkturquoise' => '#00CED1', 'darkviolet' => '#9400D3', 'deeppink' => '#FF1493', 'deepskyblue' => '#00BFFF', 'dimgray' => '#696969', 'dimgrey' => '#696969', 'dodgerblue' => '#1E90FF', 'firebrick' => '#B22222', 'floralwhite' => '#FFFAF0', 'forestgreen' => '#228B22', 'fuchsia' => '#FF00FF', 'gainsboro' => '#DCDCDC', 'ghostwhite' => '#F8F8FF', 'gold' => '#FFD700', 'goldenrod' => '#DAA520', 'gray' => '#808080', 'grey' => '#808080', 'green' => '#008000', 'greenyellow' => '#ADFF2F', 'honeydew' => '#F0FFF0', 'hotpink' => '#FF69B4', 'indianred ' => '#CD5C5C', 'indigo ' => '#4B0082', 'ivory' => '#FFFFF0', 'khaki' => '#F0E68C', 'lavender' => '#E6E6FA', 'lavenderblush' => '#FFF0F5', 'lawngreen' => '#7CFC00', 'lemonchiffon' => '#FFFACD', 'lightblue' => '#ADD8E6', 'lightcoral' => '#F08080', 'lightcyan' => '#E0FFFF', 'lightgoldenrodyellow' => '#FAFAD2', 'lightgray' => '#D3D3D3', 'lightgrey' => '#D3D3D3', 'lightgreen' => '#90EE90', 'lightpink' => '#FFB6C1', 'lightsalmon' => '#FFA07A', 'lightseagreen' => '#20B2AA', 'lightskyblue' => '#87CEFA', 'lightslategray' => '#778899', 'lightslategrey' => '#778899', 'lightsteelblue' => '#B0C4DE', 'lightyellow' => '#FFFFE0', 'lime' => '#00FF00', 'limegreen' => '#32CD32', 'linen' => '#FAF0E6', 'magenta' => '#FF00FF', 'maroon' => '#800000', 'mediumaquamarine' => '#66CDAA', 'mediumblue' => '#0000CD', 'mediumorchid' => '#BA55D3', 'mediumpurple' => '#9370D8', 'mediumseagreen' => '#3CB371', 'mediumslateblue' => '#7B68EE', 'mediumspringgreen' => '#00FA9A', 'mediumturquoise' => '#48D1CC', 'mediumvioletred' => '#C71585', 'midnightblue' => '#191970', 'mintcream' => '#F5FFFA', 'mistyrose' => '#FFE4E1', 'moccasin' => '#FFE4B5', 'navajowhite' => '#FFDEAD', 'navy' => '#000080', 'oldlace' => '#FDF5E6', 'olive' => '#808000', 'olivedrab' => '#6B8E23', 'orange' => '#FFA500', 'orangered' => '#FF4500', 'orchid' => '#DA70D6', 'palegoldenrod' => '#EEE8AA', 'palegreen' => '#98FB98', 'paleturquoise' => '#AFEEEE', 'palevioletred' => '#D87093', 'papayawhip' => '#FFEFD5', 'peachpuff' => '#FFDAB9', 'peru' => '#CD853F', 'pink' => '#FFC0CB', 'plum' => '#DDA0DD', 'powderblue' => '#B0E0E6', 'purple' => '#800080', 'red' => '#FF0000', 'rosybrown' => '#BC8F8F', 'royalblue' => '#4169E1', 'saddlebrown' => '#8B4513', 'salmon' => '#FA8072', 'sandybrown' => '#F4A460', 'seagreen' => '#2E8B57', 'seashell' => '#FFF5EE', 'sienna' => '#A0522D', 'silver' => '#C0C0C0', 'skyblue' => '#87CEEB', 'slateblue' => '#6A5ACD', 'slategray' => '#708090', 'slategrey' => '#708090', 'snow' => '#FFFAFA', 'springgreen' => '#00FF7F', 'steelblue' => '#4682B4', 'tan' => '#D2B48C', 'teal' => '#008080', 'thistle' => '#D8BFD8', 'tomato' => '#FF6347', 'transparent' => 'transparent', 'turquoise' => '#40E0D0', 'violet' => '#EE82EE', 'wheat' => '#F5DEB3', 'white' => '#FFFFFF', 'whitesmoke' => '#F5F5F5', 'yellow' => '#FFFF00', 'yellowgreen' => '#9ACD32' ); } /** * Used to prepare CSS strings * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class css_writer { /** * The current indent level * @var int */ protected static $indent = 0; /** * Returns true if the output should still maintain minimum formatting. * @return bool */ protected static function is_pretty() { global $CFG; return (!empty($CFG->cssoptimiserpretty)); } /** * Returns the indenting char to use for indenting things nicely. * @return string */ protected static function get_indent() { if (self::is_pretty()) { return str_repeat(" ", self::$indent); } return ''; } /** * Increases the current indent */ protected static function increase_indent() { self::$indent++; } /** * Decreases the current indent */ protected static function decrease_indent() { self::$indent--; } /** * Returns the string to use as a separator * @return string */ protected static function get_separator() { return (self::is_pretty())?"\n":' '; } /** * Returns CSS for media * * @param string $typestring * @param css_rule[] $rules An array of css_rule objects * @return string */ public static function media($typestring, array &$rules) { $nl = self::get_separator(); $output = ''; if ($typestring !== 'all') { $output .= "\n@media {$typestring} {".$nl; self::increase_indent(); } foreach ($rules as $rule) { $output .= $rule->out().$nl; } if ($typestring !== 'all') { self::decrease_indent(); $output .= '}'; } return $output; } /** * Returns CSS for a keyframe * * @param string $for The desired declaration. e.g. keyframes, -moz-keyframes, -webkit-keyframes * @param string $name The name for the keyframe * @param css_rule[] $rules An array of rules belonging to the keyframe * @return string */ public static function keyframe($for, $name, array &$rules) { $output = "\n@{$for} {$name} {"; foreach ($rules as $rule) { $output .= $rule->out(); } $output .= '}'; return $output; } /** * Returns CSS for a rule * * @param string $selector * @param string $styles * @return string */ public static function rule($selector, $styles) { $css = self::get_indent()."{$selector}{{$styles}}"; return $css; } /** * Returns CSS for the selectors of a rule * * @param css_selector[] $selectors Array of css_selector objects * @return string */ public static function selectors(array $selectors) { $nl = self::get_separator(); $selectorstrings = array(); foreach ($selectors as $selector) { $selectorstrings[] = $selector->out(); } return join(','.$nl, $selectorstrings); } /** * Returns a selector given the components that make it up. * * @param array $components * @return string */ public static function selector(array $components) { return trim(join(' ', $components)); } /** * Returns a CSS string for the provided styles * * @param css_style[] $styles Array of css_style objects * @return string */ public static function styles(array $styles) { $bits = array(); foreach ($styles as $style) { // Check if the style is an array. If it is then we are outputing an advanced style. // An advanced style is a style with one or more values, and can occur in situations like background-image // where browse specific values are being used. if (is_array($style)) { /* @var css_style[] $style */ foreach ($style as $advstyle) { $bits[] = $advstyle->out(); } continue; } $bits[] = $style->out(); } return join('', $bits); } /** * Returns a style CSS * * @param string $name * @param string $value * @param bool $important * @return string */ public static function style($name, $value, $important = false) { $value = trim($value); if ($important && strpos($value, '!important') === false) { $value .= ' !important'; } return "{$name}:{$value};"; } } /** * A consolidatable style interface. * * Class that implement this have a short-hand notation for specifying multiple styles. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ interface core_css_consolidatable_style { /** * Used to consolidate several styles into a single "short-hand" style. * @param array $styles * @return mixed */ public static function consolidate(array $styles); } /** * A structure to represent a CSS selector. * * The selector is the classes, id, elements, and psuedo bits that make up a CSS * rule. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_selector { /** * An array of selector bits * @var array */ protected $selectors = array(); /** * The number of selectors. * @var int */ protected $count = 0; /** * Is null if there are no selectors, true if all selectors are basic and false otherwise. * A basic selector is one that consists of just the element type. e.g. div, span, td, a * @var bool|null */ protected $isbasic = null; /** * Initialises a new CSS selector * @return css_selector */ public static function init() { return new css_selector(); } /** * CSS selectors can only be created through the init method above. */ protected function __construct() { // Nothing to do here by default. } /** * Adds a selector to the end of the current selector * @param string $selector */ public function add($selector) { $selector = trim($selector); $count = 0; $count += preg_match_all('/(\.|#)/', $selector, $matchesarray); if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) { $count ++; } // If its already false then no need to continue, its not basic. if ($this->isbasic !== false) { // If theres more than one part making up this selector its not basic. if ($count > 1) { $this->isbasic = false; } else { // Check whether it is a basic element (a-z+) with possible psuedo selector. $this->isbasic = (bool)preg_match('#^[a-z]+(:[a-zA-Z]+)?$#', $selector); } } $this->count = $count; $this->selectors[] = $selector; } /** * Returns the number of individual components that make up this selector * @return int */ public function get_selector_count() { return $this->count; } /** * Returns the selector for use in a CSS rule * @return string */ public function out() { return css_writer::selector($this->selectors); } /** * Returns true is all of the selectors act only upon basic elements (no classes/ids) * @return bool */ public function is_basic() { return ($this->isbasic === true); } } /** * A structure to represent a CSS rule. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_rule { /** * An array of CSS selectors {@link css_selector} * @var css_selector[] */ protected $selectors = array(); /** * An array of CSS styles {@link css_style} * @var css_style[] */ protected $styles = array(); /** * Created a new CSS rule. This is the only way to create a new CSS rule externally. * @return css_rule */ public static function init() { return new css_rule(); } /** * Constructs a new css rule. * * @param string $selector The selector or array of selectors that make up this rule. * @param css_style[] $styles An array of styles that belong to this rule. */ protected function __construct($selector = null, array $styles = array()) { if ($selector != null) { if (is_array($selector)) { $this->selectors = $selector; } else { $this->selectors = array($selector); } $this->add_styles($styles); } } /** * Adds a new CSS selector to this rule * * e.g. $rule->add_selector('.one #two.two'); * * @param css_selector $selector Adds a CSS selector to this rule. */ public function add_selector(css_selector $selector) { $this->selectors[] = $selector; } /** * Adds a new CSS style to this rule. * * @param css_style|string $style Adds a new style to this rule */ public function add_style($style) { if (is_string($style)) { $style = trim($style); if (empty($style)) { return; } $bits = explode(':', $style, 2); if (count($bits) == 2) { list($name, $value) = array_map('trim', $bits); } if (isset($name) && isset($value) && $name !== '' && $value !== '') { $style = css_style::init_automatic($name, $value); } } else if ($style instanceof css_style) { // Clone the style as it may be coming from another rule and we don't // want references as it will likely be overwritten by proceeding // rules. $style = clone($style); } if ($style instanceof css_style) { $name = $style->get_name(); $exists = array_key_exists($name, $this->styles); // We need to find out if the current style support multiple values, or whether the style // is already set up to record multiple values. This can happen with background images which can have single // and multiple values. if ($style->allows_multiple_values() || ($exists && is_array($this->styles[$name]))) { if (!$exists) { $this->styles[$name] = array(); } else if ($this->styles[$name] instanceof css_style) { $this->styles[$name] = array($this->styles[$name]); } $this->styles[$name][] = $style; } else if ($exists) { $this->styles[$name]->set_value($style->get_value()); } else { $this->styles[$name] = $style; } } else if (is_array($style)) { // We probably shouldn't worry about processing styles here but to // be truthful it doesn't hurt. foreach ($style as $astyle) { $this->add_style($astyle); } } } /** * An easy method of adding several styles at once. Just calls add_style. * * This method simply iterates over the array and calls {@link css_rule::add_style()} * with each. * * @param css_style[] $styles Adds an array of styles */ public function add_styles(array $styles) { foreach ($styles as $style) { $this->add_style($style); } } /** * Returns the array of selectors * * @return css_selector[] */ public function get_selectors() { return $this->selectors; } /** * Returns the array of styles * * @return css_style[] */ public function get_styles() { return $this->styles; } /** * Outputs this rule as a fragment of CSS * * @return string */ public function out() { $selectors = css_writer::selectors($this->selectors); $styles = css_writer::styles($this->get_consolidated_styles()); return css_writer::rule($selectors, $styles); } /** * Consolidates all styles associated with this rule * * @return css_style[] An array of consolidated styles */ public function get_consolidated_styles() { /* @var css_style[] $organisedstyles */ $organisedstyles = array(); /* @var css_style[] $finalstyles */ $finalstyles = array(); /* @var core_css_consolidatable_style[] $consolidate */ $consolidate = array(); /* @var css_style[] $advancedstyles */ $advancedstyles = array(); foreach ($this->styles as $style) { // If the style is an array then we are processing an advanced style. An advanced style is a style that can have // one or more values. Background-image is one such example as it can have browser specific styles. if (is_array($style)) { $single = null; $count = 0; foreach ($style as $advstyle) { /* @var css_style $advstyle */ $key = $count++; $advancedstyles[$key] = $advstyle; if (!$advstyle->allows_multiple_values()) { if (!is_null($single)) { unset($advancedstyles[$single]); } $single = $key; } } if (!is_null($single)) { $style = $advancedstyles[$single]; $consolidatetoclass = $style->consolidate_to(); if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) { $class = 'css_style_'.$consolidatetoclass; if (!array_key_exists($class, $consolidate)) { $consolidate[$class] = array(); $organisedstyles[$class] = true; } $consolidate[$class][] = $style; unset($advancedstyles[$single]); } } continue; } $consolidatetoclass = $style->consolidate_to(); if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) { $class = 'css_style_'.$consolidatetoclass; if (!array_key_exists($class, $consolidate)) { $consolidate[$class] = array(); $organisedstyles[$class] = true; } $consolidate[$class][] = $style; } else { $organisedstyles[$style->get_name()] = $style; } } foreach ($consolidate as $class => $styles) { $organisedstyles[$class] = call_user_func(array($class, 'consolidate'), $styles); } foreach ($organisedstyles as $style) { if (is_array($style)) { foreach ($style as $s) { $finalstyles[] = $s; } } else { $finalstyles[] = $style; } } $finalstyles = array_merge($finalstyles, $advancedstyles); return $finalstyles; } /** * Splits this rules into an array of CSS rules. One for each of the selectors * that make up this rule. * * @return css_rule[] */ public function split_by_selector() { $return = array(); foreach ($this->selectors as $selector) { $return[] = new css_rule($selector, $this->styles); } return $return; } /** * Splits this rule into an array of rules. One for each of the styles that * make up this rule * * @return css_rule[] Array of css_rule objects */ public function split_by_style() { $return = array(); foreach ($this->styles as $style) { if (is_array($style)) { $return[] = new css_rule($this->selectors, $style); continue; } $return[] = new css_rule($this->selectors, array($style)); } return $return; } /** * Gets a hash for the styles of this rule * * @return string */ public function get_style_hash() { return md5(css_writer::styles($this->styles)); } /** * Gets a hash for the selectors of this rule * * @return string */ public function get_selector_hash() { return md5(css_writer::selectors($this->selectors)); } /** * Gets the number of selectors that make up this rule. * * @return int */ public function get_selector_count() { $count = 0; foreach ($this->selectors as $selector) { $count += $selector->get_selector_count(); } return $count; } /** * Returns true if there are any errors with this rule. * * @return bool */ public function has_errors() { foreach ($this->styles as $style) { if (is_array($style)) { /* @var css_style[] $style */ foreach ($style as $advstyle) { if ($advstyle->has_error()) { return true; } } continue; } if ($style->has_error()) { return true; } } return false; } /** * Returns the error strings that were recorded when processing this rule. * * Before calling this function you should first call {@link css_rule::has_errors()} * to make sure there are errors (hopefully there arn't). * * @return string */ public function get_error_string() { $css = $this->out(); $errors = array(); foreach ($this->styles as $style) { if (is_array($style)) { /* @var css_style[] $style */ foreach ($style as $advstyle) { if ($advstyle instanceof css_style && $advstyle->has_error()) { $errors[] = " * ".$advstyle->get_last_error(); } } } else if ($style instanceof css_style && $style->has_error()) { $errors[] = " * ".$style->get_last_error(); } } return $css." has the following errors:\n".join("\n", $errors); } /** * Returns true if this rule could be considered a reset rule. * * A reset rule is a rule that acts upon an HTML element and does not include any other parts to its selector. * * @return bool */ public function is_reset_rule() { foreach ($this->selectors as $selector) { if (!$selector->is_basic()) { return false; } } return true; } } /** * An abstract CSS rule collection class. * * This class is extended by things such as media and keyframe declaration. They are declarations that * group rules together for a purpose. * When no declaration is specified rules accumulate into @media all. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class css_rule_collection { /** * An array of rules within this collection instance * @var css_rule[] */ protected $rules = array(); /** * The collection must be able to print itself. */ abstract public function out(); /** * Adds a new CSS rule to this collection instance * * @param css_rule $newrule */ public function add_rule(css_rule $newrule) { foreach ($newrule->split_by_selector() as $rule) { $hash = $rule->get_selector_hash(); if (!array_key_exists($hash, $this->rules)) { $this->rules[$hash] = $rule; } else { $this->rules[$hash]->add_styles($rule->get_styles()); } } } /** * Returns the rules used by this collection * * @return css_rule[] */ public function get_rules() { return $this->rules; } /** * Organises rules by gropuing selectors based upon the styles and consolidating * those selectors into single rules. * * @return bool True if the CSS was optimised by this method */ public function organise_rules_by_selectors() { /* @var css_rule[] $optimisedrules */ $optimisedrules = array(); $beforecount = count($this->rules); $lasthash = null; /* @var css_rule $lastrule */ $lastrule = null; foreach ($this->rules as $rule) { $hash = $rule->get_style_hash(); if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) { foreach ($rule->get_selectors() as $selector) { $lastrule->add_selector($selector); } continue; } $lastrule = clone($rule); $lasthash = $hash; $optimisedrules[] = $lastrule; } $this->rules = array(); foreach ($optimisedrules as $optimised) { $this->rules[$optimised->get_selector_hash()] = $optimised; } $aftercount = count($this->rules); return ($beforecount < $aftercount); } /** * Returns the total number of rules that exist within this collection * * @return int */ public function count_rules() { return count($this->rules); } /** * Returns the total number of selectors that exist within this collection * * @return int */ public function count_selectors() { $count = 0; foreach ($this->rules as $rule) { $count += $rule->get_selector_count(); } return $count; } /** * Returns true if the collection has any rules that have errors * * @return boolean */ public function has_errors() { foreach ($this->rules as $rule) { if ($rule->has_errors()) { return true; } } return false; } /** * Returns any errors that have happened within rules in this collection. * * @return string[] */ public function get_errors() { $errors = array(); foreach ($this->rules as $rule) { if ($rule->has_errors()) { $errors[] = $rule->get_error_string(); } } return $errors; } } /** * A media class to organise rules by the media they apply to. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_media extends css_rule_collection { /** * An array of the different media types this instance applies to. * @var array */ protected $types = array(); /** * Initalises a new media instance * * @param string $for The media that the contained rules are destined for. */ public function __construct($for = 'all') { $types = explode(',', $for); $this->types = array_map('trim', $types); } /** * Returns the CSS for this media and all of its rules. * * @return string */ public function out() { return css_writer::media(join(',', $this->types), $this->rules); } /** * Returns an array of media that this media instance applies to * * @return array */ public function get_types() { return $this->types; } /** * Returns all of the reset rules known by this media set. * @param bool $remove If set to true reset rules will be removed before being returned. * @return array */ public function get_reset_rules($remove = false) { $resetrules = array(); foreach ($this->rules as $key => $rule) { if ($rule->is_reset_rule()) { $resetrules[] = clone $rule; if ($remove) { unset($this->rules[$key]); } } } return $resetrules; } } /** * A media class to organise rules by the media they apply to. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_keyframe extends css_rule_collection { /** * The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes * @var string */ protected $for; /** * The name for the keyframes * @var string */ protected $name; /** * Constructs a new keyframe * * @param string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes * @param string $name The name for the keyframes */ public function __construct($for, $name) { $this->for = $for; $this->name = $name; } /** * Returns the directive of this keyframe * * e.g. keyframes, -moz-keyframes, -webkit-keyframes * @return string */ public function get_for() { return $this->for; } /** * Returns the name of this keyframe * @return string */ public function get_name() { return $this->name; } /** * Returns the CSS for this collection of keyframes and all of its rules. * * @return string */ public function out() { return css_writer::keyframe($this->for, $this->name, $this->rules); } } /** * An absract class to represent CSS styles * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ abstract class css_style { /** Constant used for recongise a special empty value in a CSS style */ const NULL_VALUE = '@@$NULL$@@'; /** * The name of the style * @var string */ protected $name; /** * The value for the style * @var mixed */ protected $value; /** * If set to true this style was defined with the !important rule. * Only trolls use !important. * Don't hide under bridges.. its not good for your skin. Do the proper thing * and fix the issue don't just force a fix that will undoubtedly one day * lead to further frustration. * @var bool */ protected $important = false; /** * Gets set to true if this style has an error * @var bool */ protected $error = false; /** * The last error message that occured * @var string */ protected $errormessage = null; /** * Initialises a new style. * * This is the only public way to create a style to ensure they that appropriate * style class is used if it exists. * * @param string $name The name of the style. * @param string $value The value of the style. * @return css_style_generic */ public static function init_automatic($name, $value) { $cleanedname = preg_replace('#[^a-zA-Z0-9]+#', '', $name); $specificclass = 'css_style_'.$cleanedname; if (class_exists($specificclass)) { $style = call_user_func(array($specificclass, 'init'), $value); if ($cleanedname !== $name && !is_array($style)) { $style->set_actual_name($name); } return $style; } return new css_style_generic($name, $value); } /** * Creates a new style when given its name and value * * @param string $name The name of the style. * @param string $value The value of the style. */ protected function __construct($name, $value) { $this->name = $name; $this->set_value($value); } /** * Sets the value for the style * * @param string $value */ final public function set_value($value) { $value = trim($value); $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches); if ($important) { $value = substr($value, 0, -(strlen($matches[1]))); $value = rtrim($value); } if (!$this->important || $important) { $this->value = $this->clean_value($value); $this->important = $important; } if (!$this->is_valid()) { $this->set_error('Invalid value for '.$this->name); } } /** * Returns true if the value associated with this style is valid * * @return bool */ public function is_valid() { return true; } /** * Returns the name for the style * * @return string */ public function get_name() { return $this->name; } /** * Returns the value for the style * * @param bool $includeimportant If set to true and the rule is important !important postfix will be used. * @return string */ public function get_value($includeimportant = true) { $value = $this->value; if ($includeimportant && $this->important) { $value .= ' !important'; } return $value; } /** * Returns the style ready for use in CSS * * @param string|null $value A value to use to override the value for this style. * @return string */ public function out($value = null) { if (is_null($value)) { $value = $this->get_value(); } return css_writer::style($this->name, $value, $this->important); } /** * This can be overridden by a specific style allowing it to clean its values * consistently. * * @param mixed $value * @return mixed */ protected function clean_value($value) { return $value; } /** * If this particular style can be consolidated into another style this function * should return the style that it can be consolidated into. * * @return string|null */ public function consolidate_to() { return null; } /** * Sets the last error message. * * @param string $message */ protected function set_error($message) { $this->error = true; $this->errormessage = $message; } /** * Returns true if an error has occured * * @return bool */ public function has_error() { return $this->error; } /** * Returns the last error that occured or null if no errors have happened. * * @return string */ public function get_last_error() { return $this->errormessage; } /** * Returns true if the value for this style is the special null value. * * This should only be overriden in circumstances where a shorthand style can lead * to move explicit styles being overwritten. Not a common place occurenace. * * Example: * This occurs if the shorthand background property was used but no proper value * was specified for this style. * This leads to a null value being used unless otherwise overridden. * * @return bool */ public function is_special_empty_value() { return false; } /** * Returns true if this style permits multiple values. * * This occurs for styles such as background image that can have browser specific values that need to be maintained because * of course we don't know what browser the user is using, and optimisation occurs before caching. * Thus we must always server all values we encounter in the order we encounter them for when this is set to true. * * @return boolean False by default, true if the style supports muliple values. */ public function allows_multiple_values() { return false; } /** * Returns true if this style was marked important. * @return bool */ public function is_important() { return !empty($this->important); } /** * Sets the important flag for this style and its current value. * @param bool $important */ public function set_important($important = true) { $this->important = (bool) $important; } /** * Sets the actual name used within the style. * * This method allows us to support browser hacks like *width:0; * * @param string $name */ public function set_actual_name($name) { $this->name = $name; } } /** * A generic CSS style class to use when a more specific class does not exist. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_generic extends css_style { /** * Cleans incoming values for typical things that can be optimised. * * @param mixed $value Cleans the provided value optimising it if possible * @return string */ protected function clean_value($value) { if (trim($value) == '0px') { $value = 0; } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) { $value = '#'.strtoupper($matches[1]); } return $value; } } /** * A colour CSS style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_color extends css_style { /** * Creates a new colour style * * @param mixed $value Initialises a new colour style * @return css_style_color */ public static function init($value) { return new css_style_color('color', $value); } /** * Cleans the colour unifing it to a 6 char hash colour if possible * Doing this allows us to associate identical colours being specified in * different ways. e.g. Red, red, #F00, and #F00000 * * @param mixed $value Cleans the provided value optimising it if possible * @return string */ protected function clean_value($value) { $value = trim($value); if (css_is_colour($value)) { if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) { $value = '#'.strtoupper($matches[1]); } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) { $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3]; $value = '#'.strtoupper($value); } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) { $value = css_optimiser::$htmlcolours[strtolower($value)]; } } return $value; } /** * Returns the colour style for use within CSS. * Will return an optimised hash colour. * * e.g #123456 * #123 instead of #112233 * #F00 instead of red * * @param string $overridevalue If provided then this value will be used instead * of the styles current value. * @return string */ public function out($overridevalue = null) { if ($overridevalue === null) { $overridevalue = $this->value; } return parent::out(self::shrink_value($overridevalue)); } /** * Shrinks the colour value is possible. * * @param string $value Shrinks the current value to an optimial form if possible * @return string */ public static function shrink_value($value) { if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) { return '#'.$matches[1].$matches[2].$matches[3]; } return $value; } /** * Returns true if the value is a valid colour. * * @return bool */ public function is_valid() { return css_is_colour($this->value); } } /** * A width style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_width extends css_style { /** * Checks if the width is valid * @return bool */ public function is_valid() { return css_is_width($this->value); } /** * Cleans the provided value * * @param mixed $value Cleans the provided value optimising it if possible * @return string */ protected function clean_value($value) { if (!css_is_width($value)) { // Note we don't actually change the value to something valid. That // would be bad for futureproofing. $this->set_error('Invalid width specified for '.$this->name); } else if (preg_match('#^0\D+$#', $value)) { $value = 0; } return trim($value); } /** * Initialises a new width style * * @param mixed $value The value this style has * @return css_style_width */ public static function init($value) { return new css_style_width('width', $value); } } /** * A margin style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_margin extends css_style_width implements core_css_consolidatable_style { /** * Initialises a margin style. * * In this case we split the margin into several other margin styles so that * we can properly condense overrides and then reconsolidate them later into * an optimal form. * * @param string $value The value the style has * @return array An array of margin values that can later be consolidated */ public static function init($value) { $important = ''; if (strpos($value, '!important') !== false) { $important = ' !important'; $value = str_replace('!important', '', $value); } $value = preg_replace('#\s+#', ' ', trim($value)); $bits = explode(' ', $value, 4); $top = $right = $bottom = $left = null; if (count($bits) > 0) { $top = $right = $bottom = $left = array_shift($bits); } if (count($bits) > 0) { $right = $left = array_shift($bits); } if (count($bits) > 0) { $bottom = array_shift($bits); } if (count($bits) > 0) { $left = array_shift($bits); } return array( new css_style_margintop('margin-top', $top.$important), new css_style_marginright('margin-right', $right.$important), new css_style_marginbottom('margin-bottom', $bottom.$important), new css_style_marginleft('margin-left', $left.$important) ); } /** * Consolidates individual margin styles into a single margin style * * @param css_style[] $styles * @return css_style[] An array of consolidated styles */ public static function consolidate(array $styles) { if (count($styles) != 4) { return $styles; } $someimportant = false; $allimportant = null; $notimportantequal = null; $firstvalue = null; foreach ($styles as $style) { if ($style->is_important()) { $someimportant = true; if ($allimportant === null) { $allimportant = true; } } else { if ($allimportant === true) { $allimportant = false; } if ($firstvalue == null) { $firstvalue = $style->get_value(false); $notimportantequal = true; } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) { $notimportantequal = false; } } } if ($someimportant && !$allimportant && !$notimportantequal) { return $styles; } if ($someimportant && !$allimportant && $notimportantequal) { $return = array( new css_style_margin('margin', $firstvalue) ); foreach ($styles as $style) { if ($style->is_important()) { $return[] = $style; } } return $return; } else { $top = null; $right = null; $bottom = null; $left = null; foreach ($styles as $style) { switch ($style->get_name()) { case 'margin-top' : $top = $style->get_value(false); break; case 'margin-right' : $right = $style->get_value(false); break; case 'margin-bottom' : $bottom = $style->get_value(false); break; case 'margin-left' : $left = $style->get_value(false); break; } } if ($top == $bottom && $left == $right) { if ($top == $left) { $returnstyle = new css_style_margin('margin', $top); } else { $returnstyle = new css_style_margin('margin', "{$top} {$left}"); } } else if ($left == $right) { $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom}"); } else { $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}"); } if ($allimportant) { $returnstyle->set_important(); } return array($returnstyle); } } } /** * A margin top style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_margintop extends css_style_margin { /** * A simple init, just a single style * * @param string $value The value the style has * @return css_style_margintop */ public static function init($value) { return new css_style_margintop('margin-top', $value); } /** * This style can be consolidated into a single margin style * * @return string */ public function consolidate_to() { return 'margin'; } } /** * A margin right style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_marginright extends css_style_margin { /** * A simple init, just a single style * * @param string $value The value the style has * @return css_style_margintop */ public static function init($value) { return new css_style_marginright('margin-right', $value); } /** * This style can be consolidated into a single margin style * * @return string */ public function consolidate_to() { return 'margin'; } } /** * A margin bottom style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_marginbottom extends css_style_margin { /** * A simple init, just a single style * * @param string $value The value the style has * @return css_style_margintop */ public static function init($value) { return new css_style_marginbottom('margin-bottom', $value); } /** * This style can be consolidated into a single margin style * * @return string */ public function consolidate_to() { return 'margin'; } } /** * A margin left style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_marginleft extends css_style_margin { /** * A simple init, just a single style * * @param string $value The value the style has * @return css_style_margintop */ public static function init($value) { return new css_style_marginleft('margin-left', $value); } /** * This style can be consolidated into a single margin style * * @return string */ public function consolidate_to() { return 'margin'; } } /** * A border style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_border extends css_style implements core_css_consolidatable_style { /** * Initalises the border style into an array of individual style compontents * * @param string $value The value the style has * @return css_style_bordercolor */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 3); $return = array(); if (count($bits) > 0) { $width = array_shift($bits); if (!css_style_borderwidth::is_border_width($width)) { $width = '0'; } $return[] = css_style_bordertopwidth::init($width); $return[] = css_style_borderrightwidth::init($width); $return[] = css_style_borderbottomwidth::init($width); $return[] = css_style_borderleftwidth::init($width); } if (count($bits) > 0) { $style = array_shift($bits); $return[] = css_style_bordertopstyle::init($style); $return[] = css_style_borderrightstyle::init($style); $return[] = css_style_borderbottomstyle::init($style); $return[] = css_style_borderleftstyle::init($style); } if (count($bits) > 0) { $colour = array_shift($bits); $return[] = css_style_bordertopcolor::init($colour); $return[] = css_style_borderrightcolor::init($colour); $return[] = css_style_borderbottomcolor::init($colour); $return[] = css_style_borderleftcolor::init($colour); } return $return; } /** * Consolidates all border styles into a single style * * @param css_style[] $styles An array of border styles * @return css_style[] An optimised array of border styles */ public static function consolidate(array $styles) { $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null); $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null); $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null); foreach ($styles as $style) { switch ($style->get_name()) { case 'border-top-width': $borderwidths['top'] = $style->get_value(); break; case 'border-right-width': $borderwidths['right'] = $style->get_value(); break; case 'border-bottom-width': $borderwidths['bottom'] = $style->get_value(); break; case 'border-left-width': $borderwidths['left'] = $style->get_value(); break; case 'border-top-style': $borderstyles['top'] = $style->get_value(); break; case 'border-right-style': $borderstyles['right'] = $style->get_value(); break; case 'border-bottom-style': $borderstyles['bottom'] = $style->get_value(); break; case 'border-left-style': $borderstyles['left'] = $style->get_value(); break; case 'border-top-color': $bordercolors['top'] = css_style_color::shrink_value($style->get_value()); break; case 'border-right-color': $bordercolors['right'] = css_style_color::shrink_value($style->get_value()); break; case 'border-bottom-color': $bordercolors['bottom'] = css_style_color::shrink_value($style->get_value()); break; case 'border-left-color': $bordercolors['left'] = css_style_color::shrink_value($style->get_value()); break; } } $uniquewidths = count(array_unique($borderwidths)); $uniquestyles = count(array_unique($borderstyles)); $uniquecolors = count(array_unique($bordercolors)); $nullwidths = in_array(null, $borderwidths, true); $nullstyles = in_array(null, $borderstyles, true); $nullcolors = in_array(null, $bordercolors, true); $allwidthsthesame = ($uniquewidths === 1)?1:0; $allstylesthesame = ($uniquestyles === 1)?1:0; $allcolorsthesame = ($uniquecolors === 1)?1:0; $allwidthsnull = $allwidthsthesame && $nullwidths; $allstylesnull = $allstylesthesame && $nullstyles; $allcolorsnull = $allcolorsthesame && $nullcolors; /* @var css_style[] $return */ $return = array(); if ($allwidthsnull && $allstylesnull && $allcolorsnull) { // Everything is null still... boo. return array(new css_style_border('border', '')); } else if ($allwidthsnull && $allstylesnull) { self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors); return $return; } else if ($allwidthsnull && $allcolorsnull) { self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles); return $return; } else if ($allcolorsnull && $allstylesnull) { self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths); return $return; } if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 3) { $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']); } else if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 2) { if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) { $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']); self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors); } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) { $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']); self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles); } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) { $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']); self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths); } else { self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths); self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles); self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors); } } else if (!$nullwidths && !$nullcolors && !$nullstyles && max(array_count_values($borderwidths)) == 3 && max(array_count_values($borderstyles)) == 3 && max(array_count_values($bordercolors)) == 3) { $widthkeys = array(); $stylekeys = array(); $colorkeys = array(); foreach ($borderwidths as $key => $value) { if (!array_key_exists($value, $widthkeys)) { $widthkeys[$value] = array(); } $widthkeys[$value][] = $key; } usort($widthkeys, 'css_sort_by_count'); $widthkeys = array_values($widthkeys); foreach ($borderstyles as $key => $value) { if (!array_key_exists($value, $stylekeys)) { $stylekeys[$value] = array(); } $stylekeys[$value][] = $key; } usort($stylekeys, 'css_sort_by_count'); $stylekeys = array_values($stylekeys); foreach ($bordercolors as $key => $value) { if (!array_key_exists($value, $colorkeys)) { $colorkeys[$value] = array(); } $colorkeys[$value][] = $key; } usort($colorkeys, 'css_sort_by_count'); $colorkeys = array_values($colorkeys); if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) { $key = $widthkeys[0][0]; self::build_style_string($return, 'css_style_border', 'border', $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]); $key = $widthkeys[1][0]; self::build_style_string($return, 'css_style_border'.$key, 'border-'.$key, $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]); } else { self::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']); self::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']); self::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']); self::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']); } } else { self::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']); self::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']); self::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']); self::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']); } foreach ($return as $key => $style) { if ($style->get_value() == '') { unset($return[$key]); } } return $return; } /** * Border styles get consolidated to a single border style. * * @return string */ public function consolidate_to() { return 'border'; } /** * Consolidates a series of border styles into an optimised array of border * styles by looking at the direction of the border and prioritising that * during the optimisation. * * @param array $array An array to add styles into during consolidation. Passed by reference. * @param string $class The class type to initalise * @param string $style The style to create * @param string|array $top The top value * @param string $right The right value * @param string $bottom The bottom value * @param string $left The left value * @return bool */ public static function consolidate_styles_by_direction(&$array, $class, $style, $top, $right = null, $bottom = null, $left = null) { if (is_array($top)) { $right = $top['right']; $bottom = $top['bottom']; $left = $top['left']; $top = $top['top']; } if ($top == $bottom && $left == $right && $top == $left) { if (is_null($top)) { $array[] = new $class($style, ''); } else { $array[] = new $class($style, $top); } } else if ($top == null || $right == null || $bottom == null || $left == null) { if ($top !== null) { $array[] = new $class(str_replace('border-', 'border-top-', $style), $top); } if ($right !== null) { $array[] = new $class(str_replace('border-', 'border-right-', $style), $right); } if ($bottom !== null) { $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom); } if ($left !== null) { $array[] = new $class(str_replace('border-', 'border-left-', $style), $left); } } else if ($top == $bottom && $left == $right) { $array[] = new $class($style, $top.' '.$right); } else if ($left == $right) { $array[] = new $class($style, $top.' '.$right.' '.$bottom); } else { $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left); } return true; } /** * Builds a border style for a set of width, style, and colour values * * @param array $array An array into which the generated style is added * @param string $class The class type to initialise * @param string $cssstyle The style to use * @param string $width The width of the border * @param string $style The style of the border * @param string $color The colour of the border * @return bool */ public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) { if (!is_null($width) && !is_null($style) && !is_null($color)) { $array[] = new $class($cssstyle, $width.' '.$style.' '.$color); } else if (!is_null($width) && !is_null($style) && is_null($color)) { $array[] = new $class($cssstyle, $width.' '.$style); } else if (!is_null($width) && is_null($style) && is_null($color)) { $array[] = new $class($cssstyle, $width); } else { if (!is_null($width)) { $array[] = new $class($cssstyle, $width); } if (!is_null($style)) { $array[] = new $class($cssstyle, $style); } if (!is_null($color)) { $array[] = new $class($cssstyle, $color); } } return true; } } /** * A border colour style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_bordercolor extends css_style_color { /** * Creates a new border colour style * * Based upon the colour style * * @param mixed $value * @return Array of css_style_bordercolor */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 4); $top = $right = $bottom = $left = null; if (count($bits) > 0) { $top = $right = $bottom = $left = array_shift($bits); } if (count($bits) > 0) { $right = $left = array_shift($bits); } if (count($bits) > 0) { $bottom = array_shift($bits); } if (count($bits) > 0) { $left = array_shift($bits); } return array( css_style_bordertopcolor::init($top), css_style_borderrightcolor::init($right), css_style_borderbottomcolor::init($bottom), css_style_borderleftcolor::init($left) ); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } /** * Cleans the value * * @param string $value Cleans the provided value optimising it if possible * @return string */ protected function clean_value($value) { $values = explode(' ', $value); $values = array_map('parent::clean_value', $values); return join (' ', $values); } /** * Outputs this style * * @param string $overridevalue * @return string */ public function out($overridevalue = null) { if ($overridevalue === null) { $overridevalue = $this->value; } $values = explode(' ', $overridevalue); $values = array_map('css_style_color::shrink_value', $values); return parent::out(join (' ', $values)); } } /** * A border left style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderleft extends css_style_generic { /** * Initialises the border left style into individual components * * @param string $value * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 3); $return = array(); if (count($bits) > 0) { $return[] = css_style_borderleftwidth::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_borderleftstyle::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_borderleftcolor::init(array_shift($bits)); } return $return; } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border right style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderright extends css_style_generic { /** * Initialises the border right style into individual components * * @param string $value The value of the style * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 3); $return = array(); if (count($bits) > 0) { $return[] = css_style_borderrightwidth::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_borderrightstyle::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_borderrightcolor::init(array_shift($bits)); } return $return; } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border top style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_bordertop extends css_style_generic { /** * Initialises the border top style into individual components * * @param string $value The value of the style * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 3); $return = array(); if (count($bits) > 0) { $return[] = css_style_bordertopwidth::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_bordertopstyle::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_bordertopcolor::init(array_shift($bits)); } return $return; } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border bottom style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderbottom extends css_style_generic { /** * Initialises the border bottom style into individual components * * @param string $value The value of the style * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 3); $return = array(); if (count($bits) > 0) { $return[] = css_style_borderbottomwidth::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_borderbottomstyle::init(array_shift($bits)); } if (count($bits) > 0) { $return[] = css_style_borderbottomcolor::init(array_shift($bits)); } return $return; } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border width style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderwidth extends css_style_width { /** * Creates a new border colour style * * Based upon the colour style * * @param string $value The value of the style * @return array Array of css_style_border*width */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 4); $top = $right = $bottom = $left = null; if (count($bits) > 0) { $top = $right = $bottom = $left = array_shift($bits); } if (count($bits) > 0) { $right = $left = array_shift($bits); } if (count($bits) > 0) { $bottom = array_shift($bits); } if (count($bits) > 0) { $left = array_shift($bits); } return array( css_style_bordertopwidth::init($top), css_style_borderrightwidth::init($right), css_style_borderbottomwidth::init($bottom), css_style_borderleftwidth::init($left) ); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } /** * Checks if the width is valid * @return bool */ public function is_valid() { return self::is_border_width($this->value); } /** * Cleans the provided value * * @param mixed $value Cleans the provided value optimising it if possible * @return string */ protected function clean_value($value) { $isvalid = self::is_border_width($value); if (!$isvalid) { $this->set_error('Invalid width specified for '.$this->name); } else if (preg_match('#^0\D+$#', $value)) { return '0'; } return trim($value); } /** * Returns true if the provided value is a permitted border width * @param string $value The value to check * @return bool */ public static function is_border_width($value) { $altwidthvalues = array('thin', 'medium', 'thick'); return css_is_width($value) || in_array($value, $altwidthvalues); } } /** * A border style style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderstyle extends css_style_generic { /** * Creates a new border colour style * * Based upon the colour style * * @param string $value The value of the style * @return array Array of css_style_border*style */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value, 4); $top = $right = $bottom = $left = null; if (count($bits) > 0) { $top = $right = $bottom = $left = array_shift($bits); } if (count($bits) > 0) { $right = $left = array_shift($bits); } if (count($bits) > 0) { $bottom = array_shift($bits); } if (count($bits) > 0) { $left = array_shift($bits); } return array( css_style_bordertopstyle::init($top), css_style_borderrightstyle::init($right), css_style_borderbottomstyle::init($bottom), css_style_borderleftstyle::init($left) ); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border top colour style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_bordertopcolor extends css_style_bordercolor { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_bordertopcolor */ public static function init($value) { return new css_style_bordertopcolor('border-top-color', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border left colour style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderleftcolor extends css_style_bordercolor { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderleftcolor */ public static function init($value) { return new css_style_borderleftcolor('border-left-color', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border right colour style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderrightcolor extends css_style_bordercolor { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderrightcolor */ public static function init($value) { return new css_style_borderrightcolor('border-right-color', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border bottom colour style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderbottomcolor extends css_style_bordercolor { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderbottomcolor */ public static function init($value) { return new css_style_borderbottomcolor('border-bottom-color', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border width top style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_bordertopwidth extends css_style_borderwidth { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_bordertopwidth */ public static function init($value) { return new css_style_bordertopwidth('border-top-width', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border width left style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderleftwidth extends css_style_borderwidth { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderleftwidth */ public static function init($value) { return new css_style_borderleftwidth('border-left-width', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border width right style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderrightwidth extends css_style_borderwidth { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderrightwidth */ public static function init($value) { return new css_style_borderrightwidth('border-right-width', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border width bottom style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderbottomwidth extends css_style_borderwidth { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderbottomwidth */ public static function init($value) { return new css_style_borderbottomwidth('border-bottom-width', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border top style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_bordertopstyle extends css_style_borderstyle { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_bordertopstyle */ public static function init($value) { return new css_style_bordertopstyle('border-top-style', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border left style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderleftstyle extends css_style_borderstyle { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderleftstyle */ public static function init($value) { return new css_style_borderleftstyle('border-left-style', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border right style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderrightstyle extends css_style_borderstyle { /** * Initialises this style object * * @param string $value The value of the style * @return css_style_borderrightstyle */ public static function init($value) { return new css_style_borderrightstyle('border-right-style', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A border bottom style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_borderbottomstyle extends css_style_borderstyle { /** * Initialises this style object * * @param string $value The value for the style * @return css_style_borderbottomstyle */ public static function init($value) { return new css_style_borderbottomstyle('border-bottom-style', $value); } /** * Consolidate this to a single border style * * @return string */ public function consolidate_to() { return 'border'; } } /** * A background style * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_background extends css_style implements core_css_consolidatable_style { /** * Initialises a background style * * @param string $value The value of the style * @return array An array of background component. */ public static function init($value) { // Colour - image - repeat - attachment - position. $imageurl = null; if (preg_match('#url\(([^\)]+)\)#', $value, $matches)) { $imageurl = trim($matches[1]); $value = str_replace($matches[1], '', $value); } // Switch out the brackets so that they don't get messed up when we explode. $brackets = array(); $bracketcount = 0; while (preg_match('#\([^\)\(]+\)#', $value, $matches)) { $key = "##BRACKET-{$bracketcount}##"; $bracketcount++; $brackets[$key] = $matches[0]; $value = str_replace($matches[0], $key, $value); } $important = (stripos($value, '!important') !== false); if ($important) { // Great some genius put !important in the background shorthand property. $value = str_replace('!important', '', $value); } $value = preg_replace('#\s+#', ' ', $value); $bits = explode(' ', $value); foreach ($bits as $key => $bit) { $bits[$key] = self::replace_bracket_placeholders($bit, $brackets); } unset($bracketcount); unset($brackets); $repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit'); $attachments = array('scroll' , 'fixed', 'inherit'); $positions = array('top', 'left', 'bottom', 'right', 'center'); /* @var css_style_background[] $return */ $return = array(); $unknownbits = array(); $color = self::NULL_VALUE; if (count($bits) > 0 && css_is_colour(reset($bits))) { $color = array_shift($bits); } $image = self::NULL_VALUE; if (count($bits) > 0 && preg_match('#^\s*(none|inherit|url\(\))\s*$#', reset($bits))) { $image = array_shift($bits); if ($image == 'url()') { $image = "url({$imageurl})"; } } $repeat = self::NULL_VALUE; if (count($bits) > 0 && in_array(reset($bits), $repeats)) { $repeat = array_shift($bits); } $attachment = self::NULL_VALUE; if (count($bits) > 0 && in_array(reset($bits), $attachments)) { // Scroll , fixed, inherit. $attachment = array_shift($bits); } $position = self::NULL_VALUE; if (count($bits) > 0) { $widthbits = array(); foreach ($bits as $bit) { if (in_array($bit, $positions) || css_is_width($bit)) { $widthbits[] = $bit; } else { $unknownbits[] = $bit; } } if (count($widthbits)) { $position = join(' ', $widthbits); } } if (count($unknownbits)) { foreach ($unknownbits as $bit) { $bit = trim($bit); if ($color === self::NULL_VALUE && css_is_colour($bit)) { $color = $bit; } else if ($repeat === self::NULL_VALUE && in_array($bit, $repeats)) { $repeat = $bit; } else if ($attachment === self::NULL_VALUE && in_array($bit, $attachments)) { $attachment = $bit; } else if ($bit !== '') { $advanced = css_style_background_advanced::init($bit); if ($important) { $advanced->set_important(); } $return[] = $advanced; } } } if ($color === self::NULL_VALUE && $image === self::NULL_VALUE && $repeat === self::NULL_VALUE && $attachment === self::NULL_VALUE && $position === self::NULL_VALUE) { // All primaries are null, return without doing anything else. There may be advanced madness there. return $return; } $return[] = css_style_backgroundcolor::init($color); $return[] = css_style_backgroundimage::init($image); $return[] = css_style_backgroundrepeat::init($repeat); $return[] = css_style_backgroundattachment::init($attachment); $return[] = css_style_backgroundposition::init($position); if ($important) { foreach ($return as $style) { $style->set_important(); } } return $return; } /** * Static helper method to switch in bracket replacements * * @param string $value * @param array $placeholders * @return string */ protected static function replace_bracket_placeholders($value, array $placeholders) { while (preg_match('/##BRACKET-\d+##/', $value, $matches)) { $value = str_replace($matches[0], $placeholders[$matches[0]], $value); } return $value; } /** * Consolidates background styles into a single background style * * @param css_style_background[] $styles Consolidates the provided array of background styles * @return css_style[] Consolidated optimised background styles */ public static function consolidate(array $styles) { if (empty($styles)) { return $styles; } $color = null; $image = null; $repeat = null; $attachment = null; $position = null; $size = null; $origin = null; $clip = null; $someimportant = false; $allimportant = null; foreach ($styles as $style) { if ($style instanceof css_style_backgroundimage_advanced) { continue; } if ($style->is_important()) { $someimportant = true; if ($allimportant === null) { $allimportant = true; } } else if ($allimportant === true) { $allimportant = false; } } /* @var css_style[] $organisedstyles */ $organisedstyles = array(); /* @var css_style[] $advancedstyles */ $advancedstyles = array(); /* @var css_style[] $importantstyles */ $importantstyles = array(); foreach ($styles as $style) { if ($style instanceof css_style_backgroundimage_advanced) { $advancedstyles[] = $style; continue; } if ($someimportant && !$allimportant && $style->is_important()) { $importantstyles[] = $style; continue; } $organisedstyles[$style->get_name()] = $style; switch ($style->get_name()) { case 'background-color' : $color = css_style_color::shrink_value($style->get_value(false)); break; case 'background-image' : $image = $style->get_value(false); break; case 'background-repeat' : $repeat = $style->get_value(false); break; case 'background-attachment' : $attachment = $style->get_value(false); break; case 'background-position' : $position = $style->get_value(false); break; case 'background-clip' : $clip = $style->get_value(); break; case 'background-origin' : $origin = $style->get_value(); break; case 'background-size' : $size = $style->get_value(); break; } } /* @var css_style[] $consolidatetosingle */ $consolidatetosingle = array(); if (!is_null($color) && !is_null($image) && !is_null($repeat) && !is_null($attachment) && !is_null($position)) { // We can use the shorthand background-style! if (!$organisedstyles['background-color']->is_special_empty_value()) { $consolidatetosingle[] = $color; } if (!$organisedstyles['background-image']->is_special_empty_value()) { $consolidatetosingle[] = $image; } if (!$organisedstyles['background-repeat']->is_special_empty_value()) { $consolidatetosingle[] = $repeat; } if (!$organisedstyles['background-attachment']->is_special_empty_value()) { $consolidatetosingle[] = $attachment; } if (!$organisedstyles['background-position']->is_special_empty_value()) { $consolidatetosingle[] = $position; } // Reset them all to null so we don't use them again. $color = null; $image = null; $repeat = null; $attachment = null; $position = null; } $return = array(); // Single background style needs to come first. if (count($consolidatetosingle) > 0) { $returnstyle = new css_style_background('background', join(' ', $consolidatetosingle)); if ($allimportant) { $returnstyle->set_important(); } $return[] = $returnstyle; } foreach ($styles as $style) { $value = null; switch ($style->get_name()) { case 'background-color' : $value = $color; break; case 'background-image' : $value = $image; break; case 'background-repeat' : $value = $repeat; break; case 'background-attachment' : $value = $attachment; break; case 'background-position' : $value = $position; break; case 'background-clip' : $value = $clip; break; case 'background-origin': $value = $origin; break; case 'background-size': $value = $size; break; } if (!is_null($value)) { $return[] = $style; } } $return = array_merge($return, $importantstyles, $advancedstyles); return $return; } } /** * A advanced background style that allows multiple values to preserve unknown entities * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_background_advanced extends css_style_generic { /** * Creates a new background colour style * * @param string $value The value of the style * @return css_style_backgroundimage */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); return new css_style_background_advanced('background', $value); } /** * Returns true because the advanced background image supports multiple values. * e.g. -webkit-linear-gradient and -moz-linear-gradient. * * @return boolean */ public function allows_multiple_values() { return true; } } /** * A background colour style. * * Based upon the colour style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundcolor extends css_style_color { /** * Creates a new background colour style * * @param string $value The value of the style * @return css_style_backgroundcolor */ public static function init($value) { return new css_style_backgroundcolor('background-color', $value); } /** * css_style_backgroundcolor consolidates to css_style_background * * @return string */ public function consolidate_to() { return 'background'; } /** * Returns true if the value for this style is the special null value. * * This occurs if the shorthand background property was used but no proper value * was specified for this style. * This leads to a null value being used unless otherwise overridden. * * @return bool */ public function is_special_empty_value() { return ($this->value === self::NULL_VALUE); } /** * Returns true if the value for this style is valid * @return bool */ public function is_valid() { return $this->is_special_empty_value() || parent::is_valid(); } } /** * A background image style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundimage extends css_style_generic { /** * Creates a new background image style * * @param string $value The value of the style * @return css_style_backgroundimage */ public static function init($value) { if ($value !== self::NULL_VALUE && !preg_match('#^\s*(none|inherit|url\()#i', $value)) { return css_style_backgroundimage_advanced::init($value); } return new css_style_backgroundimage('background-image', $value); } /** * Consolidates this style into a single background style * * @return string */ public function consolidate_to() { return 'background'; } /** * Returns true if the value for this style is the special null value. * * This occurs if the shorthand background property was used but no proper value * was specified for this style. * This leads to a null value being used unless otherwise overridden. * * @return bool */ public function is_special_empty_value() { return ($this->value === self::NULL_VALUE); } /** * Returns true if the value for this style is valid * @return bool */ public function is_valid() { return $this->is_special_empty_value() || parent::is_valid(); } } /** * A background image style that supports multiple values and masquerades as a background-image * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundimage_advanced extends css_style_generic { /** * Creates a new background colour style * * @param string $value The value of the style * @return css_style_backgroundimage */ public static function init($value) { $value = preg_replace('#\s+#', ' ', $value); return new css_style_backgroundimage_advanced('background-image', $value); } /** * Returns true because the advanced background image supports multiple values. * e.g. -webkit-linear-gradient and -moz-linear-gradient. * * @return boolean */ public function allows_multiple_values() { return true; } } /** * A background repeat style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundrepeat extends css_style_generic { /** * Creates a new background colour style * * @param string $value The value of the style * @return css_style_backgroundrepeat */ public static function init($value) { return new css_style_backgroundrepeat('background-repeat', $value); } /** * Consolidates this style into a single background style * * @return string */ public function consolidate_to() { return 'background'; } /** * Returns true if the value for this style is the special null value. * * This occurs if the shorthand background property was used but no proper value * was specified for this style. * This leads to a null value being used unless otherwise overridden. * * @return bool */ public function is_special_empty_value() { return ($this->value === self::NULL_VALUE); } /** * Returns true if the value for this style is valid * @return bool */ public function is_valid() { return $this->is_special_empty_value() || parent::is_valid(); } } /** * A background attachment style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundattachment extends css_style_generic { /** * Creates a new background colour style * * @param string $value The value of the style * @return css_style_backgroundattachment */ public static function init($value) { return new css_style_backgroundattachment('background-attachment', $value); } /** * Consolidates this style into a single background style * * @return string */ public function consolidate_to() { return 'background'; } /** * Returns true if the value for this style is the special null value. * * This occurs if the shorthand background property was used but no proper value * was specified for this style. * This leads to a null value being used unless otherwise overridden. * * @return bool */ public function is_special_empty_value() { return ($this->value === self::NULL_VALUE); } /** * Returns true if the value for this style is valid * @return bool */ public function is_valid() { return $this->is_special_empty_value() || parent::is_valid(); } } /** * A background position style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundposition extends css_style_generic { /** * Creates a new background colour style * * @param string $value The value of the style * @return css_style_backgroundposition */ public static function init($value) { return new css_style_backgroundposition('background-position', $value); } /** * Consolidates this style into a single background style * * @return string */ public function consolidate_to() { return 'background'; } /** * Returns true if the value for this style is the special null value. * * This occurs if the shorthand background property was used but no proper value * was specified for this style. * This leads to a null value being used unless otherwise overridden. * * @return bool */ public function is_special_empty_value() { return ($this->value === self::NULL_VALUE); } /** * Returns true if the value for this style is valid * @return bool */ public function is_valid() { return $this->is_special_empty_value() || parent::is_valid(); } } /** * A background size style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundsize extends css_style_generic { /** * Creates a new background size style * * @param string $value The value of the style * @return css_style_backgroundposition */ public static function init($value) { return new css_style_backgroundsize('background-size', $value); } /** * Consolidates this style into a single background style * * @return string */ public function consolidate_to() { return 'background'; } } /** * A background clip style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundclip extends css_style_generic { /** * Creates a new background clip style * * @param string $value The value of the style * @return css_style_backgroundposition */ public static function init($value) { return new css_style_backgroundclip('background-clip', $value); } /** * Consolidates this style into a single background style * * @return string */ public function consolidate_to() { return 'background'; } } /** * A background origin style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_backgroundorigin extends css_style_generic { /** * Creates a new background origin style * * @param string $value The value of the style * @return css_style_backgroundposition */ public static function init($value) { return new css_style_backgroundorigin('background-origin', $value); } /** * Consolidates this style into a single background style * * @return string */ public function consolidate_to() { return 'background'; } } /** * A padding style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_padding extends css_style_width implements core_css_consolidatable_style { /** * Initialises this padding style into several individual padding styles * * @param string $value The value fo the style * @return array An array of padding styles */ public static function init($value) { $important = ''; if (strpos($value, '!important') !== false) { $important = ' !important'; $value = str_replace('!important', '', $value); } $value = preg_replace('#\s+#', ' ', trim($value)); $bits = explode(' ', $value, 4); $top = $right = $bottom = $left = null; if (count($bits) > 0) { $top = $right = $bottom = $left = array_shift($bits); } if (count($bits) > 0) { $right = $left = array_shift($bits); } if (count($bits) > 0) { $bottom = array_shift($bits); } if (count($bits) > 0) { $left = array_shift($bits); } return array( new css_style_paddingtop('padding-top', $top.$important), new css_style_paddingright('padding-right', $right.$important), new css_style_paddingbottom('padding-bottom', $bottom.$important), new css_style_paddingleft('padding-left', $left.$important) ); } /** * Consolidates several padding styles into a single style. * * @param css_style_padding[] $styles Array of padding styles * @return css_style[] Optimised+consolidated array of padding styles */ public static function consolidate(array $styles) { if (count($styles) != 4) { return $styles; } $someimportant = false; $allimportant = null; $notimportantequal = null; $firstvalue = null; foreach ($styles as $style) { if ($style->is_important()) { $someimportant = true; if ($allimportant === null) { $allimportant = true; } } else { if ($allimportant === true) { $allimportant = false; } if ($firstvalue == null) { $firstvalue = $style->get_value(false); $notimportantequal = true; } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) { $notimportantequal = false; } } } if ($someimportant && !$allimportant && !$notimportantequal) { return $styles; } if ($someimportant && !$allimportant && $notimportantequal) { $return = array( new css_style_padding('padding', $firstvalue) ); foreach ($styles as $style) { if ($style->is_important()) { $return[] = $style; } } return $return; } else { $top = null; $right = null; $bottom = null; $left = null; foreach ($styles as $style) { switch ($style->get_name()) { case 'padding-top' : $top = $style->get_value(false); break; case 'padding-right' : $right = $style->get_value(false); break; case 'padding-bottom' : $bottom = $style->get_value(false); break; case 'padding-left' : $left = $style->get_value(false); break; } } if ($top == $bottom && $left == $right) { if ($top == $left) { $returnstyle = new css_style_padding('padding', $top); } else { $returnstyle = new css_style_padding('padding', "{$top} {$left}"); } } else if ($left == $right) { $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom}"); } else { $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom} {$left}"); } if ($allimportant) { $returnstyle->set_important(); } return array($returnstyle); } } } /** * A padding top style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_paddingtop extends css_style_padding { /** * Initialises this style * * @param string $value The value of the style * @return css_style_paddingtop */ public static function init($value) { return new css_style_paddingtop('padding-top', $value); } /** * Consolidates this style into a single padding style * * @return string */ public function consolidate_to() { return 'padding'; } } /** * A padding right style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_paddingright extends css_style_padding { /** * Initialises this style * * @param string $value The value of the style * @return css_style_paddingright */ public static function init($value) { return new css_style_paddingright('padding-right', $value); } /** * Consolidates this style into a single padding style * * @return string */ public function consolidate_to() { return 'padding'; } } /** * A padding bottom style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_paddingbottom extends css_style_padding { /** * Initialises this style * * @param string $value The value of the style * @return css_style_paddingbottom */ public static function init($value) { return new css_style_paddingbottom('padding-bottom', $value); } /** * Consolidates this style into a single padding style * * @return string */ public function consolidate_to() { return 'padding'; } } /** * A padding left style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_paddingleft extends css_style_padding { /** * Initialises this style * * @param string $value The value of the style * @return css_style_paddingleft */ public static function init($value) { return new css_style_paddingleft('padding-left', $value); } /** * Consolidates this style into a single padding style * * @return string */ public function consolidate_to() { return 'padding'; } } /** * A cursor style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_cursor extends css_style_generic { /** * Initialises a new cursor style * @param string $value * @return css_style_cursor */ public static function init($value) { return new css_style_cursor('cursor', $value); } /** * Cleans the given value and returns it. * * @param string $value * @return string */ protected function clean_value($value) { // Allowed values for the cursor style. $allowed = array('auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize', 'pointer', 'progress', 's-resize', 'se-resize', 'sw-resize', 'text', 'w-resize', 'wait', 'inherit'); // Has to be one of the allowed values of an image to use. Loosely match the image... doesn't need to be thorough. if (!in_array($value, $allowed) && !preg_match('#\.[a-zA-Z0-9_\-]{1,5}$#', $value)) { $this->set_error('Invalid or unexpected cursor value specified: '.$value); } return trim($value); } } /** * A vertical alignment style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_verticalalign extends css_style_generic { /** * Initialises a new vertical alignment style * @param string $value * @return css_style_verticalalign */ public static function init($value) { return new css_style_verticalalign('vertical-align', $value); } /** * Cleans the given value and returns it. * * @param string $value * @return string */ protected function clean_value($value) { $allowed = array('baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'inherit'); if (!css_is_width($value) && !in_array($value, $allowed)) { $this->set_error('Invalid vertical-align value specified: '.$value); } return trim($value); } } /** * A float style. * * @package core * @subpackage cssoptimiser * @copyright 2012 Sam Hemelryk * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later */ class css_style_float extends css_style_generic { /** * Initialises a new float style * @param string $value * @return css_style_float */ public static function init($value) { return new css_style_float('float', $value); } /** * Cleans the given value and returns it. * * @param string $value * @return string */ protected function clean_value($value) { $allowed = array('left', 'right', 'none', 'inherit'); if (!css_is_width($value) && !in_array($value, $allowed)) { $this->set_error('Invalid float value specified: '.$value); } return trim($value); } }