/lib/csslib.php
PHP | 4999 lines | 2531 code | 408 blank | 2060 comment | 513 complexity | 7bb012abb03559126e97ab4ff43083e8 MD5 | raw file
Possible License(s): MPL-2.0-no-copyleft-exception, LGPL-3.0, GPL-3.0, LGPL-2.1, Apache-2.0, BSD-3-Clause, AGPL-3.0
Large files files are truncated, but you can click here to view the full file
- <?php
- // This file is part of Moodle - http://moodle.org/
- //
- // Moodle is free software: you can redistribute it and/or modify
- // it under the terms of the GNU General Public License as published by
- // the Free Software Foundation, either version 3 of the License, or
- // (at your option) any later version.
- //
- // Moodle is distributed in the hope that it will be useful,
- // but WITHOUT ANY WARRANTY; without even the implied warranty of
- // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- // GNU General Public License for more details.
- //
- // You should have received a copy of the GNU General Public License
- // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
- /**
- * This file contains CSS related class, and function for the CSS optimiser
- *
- * Please see the {@link css_optimiser} class for greater detail.
- *
- * @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();
- /**
- * 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 array $cssfiles The CSS files to store.
- * @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, array $cssfiles, $chunk = false, $chunkurl = null) {
- global $CFG;
- $css = '';
- foreach ($cssfiles as $file) {
- $css .= file_get_contents($file)."\n";
- }
- // Check if both the CSS optimiser is enabled and the theme supports it.
- if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) {
- // This is an experimental feature introduced in Moodle 2.3
- // The CSS optimiser organises the CSS in order to reduce the overall number
- // of rules and styles being sent to the client. It does this by collating
- // the CSS before it is cached removing excess styles and rules and stripping
- // out any extraneous content such as comments and empty rules.
- $optimiser = new css_optimiser;
- $css = $theme->post_process($css);
- $css = $optimiser->process($css);
- // If cssoptimisestats is set then stats from the optimisation are collected
- // and output at the beginning of the CSS.
- if (!empty($CFG->cssoptimiserstats)) {
- $css = $optimiser->output_stats_css().$css;
- }
- } else {
- // This is the default behaviour.
- // The cssoptimise setting was introduced in Moodle 2.3 and will hopefully
- // in the future be changed from an experimental setting to the default.
- // The css_minify_css will method will use the Minify library remove
- // comments, additional whitespace and other minor measures to reduce the
- // the overall CSS being sent.
- // However it has the distinct disadvantage of having to minify the CSS
- // before running the post process functions. Potentially things may break
- // here if theme designers try to push things with CSS post processing.
- $css = $theme->post_process($css);
- $css = core_minify::css($css);
- }
- 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, $css);
- if ($chunk) {
- // If we need to chunk the CSS for browsers that are sub-par.
- $css = css_chunk_by_selector_count($css, $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 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' => '#2…
Large files files are truncated, but you can click here to view the full file