Merge branch 'MDL-42592_master' of https://github.com/markn86/moodle
[moodle.git] / lib / csslib.php
blob2f9eeb7a43f2458151d39cc253d14735c80e3af0
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * This file contains CSS related class, and function for the CSS optimiser
20 * Please see the {@link css_optimiser} class for greater detail.
22 * @package core
23 * @subpackage cssoptimiser
24 * @copyright 2012 Sam Hemelryk
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 defined('MOODLE_INTERNAL') || die();
30 /**
31 * Stores CSS in a file at the given path.
33 * This function either succeeds or throws an exception.
35 * @param theme_config $theme The theme that the CSS belongs to.
36 * @param string $csspath The path to store the CSS at.
37 * @param array $cssfiles The CSS files to store.
38 * @param bool $chunk If set to true these files will be chunked to ensure
39 * that no one file contains more than 4095 selectors.
40 * @param string $chunkurl If the CSS is be chunked then we need to know the URL
41 * to use for the chunked files.
43 function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) {
44 global $CFG;
46 $css = '';
47 foreach ($cssfiles as $file) {
48 $css .= file_get_contents($file)."\n";
51 // Check if both the CSS optimiser is enabled and the theme supports it.
52 if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) {
53 // This is an experimental feature introduced in Moodle 2.3
54 // The CSS optimiser organises the CSS in order to reduce the overall number
55 // of rules and styles being sent to the client. It does this by collating
56 // the CSS before it is cached removing excess styles and rules and stripping
57 // out any extraneous content such as comments and empty rules.
58 $optimiser = new css_optimiser;
59 $css = $theme->post_process($css);
60 $css = $optimiser->process($css);
62 // If cssoptimisestats is set then stats from the optimisation are collected
63 // and output at the beginning of the CSS.
64 if (!empty($CFG->cssoptimiserstats)) {
65 $css = $optimiser->output_stats_css().$css;
67 } else {
68 // This is the default behaviour.
69 // The cssoptimise setting was introduced in Moodle 2.3 and will hopefully
70 // in the future be changed from an experimental setting to the default.
71 // The css_minify_css will method will use the Minify library remove
72 // comments, additional whitespace and other minor measures to reduce the
73 // the overall CSS being sent.
74 // However it has the distinct disadvantage of having to minify the CSS
75 // before running the post process functions. Potentially things may break
76 // here if theme designers try to push things with CSS post processing.
77 $css = $theme->post_process($css);
78 $css = core_minify::css($css);
81 clearstatcache();
82 if (!file_exists(dirname($csspath))) {
83 @mkdir(dirname($csspath), $CFG->directorypermissions, true);
86 // Prevent serving of incomplete file from concurrent request,
87 // the rename() should be more atomic than fwrite().
88 ignore_user_abort(true);
90 // First up write out the single file for all those using decent browsers.
91 css_write_file($csspath, $css);
93 if ($chunk) {
94 // If we need to chunk the CSS for browsers that are sub-par.
95 $css = css_chunk_by_selector_count($css, $chunkurl);
96 $files = count($css);
97 $count = 1;
98 foreach ($css as $content) {
99 if ($count === $files) {
100 // If there is more than one file and this IS the last file.
101 $filename = preg_replace('#\.css$#', '.0.css', $csspath);
102 } else {
103 // If there is more than one file and this is not the last file.
104 $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
106 $count++;
107 css_write_file($filename, $content);
111 ignore_user_abort(false);
112 if (connection_aborted()) {
113 die;
118 * Writes a CSS file.
120 * @param string $filename
121 * @param string $content
123 function css_write_file($filename, $content) {
124 global $CFG;
125 if ($fp = fopen($filename.'.tmp', 'xb')) {
126 fwrite($fp, $content);
127 fclose($fp);
128 rename($filename.'.tmp', $filename);
129 @chmod($filename, $CFG->filepermissions);
130 @unlink($filename.'.tmp'); // Just in case anything fails.
135 * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
137 * @param string $css The CSS to chunk.
138 * @param string $importurl The URL to use for import statements.
139 * @param int $maxselectors The number of selectors to limit a chunk to.
140 * @param int $buffer The buffer size to use when chunking. You shouldn't need to reduce this
141 * unless you are lowering the maximum selectors.
142 * @return array An array of CSS chunks.
144 function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
145 // Check if we need to chunk this CSS file.
146 $count = substr_count($css, ',') + substr_count($css, '{');
147 if ($count < $maxselectors) {
148 // The number of selectors is less then the max - we're fine.
149 return array($css);
152 // Chunk time ?!
153 // Split the CSS by array, making sure to save the delimiter in the process.
154 $parts = preg_split('#([,\}])#', $css, null, PREG_SPLIT_DELIM_CAPTURE + PREG_SPLIT_NO_EMPTY);
155 // We need to chunk the array. Each delimiter is stored separately so we multiple by 2.
156 // We also subtract 100 to give us a small buffer just in case.
157 $parts = array_chunk($parts, $maxselectors * 2 - $buffer * 2);
158 $css = array();
159 $partcount = count($parts);
160 foreach ($parts as $key => $chunk) {
161 if (end($chunk) === ',') {
162 // Damn last element was a comma.
163 // Pretty much the only way to deal with this is to take the styles from the end of the
164 // comma separated chain of selectors and apply it to the last selector we have here in place
165 // of the comma.
166 // Unit tests are essential for making sure this works.
167 $styles = false;
168 $i = $key;
169 while ($styles === false && $i < ($partcount - 1)) {
170 $i++;
171 $nextpart = $parts[$i];
172 foreach ($nextpart as $style) {
173 if (strpos($style, '{') !== false) {
174 $styles = preg_replace('#^[^\{]+#', '', $style);
175 break;
179 if ($styles === false) {
180 $styles = '/** Error chunking CSS **/';
181 } else {
182 $styles .= '}';
184 array_pop($chunk);
185 array_push($chunk, $styles);
187 $css[] = join('', $chunk);
189 // The array $css now contains CSS split into perfect sized chunks.
190 // Import statements can only appear at the very top of a CSS file.
191 // Imported sheets are applied in the the order they are imported and
192 // are followed by the contents of the CSS.
193 // This is terrible for performance.
194 // It means we must put the import statements at the top of the last chunk
195 // to ensure that things are always applied in the correct order.
196 // This way the chunked files are included in the order they were chunked
197 // followed by the contents of the final chunk in the actual sheet.
198 $importcss = '';
199 $slashargs = strpos($importurl, '.php?') === false;
200 $parts = count($css);
201 for ($i = 1; $i < $parts; $i++) {
202 if ($slashargs) {
203 $importcss .= "@import url({$importurl}/chunk{$i});\n";
204 } else {
205 $importcss .= "@import url({$importurl}&chunk={$i});\n";
208 $importcss .= end($css);
209 $css[key($css)] = $importcss;
211 return $css;
215 * Sends a cached CSS file
217 * This function sends the cached CSS file. Remember it is generated on the first
218 * request, then optimised/minified, and finally cached for serving.
220 * @param string $csspath The path to the CSS file we want to serve.
221 * @param string $etag The revision to make sure we utilise any caches.
223 function css_send_cached_css($csspath, $etag) {
224 // 60 days only - the revision may get incremented quite often.
225 $lifetime = 60*60*24*60;
227 header('Etag: "'.$etag.'"');
228 header('Content-Disposition: inline; filename="styles.php"');
229 header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
230 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
231 header('Pragma: ');
232 header('Cache-Control: public, max-age='.$lifetime);
233 header('Accept-Ranges: none');
234 header('Content-Type: text/css; charset=utf-8');
235 if (!min_enable_zlib_compression()) {
236 header('Content-Length: '.filesize($csspath));
239 readfile($csspath);
240 die;
244 * Sends CSS directly without caching it.
246 * This function takes a raw CSS string, optimises it if required, and then
247 * serves it.
248 * Turning both themedesignermode and CSS optimiser on at the same time is awful
249 * for performance because of the optimiser running here. However it was done so
250 * that theme designers could utilise the optimised output during development to
251 * help them optimise their CSS... not that they should write lazy CSS.
253 * @param string $css
255 function css_send_uncached_css($css) {
256 header('Content-Disposition: inline; filename="styles_debug.php"');
257 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
258 header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT');
259 header('Pragma: ');
260 header('Accept-Ranges: none');
261 header('Content-Type: text/css; charset=utf-8');
263 if (is_array($css)) {
264 $css = implode("\n\n", $css);
266 echo $css;
267 die;
271 * Send file not modified headers
273 * @param int $lastmodified
274 * @param string $etag
276 function css_send_unmodified($lastmodified, $etag) {
277 // 60 days only - the revision may get incremented quite often.
278 $lifetime = 60*60*24*60;
279 header('HTTP/1.1 304 Not Modified');
280 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
281 header('Cache-Control: public, max-age='.$lifetime);
282 header('Content-Type: text/css; charset=utf-8');
283 header('Etag: "'.$etag.'"');
284 if ($lastmodified) {
285 header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
287 die;
291 * Sends a 404 message about CSS not being found.
293 function css_send_css_not_found() {
294 header('HTTP/1.0 404 not found');
295 die('CSS was not found, sorry.');
299 * Determines if the given value is a valid CSS colour.
301 * A CSS colour can be one of the following:
302 * - Hex colour: #AA66BB
303 * - RGB colour: rgb(0-255, 0-255, 0-255)
304 * - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1)
305 * - HSL colour: hsl(0-360, 0-100%, 0-100%)
306 * - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1)
308 * Or a recognised browser colour mapping {@link css_optimiser::$htmlcolours}
310 * @param string $value The colour value to check
311 * @return bool
313 function css_is_colour($value) {
314 $value = trim($value);
316 $hex = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/';
317 $rgb = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i';
318 $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
319 $hsl = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i';
320 $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
322 if (in_array(strtolower($value), array('inherit'))) {
323 return true;
324 } else if (preg_match($hex, $value)) {
325 return true;
326 } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
327 return true;
328 } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
329 // It is an RGB colour.
330 return true;
331 } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
332 // It is an RGBA colour.
333 return true;
334 } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
335 // It is an HSL colour.
336 return true;
337 } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
338 // It is an HSLA colour.
339 return true;
341 // Doesn't look like a colour.
342 return false;
346 * Returns true is the passed value looks like a CSS width.
347 * In order to pass this test the value must be purely numerical or end with a
348 * valid CSS unit term.
350 * @param string|int $value
351 * @return boolean
353 function css_is_width($value) {
354 $value = trim($value);
355 if (in_array(strtolower($value), array('auto', 'inherit'))) {
356 return true;
358 if ((string)$value === '0' || preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) {
359 return true;
361 return false;
365 * A simple sorting function to sort two array values on the number of items they contain
367 * @param array $a
368 * @param array $b
369 * @return int
371 function css_sort_by_count(array $a, array $b) {
372 $a = count($a);
373 $b = count($b);
374 if ($a == $b) {
375 return 0;
377 return ($a > $b) ? -1 : 1;
381 * A basic CSS optimiser that strips out unwanted things and then processes CSS organising and cleaning styles.
383 * This CSS optimiser works by reading through a CSS string one character at a
384 * time and building an object structure of the CSS.
385 * As part of that processing styles are expanded out as much as they can be to
386 * ensure we collect all mappings, at the end of the processing those styles are
387 * then combined into an optimised form to keep them as short as possible.
389 * @package core
390 * @subpackage cssoptimiser
391 * @copyright 2012 Sam Hemelryk
392 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
394 class css_optimiser {
397 * Used when the processor is about to start processing.
398 * Processing states. Used internally.
400 const PROCESSING_START = 0;
403 * Used when the processor is currently processing a selector.
404 * Processing states. Used internally.
406 const PROCESSING_SELECTORS = 0;
409 * Used when the processor is currently processing a style.
410 * Processing states. Used internally.
412 const PROCESSING_STYLES = 1;
415 * Used when the processor is currently processing a comment.
416 * Processing states. Used internally.
418 const PROCESSING_COMMENT = 2;
421 * Used when the processor is currently processing an @ rule.
422 * Processing states. Used internally.
424 const PROCESSING_ATRULE = 3;
427 * The raw string length before optimisation.
428 * Stats variables set during and after processing
429 * @var int
431 protected $rawstrlen = 0;
434 * The number of comments that were removed during optimisation.
435 * Stats variables set during and after processing
436 * @var int
438 protected $commentsincss = 0;
441 * The number of rules in the CSS before optimisation.
442 * Stats variables set during and after processing
443 * @var int
445 protected $rawrules = 0;
448 * The number of selectors using in CSS rules before optimisation.
449 * Stats variables set during and after processing
450 * @var int
452 protected $rawselectors = 0;
455 * The string length after optimisation.
456 * Stats variables set during and after processing
457 * @var int
459 protected $optimisedstrlen = 0;
462 * The number of rules after optimisation.
463 * Stats variables set during and after processing
464 * @var int
466 protected $optimisedrules = 0;
469 * The number of selectors used in rules after optimisation.
470 * Stats variables set during and after processing
471 * @var int
473 protected $optimisedselectors = 0;
476 * The start time of the optimisation.
477 * Stats variables set during and after processing
478 * @var int
480 protected $timestart = 0;
483 * The end time of the optimisation.
484 * Stats variables set during and after processing
485 * @var int
487 protected $timecomplete = 0;
490 * Will be set to any errors that may have occured during processing.
491 * This is updated only at the end of processing NOT during.
493 * @var array
495 protected $errors = array();
498 * Processes incoming CSS optimising it and then returning it.
500 * @param string $css The raw CSS to optimise
501 * @return string The optimised CSS
503 public function process($css) {
504 // Easiest win there is.
505 $css = trim($css);
507 $this->reset_stats();
508 $this->timestart = microtime(true);
509 $this->rawstrlen = strlen($css);
511 // Don't try to process files with no content... it just doesn't make sense.
512 // But we should produce an error for them, an empty CSS file will lead to a
513 // useless request for those running theme designer mode.
514 if ($this->rawstrlen === 0) {
515 $this->errors[] = 'Skipping file as it has no content.';
516 return '';
519 // First up we need to remove all line breaks - this allows us to instantly
520 // reduce our processing requirements and as we will process everything
521 // into a new structure there's really nothing lost.
522 $css = preg_replace('#\r?\n#', ' ', $css);
524 // Next remove the comments... no need to them in an optimised world and
525 // knowing they're all gone allows us to REALLY make our processing simpler.
526 $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
528 $medias = array(
529 'all' => new css_media()
531 $imports = array();
532 $charset = false;
533 // Keyframes are used for CSS animation they will be processed right at the very end.
534 $keyframes = array();
536 $currentprocess = self::PROCESSING_START;
537 $currentrule = css_rule::init();
538 $currentselector = css_selector::init();
539 $inquotes = false; // ' or "
540 $inbraces = false; // {
541 $inbrackets = false; // [
542 $inparenthesis = false; // (
543 /* @var css_media $currentmedia */
544 $currentmedia = $medias['all'];
545 $currentatrule = null;
546 $suspectatrule = false;
548 $buffer = '';
549 $char = null;
551 // Next we are going to iterate over every single character in $css.
552 // This is why we removed line breaks and comments!
553 for ($i = 0; $i < $this->rawstrlen; $i++) {
554 $lastchar = $char;
555 $char = substr($css, $i, 1);
556 if ($char == '@' && $buffer == '') {
557 $suspectatrule = true;
559 switch ($currentprocess) {
560 // Start processing an @ rule e.g. @media, @page, @keyframes.
561 case self::PROCESSING_ATRULE:
562 switch ($char) {
563 case ';':
564 if (!$inbraces) {
565 $buffer .= $char;
566 if ($currentatrule == 'import') {
567 $imports[] = $buffer;
568 $currentprocess = self::PROCESSING_SELECTORS;
569 } else if ($currentatrule == 'charset') {
570 $charset = $buffer;
571 $currentprocess = self::PROCESSING_SELECTORS;
574 if ($currentatrule !== 'media') {
575 $buffer = '';
576 $currentatrule = false;
578 // Continue 1: The switch processing chars
579 // Continue 2: The switch processing the state
580 // Continue 3: The for loop.
581 continue 3;
582 case '{':
583 $regexmediabasic = '#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#';
584 $regexadvmedia = '#\s*@media\s*([^{]+)#';
585 $regexkeyframes = '#@((\-moz\-|\-webkit\-|\-ms\-|\-o\-)?keyframes)\s*([^\s]+)#';
587 if ($currentatrule == 'media' && preg_match($regexmediabasic, $buffer, $matches)) {
588 // Basic media declaration.
589 $mediatypes = str_replace(' ', '', $matches[1]);
590 if (!array_key_exists($mediatypes, $medias)) {
591 $medias[$mediatypes] = new css_media($mediatypes);
593 $currentmedia = $medias[$mediatypes];
594 $currentprocess = self::PROCESSING_SELECTORS;
595 $buffer = '';
596 } else if ($currentatrule == 'media' && preg_match($regexadvmedia, $buffer, $matches)) {
597 // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/.
598 $mediatypes = $matches[1];
599 $hash = md5($mediatypes);
600 $medias[$hash] = new css_media($mediatypes);
601 $currentmedia = $medias[$hash];
602 $currentprocess = self::PROCESSING_SELECTORS;
603 $buffer = '';
604 } else if ($currentatrule == 'keyframes' && preg_match($regexkeyframes, $buffer, $matches)) {
605 // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow
606 // them to be overridden to ensure we don't mess anything up. (means we keep everything in order).
607 $keyframefor = $matches[1];
608 $keyframename = $matches[3];
609 $keyframe = new css_keyframe($keyframefor, $keyframename);
610 $keyframes[] = $keyframe;
611 $currentmedia = $keyframe;
612 $currentprocess = self::PROCESSING_SELECTORS;
613 $buffer = '';
615 // Continue 1: The switch processing chars
616 // Continue 2: The switch processing the state
617 // Continue 3: The for loop.
618 continue 3;
620 break;
621 // Start processing selectors.
622 case self::PROCESSING_START:
623 case self::PROCESSING_SELECTORS:
624 $regexatrule = '#@(media|import|charset|(\-moz\-|\-webkit\-|\-ms\-|\-o\-)?(keyframes))\s*#';
625 switch ($char) {
626 case '[':
627 $inbrackets ++;
628 $buffer .= $char;
629 // Continue 1: The switch processing chars
630 // Continue 2: The switch processing the state
631 // Continue 3: The for loop.
632 continue 3;
633 case ']':
634 $inbrackets --;
635 $buffer .= $char;
636 // Continue 1: The switch processing chars
637 // Continue 2: The switch processing the state
638 // Continue 3: The for loop.
639 continue 3;
640 case ' ':
641 if ($inbrackets) {
642 // Continue 1: The switch processing chars
643 // Continue 2: The switch processing the state
644 // Continue 3: The for loop.
645 continue 3;
647 if (!empty($buffer)) {
648 // Check for known @ rules.
649 if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
650 $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
651 $currentprocess = self::PROCESSING_ATRULE;
652 $buffer .= $char;
653 } else {
654 $currentselector->add($buffer);
655 $buffer = '';
658 $suspectatrule = false;
659 // Continue 1: The switch processing chars
660 // Continue 2: The switch processing the state
661 // Continue 3: The for loop.
662 continue 3;
663 case '{':
664 if ($inbrackets) {
665 // Continue 1: The switch processing chars
666 // Continue 2: The switch processing the state
667 // Continue 3: The for loop.
668 continue 3;
670 // Check for known @ rules.
671 if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
672 // Ahh we've been in an @rule, lets rewind one and have the @rule case process this.
673 $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
674 $currentprocess = self::PROCESSING_ATRULE;
675 $i--;
676 $suspectatrule = false;
677 // Continue 1: The switch processing chars
678 // Continue 2: The switch processing the state
679 // Continue 3: The for loop.
680 continue 3;
682 if ($buffer !== '') {
683 $currentselector->add($buffer);
685 $currentrule->add_selector($currentselector);
686 $currentselector = css_selector::init();
687 $currentprocess = self::PROCESSING_STYLES;
689 $buffer = '';
690 // Continue 1: The switch processing chars
691 // Continue 2: The switch processing the state
692 // Continue 3: The for loop.
693 continue 3;
694 case '}':
695 if ($inbrackets) {
696 // Continue 1: The switch processing chars
697 // Continue 2: The switch processing the state
698 // Continue 3: The for loop.
699 continue 3;
701 if ($currentatrule == 'media') {
702 $currentmedia = $medias['all'];
703 $currentatrule = false;
704 $buffer = '';
705 } else if (strpos($currentatrule, 'keyframes') !== false) {
706 $currentmedia = $medias['all'];
707 $currentatrule = false;
708 $buffer = '';
710 // Continue 1: The switch processing chars
711 // Continue 2: The switch processing the state
712 // Continue 3: The for loop.
713 continue 3;
714 case ',':
715 if ($inbrackets) {
716 // Continue 1: The switch processing chars
717 // Continue 2: The switch processing the state
718 // Continue 3: The for loop.
719 continue 3;
721 $currentselector->add($buffer);
722 $currentrule->add_selector($currentselector);
723 $currentselector = css_selector::init();
724 $buffer = '';
725 // Continue 1: The switch processing chars
726 // Continue 2: The switch processing the state
727 // Continue 3: The for loop.
728 continue 3;
730 break;
731 // Start processing styles.
732 case self::PROCESSING_STYLES:
733 if ($char == '"' || $char == "'") {
734 if ($inquotes === false) {
735 $inquotes = $char;
737 if ($inquotes === $char && $lastchar !== '\\') {
738 $inquotes = false;
741 if ($inquotes) {
742 $buffer .= $char;
743 continue 2;
745 switch ($char) {
746 case ';':
747 if ($inparenthesis) {
748 $buffer .= $char;
749 // Continue 1: The switch processing chars
750 // Continue 2: The switch processing the state
751 // Continue 3: The for loop.
752 continue 3;
754 $currentrule->add_style($buffer);
755 $buffer = '';
756 $inquotes = false;
757 // Continue 1: The switch processing chars
758 // Continue 2: The switch processing the state
759 // Continue 3: The for loop.
760 continue 3;
761 case '}':
762 $currentrule->add_style($buffer);
763 $this->rawselectors += $currentrule->get_selector_count();
765 $currentmedia->add_rule($currentrule);
767 $currentrule = css_rule::init();
768 $currentprocess = self::PROCESSING_SELECTORS;
769 $this->rawrules++;
770 $buffer = '';
771 $inquotes = false;
772 $inparenthesis = false;
773 // Continue 1: The switch processing chars
774 // Continue 2: The switch processing the state
775 // Continue 3: The for loop.
776 continue 3;
777 case '(':
778 $inparenthesis = true;
779 $buffer .= $char;
780 // Continue 1: The switch processing chars
781 // Continue 2: The switch processing the state
782 // Continue 3: The for loop.
783 continue 3;
784 case ')':
785 $inparenthesis = false;
786 $buffer .= $char;
787 // Continue 1: The switch processing chars
788 // Continue 2: The switch processing the state
789 // Continue 3: The for loop.
790 continue 3;
792 break;
794 $buffer .= $char;
797 foreach ($medias as $media) {
798 $this->optimise($media);
800 $css = $this->produce_css($charset, $imports, $medias, $keyframes);
802 $this->timecomplete = microtime(true);
803 return trim($css);
807 * Produces CSS for the given charset, imports, media, and keyframes
808 * @param string $charset
809 * @param array $imports
810 * @param css_media[] $medias
811 * @param css_keyframe[] $keyframes
812 * @return string
814 protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
815 $css = '';
816 if (!empty($charset)) {
817 $imports[] = $charset;
819 if (!empty($imports)) {
820 $css .= implode("\n", $imports);
821 $css .= "\n\n";
824 $cssreset = array();
825 $cssstandard = array();
826 $csskeyframes = array();
828 // Process each media declaration individually.
829 foreach ($medias as $media) {
830 // If this declaration applies to all media types.
831 if (in_array('all', $media->get_types())) {
832 // Collect all rules that represet reset rules and remove them from the media object at the same time.
833 // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they
834 // can't end up out of order because of optimisation.
835 $resetrules = $media->get_reset_rules(true);
836 if (!empty($resetrules)) {
837 $cssreset[] = css_writer::media('all', $resetrules);
840 // Get the standard cSS.
841 $cssstandard[] = $media->out();
844 // Finally if there are any keyframe declarations process them now.
845 if (count($keyframes) > 0) {
846 foreach ($keyframes as $keyframe) {
847 $this->optimisedrules += $keyframe->count_rules();
848 $this->optimisedselectors += $keyframe->count_selectors();
849 if ($keyframe->has_errors()) {
850 $this->errors += $keyframe->get_errors();
852 $csskeyframes[] = $keyframe->out();
856 // Join it all together.
857 $css .= join('', $cssreset);
858 $css .= join('', $cssstandard);
859 $css .= join('', $csskeyframes);
861 // Record the strlenght of the now optimised CSS.
862 $this->optimisedstrlen = strlen($css);
864 // Return the now produced CSS.
865 return $css;
869 * Optimises the CSS rules within a rule collection of one form or another
871 * @param css_rule_collection $media
872 * @return void This function acts in reference
874 protected function optimise(css_rule_collection $media) {
875 $media->organise_rules_by_selectors();
876 $this->optimisedrules += $media->count_rules();
877 $this->optimisedselectors += $media->count_selectors();
878 if ($media->has_errors()) {
879 $this->errors += $media->get_errors();
884 * Returns an array of stats from the last processing run
885 * @return string
887 public function get_stats() {
888 $stats = array(
889 'timestart' => $this->timestart,
890 'timecomplete' => $this->timecomplete,
891 'timetaken' => round($this->timecomplete - $this->timestart, 4),
892 'commentsincss' => $this->commentsincss,
893 'rawstrlen' => $this->rawstrlen,
894 'rawselectors' => $this->rawselectors,
895 'rawrules' => $this->rawrules,
896 'optimisedstrlen' => $this->optimisedstrlen,
897 'optimisedrules' => $this->optimisedrules,
898 'optimisedselectors' => $this->optimisedselectors,
899 'improvementstrlen' => '-',
900 'improvementrules' => '-',
901 'improvementselectors' => '-',
903 // Avoid division by 0 errors by checking we have valid raw values.
904 if ($this->rawstrlen > 0) {
905 $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%';
907 if ($this->rawrules > 0) {
908 $stats['improvementrules'] = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%';
910 if ($this->rawselectors > 0) {
911 $stats['improvementselectors'] = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%';
913 return $stats;
917 * Returns true if any errors have occured during processing
919 * @return bool
921 public function has_errors() {
922 return !empty($this->errors);
926 * Returns an array of errors that have occured
928 * @param bool $clear If set to true the errors will be cleared after being returned.
929 * @return array
931 public function get_errors($clear = false) {
932 $errors = $this->errors;
933 if ($clear) {
934 // Reset the error array.
935 $this->errors = array();
937 return $errors;
941 * Returns any errors as a string that can be included in CSS.
943 * @return string
945 public function output_errors_css() {
946 $computedcss = "/****************************************\n";
947 $computedcss .= " *--- Errors found during processing ----\n";
948 foreach ($this->errors as $error) {
949 $computedcss .= preg_replace('#^#m', '* ', $error);
951 $computedcss .= " ****************************************/\n\n";
952 return $computedcss;
956 * Returns a string to display stats about the last generation within CSS output
958 * @return string
960 public function output_stats_css() {
962 $computedcss = "/****************************************\n";
963 $computedcss .= " *------- CSS Optimisation stats --------\n";
965 if ($this->rawstrlen === 0) {
966 $computedcss .= " File not processed as it has no content /\n\n";
967 $computedcss .= " ****************************************/\n\n";
968 return $computedcss;
969 } else if ($this->rawrules === 0) {
970 $computedcss .= " File contained no rules to be processed /\n\n";
971 $computedcss .= " ****************************************/\n\n";
972 return $computedcss;
975 $stats = $this->get_stats();
977 $computedcss .= " * ".date('r')."\n";
978 $computedcss .= " * {$stats['commentsincss']} \t comments removed\n";
979 $computedcss .= " * Optimisation took {$stats['timetaken']} seconds\n";
980 $computedcss .= " *--------------- before ----------------\n";
981 $computedcss .= " * {$stats['rawstrlen']} \t chars read in\n";
982 $computedcss .= " * {$stats['rawrules']} \t rules read in\n";
983 $computedcss .= " * {$stats['rawselectors']} \t total selectors\n";
984 $computedcss .= " *---------------- after ----------------\n";
985 $computedcss .= " * {$stats['optimisedstrlen']} \t chars once optimized\n";
986 $computedcss .= " * {$stats['optimisedrules']} \t optimized rules\n";
987 $computedcss .= " * {$stats['optimisedselectors']} \t total selectors once optimized\n";
988 $computedcss .= " *---------------- stats ----------------\n";
989 $computedcss .= " * {$stats['improvementstrlen']} \t reduction in chars\n";
990 $computedcss .= " * {$stats['improvementrules']} \t reduction in rules\n";
991 $computedcss .= " * {$stats['improvementselectors']} \t reduction in selectors\n";
992 $computedcss .= " ****************************************/\n\n";
994 return $computedcss;
998 * Resets the stats ready for another fresh processing
1000 public function reset_stats() {
1001 $this->commentsincss = 0;
1002 $this->optimisedrules = 0;
1003 $this->optimisedselectors = 0;
1004 $this->optimisedstrlen = 0;
1005 $this->rawrules = 0;
1006 $this->rawselectors = 0;
1007 $this->rawstrlen = 0;
1008 $this->timecomplete = 0;
1009 $this->timestart = 0;
1013 * An array of the common HTML colours that are supported by most browsers.
1015 * This reference table is used to allow us to unify colours, and will aid
1016 * us in identifying buggy CSS using unsupported colours.
1018 * @var string[]
1020 public static $htmlcolours = array(
1021 'aliceblue' => '#F0F8FF',
1022 'antiquewhite' => '#FAEBD7',
1023 'aqua' => '#00FFFF',
1024 'aquamarine' => '#7FFFD4',
1025 'azure' => '#F0FFFF',
1026 'beige' => '#F5F5DC',
1027 'bisque' => '#FFE4C4',
1028 'black' => '#000000',
1029 'blanchedalmond' => '#FFEBCD',
1030 'blue' => '#0000FF',
1031 'blueviolet' => '#8A2BE2',
1032 'brown' => '#A52A2A',
1033 'burlywood' => '#DEB887',
1034 'cadetblue' => '#5F9EA0',
1035 'chartreuse' => '#7FFF00',
1036 'chocolate' => '#D2691E',
1037 'coral' => '#FF7F50',
1038 'cornflowerblue' => '#6495ED',
1039 'cornsilk' => '#FFF8DC',
1040 'crimson' => '#DC143C',
1041 'cyan' => '#00FFFF',
1042 'darkblue' => '#00008B',
1043 'darkcyan' => '#008B8B',
1044 'darkgoldenrod' => '#B8860B',
1045 'darkgray' => '#A9A9A9',
1046 'darkgrey' => '#A9A9A9',
1047 'darkgreen' => '#006400',
1048 'darkKhaki' => '#BDB76B',
1049 'darkmagenta' => '#8B008B',
1050 'darkolivegreen' => '#556B2F',
1051 'arkorange' => '#FF8C00',
1052 'darkorchid' => '#9932CC',
1053 'darkred' => '#8B0000',
1054 'darksalmon' => '#E9967A',
1055 'darkseagreen' => '#8FBC8F',
1056 'darkslateblue' => '#483D8B',
1057 'darkslategray' => '#2F4F4F',
1058 'darkslategrey' => '#2F4F4F',
1059 'darkturquoise' => '#00CED1',
1060 'darkviolet' => '#9400D3',
1061 'deeppink' => '#FF1493',
1062 'deepskyblue' => '#00BFFF',
1063 'dimgray' => '#696969',
1064 'dimgrey' => '#696969',
1065 'dodgerblue' => '#1E90FF',
1066 'firebrick' => '#B22222',
1067 'floralwhite' => '#FFFAF0',
1068 'forestgreen' => '#228B22',
1069 'fuchsia' => '#FF00FF',
1070 'gainsboro' => '#DCDCDC',
1071 'ghostwhite' => '#F8F8FF',
1072 'gold' => '#FFD700',
1073 'goldenrod' => '#DAA520',
1074 'gray' => '#808080',
1075 'grey' => '#808080',
1076 'green' => '#008000',
1077 'greenyellow' => '#ADFF2F',
1078 'honeydew' => '#F0FFF0',
1079 'hotpink' => '#FF69B4',
1080 'indianred ' => '#CD5C5C',
1081 'indigo ' => '#4B0082',
1082 'ivory' => '#FFFFF0',
1083 'khaki' => '#F0E68C',
1084 'lavender' => '#E6E6FA',
1085 'lavenderblush' => '#FFF0F5',
1086 'lawngreen' => '#7CFC00',
1087 'lemonchiffon' => '#FFFACD',
1088 'lightblue' => '#ADD8E6',
1089 'lightcoral' => '#F08080',
1090 'lightcyan' => '#E0FFFF',
1091 'lightgoldenrodyellow' => '#FAFAD2',
1092 'lightgray' => '#D3D3D3',
1093 'lightgrey' => '#D3D3D3',
1094 'lightgreen' => '#90EE90',
1095 'lightpink' => '#FFB6C1',
1096 'lightsalmon' => '#FFA07A',
1097 'lightseagreen' => '#20B2AA',
1098 'lightskyblue' => '#87CEFA',
1099 'lightslategray' => '#778899',
1100 'lightslategrey' => '#778899',
1101 'lightsteelblue' => '#B0C4DE',
1102 'lightyellow' => '#FFFFE0',
1103 'lime' => '#00FF00',
1104 'limegreen' => '#32CD32',
1105 'linen' => '#FAF0E6',
1106 'magenta' => '#FF00FF',
1107 'maroon' => '#800000',
1108 'mediumaquamarine' => '#66CDAA',
1109 'mediumblue' => '#0000CD',
1110 'mediumorchid' => '#BA55D3',
1111 'mediumpurple' => '#9370D8',
1112 'mediumseagreen' => '#3CB371',
1113 'mediumslateblue' => '#7B68EE',
1114 'mediumspringgreen' => '#00FA9A',
1115 'mediumturquoise' => '#48D1CC',
1116 'mediumvioletred' => '#C71585',
1117 'midnightblue' => '#191970',
1118 'mintcream' => '#F5FFFA',
1119 'mistyrose' => '#FFE4E1',
1120 'moccasin' => '#FFE4B5',
1121 'navajowhite' => '#FFDEAD',
1122 'navy' => '#000080',
1123 'oldlace' => '#FDF5E6',
1124 'olive' => '#808000',
1125 'olivedrab' => '#6B8E23',
1126 'orange' => '#FFA500',
1127 'orangered' => '#FF4500',
1128 'orchid' => '#DA70D6',
1129 'palegoldenrod' => '#EEE8AA',
1130 'palegreen' => '#98FB98',
1131 'paleturquoise' => '#AFEEEE',
1132 'palevioletred' => '#D87093',
1133 'papayawhip' => '#FFEFD5',
1134 'peachpuff' => '#FFDAB9',
1135 'peru' => '#CD853F',
1136 'pink' => '#FFC0CB',
1137 'plum' => '#DDA0DD',
1138 'powderblue' => '#B0E0E6',
1139 'purple' => '#800080',
1140 'red' => '#FF0000',
1141 'rosybrown' => '#BC8F8F',
1142 'royalblue' => '#4169E1',
1143 'saddlebrown' => '#8B4513',
1144 'salmon' => '#FA8072',
1145 'sandybrown' => '#F4A460',
1146 'seagreen' => '#2E8B57',
1147 'seashell' => '#FFF5EE',
1148 'sienna' => '#A0522D',
1149 'silver' => '#C0C0C0',
1150 'skyblue' => '#87CEEB',
1151 'slateblue' => '#6A5ACD',
1152 'slategray' => '#708090',
1153 'slategrey' => '#708090',
1154 'snow' => '#FFFAFA',
1155 'springgreen' => '#00FF7F',
1156 'steelblue' => '#4682B4',
1157 'tan' => '#D2B48C',
1158 'teal' => '#008080',
1159 'thistle' => '#D8BFD8',
1160 'tomato' => '#FF6347',
1161 'transparent' => 'transparent',
1162 'turquoise' => '#40E0D0',
1163 'violet' => '#EE82EE',
1164 'wheat' => '#F5DEB3',
1165 'white' => '#FFFFFF',
1166 'whitesmoke' => '#F5F5F5',
1167 'yellow' => '#FFFF00',
1168 'yellowgreen' => '#9ACD32'
1173 * Used to prepare CSS strings
1175 * @package core
1176 * @subpackage cssoptimiser
1177 * @copyright 2012 Sam Hemelryk
1178 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1180 abstract class css_writer {
1183 * The current indent level
1184 * @var int
1186 protected static $indent = 0;
1189 * Returns true if the output should still maintain minimum formatting.
1190 * @return bool
1192 protected static function is_pretty() {
1193 global $CFG;
1194 return (!empty($CFG->cssoptimiserpretty));
1198 * Returns the indenting char to use for indenting things nicely.
1199 * @return string
1201 protected static function get_indent() {
1202 if (self::is_pretty()) {
1203 return str_repeat(" ", self::$indent);
1205 return '';
1209 * Increases the current indent
1211 protected static function increase_indent() {
1212 self::$indent++;
1216 * Decreases the current indent
1218 protected static function decrease_indent() {
1219 self::$indent--;
1223 * Returns the string to use as a separator
1224 * @return string
1226 protected static function get_separator() {
1227 return (self::is_pretty())?"\n":' ';
1231 * Returns CSS for media
1233 * @param string $typestring
1234 * @param css_rule[] $rules An array of css_rule objects
1235 * @return string
1237 public static function media($typestring, array &$rules) {
1238 $nl = self::get_separator();
1240 $output = '';
1241 if ($typestring !== 'all') {
1242 $output .= "\n@media {$typestring} {".$nl;
1243 self::increase_indent();
1245 foreach ($rules as $rule) {
1246 $output .= $rule->out().$nl;
1248 if ($typestring !== 'all') {
1249 self::decrease_indent();
1250 $output .= '}';
1252 return $output;
1256 * Returns CSS for a keyframe
1258 * @param string $for The desired declaration. e.g. keyframes, -moz-keyframes, -webkit-keyframes
1259 * @param string $name The name for the keyframe
1260 * @param css_rule[] $rules An array of rules belonging to the keyframe
1261 * @return string
1263 public static function keyframe($for, $name, array &$rules) {
1264 $output = "\n@{$for} {$name} {";
1265 foreach ($rules as $rule) {
1266 $output .= $rule->out();
1268 $output .= '}';
1269 return $output;
1273 * Returns CSS for a rule
1275 * @param string $selector
1276 * @param string $styles
1277 * @return string
1279 public static function rule($selector, $styles) {
1280 $css = self::get_indent()."{$selector}{{$styles}}";
1281 return $css;
1285 * Returns CSS for the selectors of a rule
1287 * @param css_selector[] $selectors Array of css_selector objects
1288 * @return string
1290 public static function selectors(array $selectors) {
1291 $nl = self::get_separator();
1292 $selectorstrings = array();
1293 foreach ($selectors as $selector) {
1294 $selectorstrings[] = $selector->out();
1296 return join(','.$nl, $selectorstrings);
1300 * Returns a selector given the components that make it up.
1302 * @param array $components
1303 * @return string
1305 public static function selector(array $components) {
1306 return trim(join(' ', $components));
1310 * Returns a CSS string for the provided styles
1312 * @param css_style[] $styles Array of css_style objects
1313 * @return string
1315 public static function styles(array $styles) {
1316 $bits = array();
1317 foreach ($styles as $style) {
1318 // Check if the style is an array. If it is then we are outputing an advanced style.
1319 // An advanced style is a style with one or more values, and can occur in situations like background-image
1320 // where browse specific values are being used.
1321 if (is_array($style)) {
1322 /* @var css_style[] $style */
1323 foreach ($style as $advstyle) {
1324 $bits[] = $advstyle->out();
1326 continue;
1328 $bits[] = $style->out();
1330 return join('', $bits);
1334 * Returns a style CSS
1336 * @param string $name
1337 * @param string $value
1338 * @param bool $important
1339 * @return string
1341 public static function style($name, $value, $important = false) {
1342 $value = trim($value);
1343 if ($important && strpos($value, '!important') === false) {
1344 $value .= ' !important';
1346 return "{$name}:{$value};";
1351 * A consolidatable style interface.
1353 * Class that implement this have a short-hand notation for specifying multiple styles.
1355 * @package core
1356 * @subpackage cssoptimiser
1357 * @copyright 2012 Sam Hemelryk
1358 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1360 interface core_css_consolidatable_style {
1362 * Used to consolidate several styles into a single "short-hand" style.
1363 * @param array $styles
1364 * @return mixed
1366 public static function consolidate(array $styles);
1370 * A structure to represent a CSS selector.
1372 * The selector is the classes, id, elements, and psuedo bits that make up a CSS
1373 * rule.
1375 * @package core
1376 * @subpackage cssoptimiser
1377 * @copyright 2012 Sam Hemelryk
1378 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1380 class css_selector {
1383 * An array of selector bits
1384 * @var array
1386 protected $selectors = array();
1389 * The number of selectors.
1390 * @var int
1392 protected $count = 0;
1395 * Is null if there are no selectors, true if all selectors are basic and false otherwise.
1396 * A basic selector is one that consists of just the element type. e.g. div, span, td, a
1397 * @var bool|null
1399 protected $isbasic = null;
1402 * Initialises a new CSS selector
1403 * @return css_selector
1405 public static function init() {
1406 return new css_selector();
1410 * CSS selectors can only be created through the init method above.
1412 protected function __construct() {
1413 // Nothing to do here by default.
1417 * Adds a selector to the end of the current selector
1418 * @param string $selector
1420 public function add($selector) {
1421 $selector = trim($selector);
1422 $count = 0;
1423 $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
1424 if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
1425 $count ++;
1427 // If its already false then no need to continue, its not basic.
1428 if ($this->isbasic !== false) {
1429 // If theres more than one part making up this selector its not basic.
1430 if ($count > 1) {
1431 $this->isbasic = false;
1432 } else {
1433 // Check whether it is a basic element (a-z+) with possible psuedo selector.
1434 $this->isbasic = (bool)preg_match('#^[a-z]+(:[a-zA-Z]+)?$#', $selector);
1437 $this->count = $count;
1438 $this->selectors[] = $selector;
1441 * Returns the number of individual components that make up this selector
1442 * @return int
1444 public function get_selector_count() {
1445 return $this->count;
1449 * Returns the selector for use in a CSS rule
1450 * @return string
1452 public function out() {
1453 return css_writer::selector($this->selectors);
1457 * Returns true is all of the selectors act only upon basic elements (no classes/ids)
1458 * @return bool
1460 public function is_basic() {
1461 return ($this->isbasic === true);
1466 * A structure to represent a CSS rule.
1468 * @package core
1469 * @subpackage cssoptimiser
1470 * @copyright 2012 Sam Hemelryk
1471 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1473 class css_rule {
1476 * An array of CSS selectors {@link css_selector}
1477 * @var css_selector[]
1479 protected $selectors = array();
1482 * An array of CSS styles {@link css_style}
1483 * @var css_style[]
1485 protected $styles = array();
1488 * Created a new CSS rule. This is the only way to create a new CSS rule externally.
1489 * @return css_rule
1491 public static function init() {
1492 return new css_rule();
1496 * Constructs a new css rule.
1498 * @param string $selector The selector or array of selectors that make up this rule.
1499 * @param css_style[] $styles An array of styles that belong to this rule.
1501 protected function __construct($selector = null, array $styles = array()) {
1502 if ($selector != null) {
1503 if (is_array($selector)) {
1504 $this->selectors = $selector;
1505 } else {
1506 $this->selectors = array($selector);
1508 $this->add_styles($styles);
1513 * Adds a new CSS selector to this rule
1515 * e.g. $rule->add_selector('.one #two.two');
1517 * @param css_selector $selector Adds a CSS selector to this rule.
1519 public function add_selector(css_selector $selector) {
1520 $this->selectors[] = $selector;
1524 * Adds a new CSS style to this rule.
1526 * @param css_style|string $style Adds a new style to this rule
1528 public function add_style($style) {
1529 if (is_string($style)) {
1530 $style = trim($style);
1531 if (empty($style)) {
1532 return;
1534 $bits = explode(':', $style, 2);
1535 if (count($bits) == 2) {
1536 list($name, $value) = array_map('trim', $bits);
1538 if (isset($name) && isset($value) && $name !== '' && $value !== '') {
1539 $style = css_style::init_automatic($name, $value);
1541 } else if ($style instanceof css_style) {
1542 // Clone the style as it may be coming from another rule and we don't
1543 // want references as it will likely be overwritten by proceeding
1544 // rules.
1545 $style = clone($style);
1547 if ($style instanceof css_style) {
1548 $name = $style->get_name();
1549 $exists = array_key_exists($name, $this->styles);
1550 // We need to find out if the current style support multiple values, or whether the style
1551 // is already set up to record multiple values. This can happen with background images which can have single
1552 // and multiple values.
1553 if ($style->allows_multiple_values() || ($exists && is_array($this->styles[$name]))) {
1554 if (!$exists) {
1555 $this->styles[$name] = array();
1556 } else if ($this->styles[$name] instanceof css_style) {
1557 $this->styles[$name] = array($this->styles[$name]);
1559 $this->styles[$name][] = $style;
1560 } else if ($exists) {
1561 $this->styles[$name]->set_value($style->get_value());
1562 } else {
1563 $this->styles[$name] = $style;
1565 } else if (is_array($style)) {
1566 // We probably shouldn't worry about processing styles here but to
1567 // be truthful it doesn't hurt.
1568 foreach ($style as $astyle) {
1569 $this->add_style($astyle);
1575 * An easy method of adding several styles at once. Just calls add_style.
1577 * This method simply iterates over the array and calls {@link css_rule::add_style()}
1578 * with each.
1580 * @param css_style[] $styles Adds an array of styles
1582 public function add_styles(array $styles) {
1583 foreach ($styles as $style) {
1584 $this->add_style($style);
1589 * Returns the array of selectors
1591 * @return css_selector[]
1593 public function get_selectors() {
1594 return $this->selectors;
1598 * Returns the array of styles
1600 * @return css_style[]
1602 public function get_styles() {
1603 return $this->styles;
1607 * Outputs this rule as a fragment of CSS
1609 * @return string
1611 public function out() {
1612 $selectors = css_writer::selectors($this->selectors);
1613 $styles = css_writer::styles($this->get_consolidated_styles());
1614 return css_writer::rule($selectors, $styles);
1618 * Consolidates all styles associated with this rule
1620 * @return css_style[] An array of consolidated styles
1622 public function get_consolidated_styles() {
1623 /* @var css_style[] $organisedstyles */
1624 $organisedstyles = array();
1625 /* @var css_style[] $finalstyles */
1626 $finalstyles = array();
1627 /* @var core_css_consolidatable_style[] $consolidate */
1628 $consolidate = array();
1629 /* @var css_style[] $advancedstyles */
1630 $advancedstyles = array();
1631 foreach ($this->styles as $style) {
1632 // If the style is an array then we are processing an advanced style. An advanced style is a style that can have
1633 // one or more values. Background-image is one such example as it can have browser specific styles.
1634 if (is_array($style)) {
1635 $single = null;
1636 $count = 0;
1637 foreach ($style as $advstyle) {
1638 /* @var css_style $advstyle */
1639 $key = $count++;
1640 $advancedstyles[$key] = $advstyle;
1641 if (!$advstyle->allows_multiple_values()) {
1642 if (!is_null($single)) {
1643 unset($advancedstyles[$single]);
1645 $single = $key;
1648 if (!is_null($single)) {
1649 $style = $advancedstyles[$single];
1651 $consolidatetoclass = $style->consolidate_to();
1652 if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1653 class_exists('css_style_'.$consolidatetoclass)) {
1654 $class = 'css_style_'.$consolidatetoclass;
1655 if (!array_key_exists($class, $consolidate)) {
1656 $consolidate[$class] = array();
1657 $organisedstyles[$class] = true;
1659 $consolidate[$class][] = $style;
1660 unset($advancedstyles[$single]);
1664 continue;
1666 $consolidatetoclass = $style->consolidate_to();
1667 if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1668 class_exists('css_style_'.$consolidatetoclass)) {
1669 $class = 'css_style_'.$consolidatetoclass;
1670 if (!array_key_exists($class, $consolidate)) {
1671 $consolidate[$class] = array();
1672 $organisedstyles[$class] = true;
1674 $consolidate[$class][] = $style;
1675 } else {
1676 $organisedstyles[$style->get_name()] = $style;
1680 foreach ($consolidate as $class => $styles) {
1681 $organisedstyles[$class] = call_user_func(array($class, 'consolidate'), $styles);
1684 foreach ($organisedstyles as $style) {
1685 if (is_array($style)) {
1686 foreach ($style as $s) {
1687 $finalstyles[] = $s;
1689 } else {
1690 $finalstyles[] = $style;
1693 $finalstyles = array_merge($finalstyles, $advancedstyles);
1694 return $finalstyles;
1698 * Splits this rules into an array of CSS rules. One for each of the selectors
1699 * that make up this rule.
1701 * @return css_rule[]
1703 public function split_by_selector() {
1704 $return = array();
1705 foreach ($this->selectors as $selector) {
1706 $return[] = new css_rule($selector, $this->styles);
1708 return $return;
1712 * Splits this rule into an array of rules. One for each of the styles that
1713 * make up this rule
1715 * @return css_rule[] Array of css_rule objects
1717 public function split_by_style() {
1718 $return = array();
1719 foreach ($this->styles as $style) {
1720 if (is_array($style)) {
1721 $return[] = new css_rule($this->selectors, $style);
1722 continue;
1724 $return[] = new css_rule($this->selectors, array($style));
1726 return $return;
1730 * Gets a hash for the styles of this rule
1732 * @return string
1734 public function get_style_hash() {
1735 return md5(css_writer::styles($this->styles));
1739 * Gets a hash for the selectors of this rule
1741 * @return string
1743 public function get_selector_hash() {
1744 return md5(css_writer::selectors($this->selectors));
1748 * Gets the number of selectors that make up this rule.
1750 * @return int
1752 public function get_selector_count() {
1753 $count = 0;
1754 foreach ($this->selectors as $selector) {
1755 $count += $selector->get_selector_count();
1757 return $count;
1761 * Returns true if there are any errors with this rule.
1763 * @return bool
1765 public function has_errors() {
1766 foreach ($this->styles as $style) {
1767 if (is_array($style)) {
1768 /* @var css_style[] $style */
1769 foreach ($style as $advstyle) {
1770 if ($advstyle->has_error()) {
1771 return true;
1774 continue;
1776 if ($style->has_error()) {
1777 return true;
1780 return false;
1784 * Returns the error strings that were recorded when processing this rule.
1786 * Before calling this function you should first call {@link css_rule::has_errors()}
1787 * to make sure there are errors (hopefully there arn't).
1789 * @return string
1791 public function get_error_string() {
1792 $css = $this->out();
1793 $errors = array();
1794 foreach ($this->styles as $style) {
1795 if (is_array($style)) {
1796 /* @var css_style[] $style */
1797 foreach ($style as $advstyle) {
1798 if ($advstyle instanceof css_style && $advstyle->has_error()) {
1799 $errors[] = " * ".$advstyle->get_last_error();
1802 } else if ($style instanceof css_style && $style->has_error()) {
1803 $errors[] = " * ".$style->get_last_error();
1806 return $css." has the following errors:\n".join("\n", $errors);
1810 * Returns true if this rule could be considered a reset rule.
1812 * A reset rule is a rule that acts upon an HTML element and does not include any other parts to its selector.
1814 * @return bool
1816 public function is_reset_rule() {
1817 foreach ($this->selectors as $selector) {
1818 if (!$selector->is_basic()) {
1819 return false;
1822 return true;
1827 * An abstract CSS rule collection class.
1829 * This class is extended by things such as media and keyframe declaration. They are declarations that
1830 * group rules together for a purpose.
1831 * When no declaration is specified rules accumulate into @media all.
1833 * @package core
1834 * @subpackage cssoptimiser
1835 * @copyright 2012 Sam Hemelryk
1836 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1838 abstract class css_rule_collection {
1840 * An array of rules within this collection instance
1841 * @var css_rule[]
1843 protected $rules = array();
1846 * The collection must be able to print itself.
1848 abstract public function out();
1851 * Adds a new CSS rule to this collection instance
1853 * @param css_rule $newrule
1855 public function add_rule(css_rule $newrule) {
1856 foreach ($newrule->split_by_selector() as $rule) {
1857 $hash = $rule->get_selector_hash();
1858 if (!array_key_exists($hash, $this->rules)) {
1859 $this->rules[$hash] = $rule;
1860 } else {
1861 $this->rules[$hash]->add_styles($rule->get_styles());
1867 * Returns the rules used by this collection
1869 * @return css_rule[]
1871 public function get_rules() {
1872 return $this->rules;
1876 * Organises rules by gropuing selectors based upon the styles and consolidating
1877 * those selectors into single rules.
1879 * @return bool True if the CSS was optimised by this method
1881 public function organise_rules_by_selectors() {
1882 /* @var css_rule[] $optimisedrules */
1883 $optimisedrules = array();
1884 $beforecount = count($this->rules);
1885 $lasthash = null;
1886 /* @var css_rule $lastrule */
1887 $lastrule = null;
1888 foreach ($this->rules as $rule) {
1889 $hash = $rule->get_style_hash();
1890 if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) {
1891 foreach ($rule->get_selectors() as $selector) {
1892 $lastrule->add_selector($selector);
1894 continue;
1896 $lastrule = clone($rule);
1897 $lasthash = $hash;
1898 $optimisedrules[] = $lastrule;
1900 $this->rules = array();
1901 foreach ($optimisedrules as $optimised) {
1902 $this->rules[$optimised->get_selector_hash()] = $optimised;
1904 $aftercount = count($this->rules);
1905 return ($beforecount < $aftercount);
1909 * Returns the total number of rules that exist within this collection
1911 * @return int
1913 public function count_rules() {
1914 return count($this->rules);
1918 * Returns the total number of selectors that exist within this collection
1920 * @return int
1922 public function count_selectors() {
1923 $count = 0;
1924 foreach ($this->rules as $rule) {
1925 $count += $rule->get_selector_count();
1927 return $count;
1931 * Returns true if the collection has any rules that have errors
1933 * @return boolean
1935 public function has_errors() {
1936 foreach ($this->rules as $rule) {
1937 if ($rule->has_errors()) {
1938 return true;
1941 return false;
1945 * Returns any errors that have happened within rules in this collection.
1947 * @return string[]
1949 public function get_errors() {
1950 $errors = array();
1951 foreach ($this->rules as $rule) {
1952 if ($rule->has_errors()) {
1953 $errors[] = $rule->get_error_string();
1956 return $errors;
1961 * A media class to organise rules by the media they apply to.
1963 * @package core
1964 * @subpackage cssoptimiser
1965 * @copyright 2012 Sam Hemelryk
1966 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1968 class css_media extends css_rule_collection {
1971 * An array of the different media types this instance applies to.
1972 * @var array
1974 protected $types = array();
1977 * Initalises a new media instance
1979 * @param string $for The media that the contained rules are destined for.
1981 public function __construct($for = 'all') {
1982 $types = explode(',', $for);
1983 $this->types = array_map('trim', $types);
1987 * Returns the CSS for this media and all of its rules.
1989 * @return string
1991 public function out() {
1992 return css_writer::media(join(',', $this->types), $this->rules);
1996 * Returns an array of media that this media instance applies to
1998 * @return array
2000 public function get_types() {
2001 return $this->types;
2005 * Returns all of the reset rules known by this media set.
2006 * @param bool $remove If set to true reset rules will be removed before being returned.
2007 * @return array
2009 public function get_reset_rules($remove = false) {
2010 $resetrules = array();
2011 foreach ($this->rules as $key => $rule) {
2012 if ($rule->is_reset_rule()) {
2013 $resetrules[] = clone $rule;
2014 if ($remove) {
2015 unset($this->rules[$key]);
2019 return $resetrules;
2024 * A media class to organise rules by the media they apply to.
2026 * @package core
2027 * @subpackage cssoptimiser
2028 * @copyright 2012 Sam Hemelryk
2029 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2031 class css_keyframe extends css_rule_collection {
2034 * The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2035 * @var string
2037 protected $for;
2040 * The name for the keyframes
2041 * @var string
2043 protected $name;
2045 * Constructs a new keyframe
2047 * @param string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2048 * @param string $name The name for the keyframes
2050 public function __construct($for, $name) {
2051 $this->for = $for;
2052 $this->name = $name;
2055 * Returns the directive of this keyframe
2057 * e.g. keyframes, -moz-keyframes, -webkit-keyframes
2058 * @return string
2060 public function get_for() {
2061 return $this->for;
2064 * Returns the name of this keyframe
2065 * @return string
2067 public function get_name() {
2068 return $this->name;
2071 * Returns the CSS for this collection of keyframes and all of its rules.
2073 * @return string
2075 public function out() {
2076 return css_writer::keyframe($this->for, $this->name, $this->rules);
2081 * An absract class to represent CSS styles
2083 * @package core
2084 * @subpackage cssoptimiser
2085 * @copyright 2012 Sam Hemelryk
2086 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2088 abstract class css_style {
2090 /** Constant used for recongise a special empty value in a CSS style */
2091 const NULL_VALUE = '@@$NULL$@@';
2094 * The name of the style
2095 * @var string
2097 protected $name;
2100 * The value for the style
2101 * @var mixed
2103 protected $value;
2106 * If set to true this style was defined with the !important rule.
2107 * Only trolls use !important.
2108 * Don't hide under bridges.. its not good for your skin. Do the proper thing
2109 * and fix the issue don't just force a fix that will undoubtedly one day
2110 * lead to further frustration.
2111 * @var bool
2113 protected $important = false;
2116 * Gets set to true if this style has an error
2117 * @var bool
2119 protected $error = false;
2122 * The last error message that occured
2123 * @var string
2125 protected $errormessage = null;
2128 * Initialises a new style.
2130 * This is the only public way to create a style to ensure they that appropriate
2131 * style class is used if it exists.
2133 * @param string $name The name of the style.
2134 * @param string $value The value of the style.
2135 * @return css_style_generic
2137 public static function init_automatic($name, $value) {
2138 $cleanedname = preg_replace('#[^a-zA-Z0-9]+#', '', $name);
2139 $specificclass = 'css_style_'.$cleanedname;
2140 if (class_exists($specificclass)) {
2141 $style = call_user_func(array($specificclass, 'init'), $value);
2142 if ($cleanedname !== $name && !is_array($style)) {
2143 $style->set_actual_name($name);
2145 return $style;
2147 return new css_style_generic($name, $value);
2151 * Creates a new style when given its name and value
2153 * @param string $name The name of the style.
2154 * @param string $value The value of the style.
2156 protected function __construct($name, $value) {
2157 $this->name = $name;
2158 $this->set_value($value);
2162 * Sets the value for the style
2164 * @param string $value
2166 final public function set_value($value) {
2167 $value = trim($value);
2168 $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
2169 if ($important) {
2170 $value = substr($value, 0, -(strlen($matches[1])));
2171 $value = rtrim($value);
2173 if (!$this->important || $important) {
2174 $this->value = $this->clean_value($value);
2175 $this->important = $important;
2177 if (!$this->is_valid()) {
2178 $this->set_error('Invalid value for '.$this->name);
2183 * Returns true if the value associated with this style is valid
2185 * @return bool
2187 public function is_valid() {
2188 return true;
2192 * Returns the name for the style
2194 * @return string
2196 public function get_name() {
2197 return $this->name;
2201 * Returns the value for the style
2203 * @param bool $includeimportant If set to true and the rule is important !important postfix will be used.
2204 * @return string
2206 public function get_value($includeimportant = true) {
2207 $value = $this->value;
2208 if ($includeimportant && $this->important) {
2209 $value .= ' !important';
2211 return $value;
2215 * Returns the style ready for use in CSS
2217 * @param string|null $value A value to use to override the value for this style.
2218 * @return string
2220 public function out($value = null) {
2221 if (is_null($value)) {
2222 $value = $this->get_value();
2224 return css_writer::style($this->name, $value, $this->important);
2228 * This can be overridden by a specific style allowing it to clean its values
2229 * consistently.
2231 * @param mixed $value
2232 * @return mixed
2234 protected function clean_value($value) {
2235 return $value;
2239 * If this particular style can be consolidated into another style this function
2240 * should return the style that it can be consolidated into.
2242 * @return string|null
2244 public function consolidate_to() {
2245 return null;
2249 * Sets the last error message.
2251 * @param string $message
2253 protected function set_error($message) {
2254 $this->error = true;
2255 $this->errormessage = $message;
2259 * Returns true if an error has occured
2261 * @return bool
2263 public function has_error() {
2264 return $this->error;
2268 * Returns the last error that occured or null if no errors have happened.
2270 * @return string
2272 public function get_last_error() {
2273 return $this->errormessage;
2277 * Returns true if the value for this style is the special null value.
2279 * This should only be overriden in circumstances where a shorthand style can lead
2280 * to move explicit styles being overwritten. Not a common place occurenace.
2282 * Example:
2283 * This occurs if the shorthand background property was used but no proper value
2284 * was specified for this style.
2285 * This leads to a null value being used unless otherwise overridden.
2287 * @return bool
2289 public function is_special_empty_value() {
2290 return false;
2294 * Returns true if this style permits multiple values.
2296 * This occurs for styles such as background image that can have browser specific values that need to be maintained because
2297 * of course we don't know what browser the user is using, and optimisation occurs before caching.
2298 * Thus we must always server all values we encounter in the order we encounter them for when this is set to true.
2300 * @return boolean False by default, true if the style supports muliple values.
2302 public function allows_multiple_values() {
2303 return false;
2307 * Returns true if this style was marked important.
2308 * @return bool
2310 public function is_important() {
2311 return !empty($this->important);
2315 * Sets the important flag for this style and its current value.
2316 * @param bool $important
2318 public function set_important($important = true) {
2319 $this->important = (bool) $important;
2323 * Sets the actual name used within the style.
2325 * This method allows us to support browser hacks like *width:0;
2327 * @param string $name
2329 public function set_actual_name($name) {
2330 $this->name = $name;
2335 * A generic CSS style class to use when a more specific class does not exist.
2337 * @package core
2338 * @subpackage cssoptimiser
2339 * @copyright 2012 Sam Hemelryk
2340 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2342 class css_style_generic extends css_style {
2345 * Cleans incoming values for typical things that can be optimised.
2347 * @param mixed $value Cleans the provided value optimising it if possible
2348 * @return string
2350 protected function clean_value($value) {
2351 if (trim($value) == '0px') {
2352 $value = 0;
2353 } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
2354 $value = '#'.strtoupper($matches[1]);
2356 return $value;
2361 * A colour CSS style
2363 * @package core
2364 * @subpackage cssoptimiser
2365 * @copyright 2012 Sam Hemelryk
2366 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2368 class css_style_color extends css_style {
2371 * Creates a new colour style
2373 * @param mixed $value Initialises a new colour style
2374 * @return css_style_color
2376 public static function init($value) {
2377 return new css_style_color('color', $value);
2381 * Cleans the colour unifing it to a 6 char hash colour if possible
2382 * Doing this allows us to associate identical colours being specified in
2383 * different ways. e.g. Red, red, #F00, and #F00000
2385 * @param mixed $value Cleans the provided value optimising it if possible
2386 * @return string
2388 protected function clean_value($value) {
2389 $value = trim($value);
2390 if (css_is_colour($value)) {
2391 if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
2392 $value = '#'.strtoupper($matches[1]);
2393 } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
2394 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
2395 $value = '#'.strtoupper($value);
2396 } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
2397 $value = css_optimiser::$htmlcolours[strtolower($value)];
2400 return $value;
2404 * Returns the colour style for use within CSS.
2405 * Will return an optimised hash colour.
2407 * e.g #123456
2408 * #123 instead of #112233
2409 * #F00 instead of red
2411 * @param string $overridevalue If provided then this value will be used instead
2412 * of the styles current value.
2413 * @return string
2415 public function out($overridevalue = null) {
2416 if ($overridevalue === null) {
2417 $overridevalue = $this->value;
2419 return parent::out(self::shrink_value($overridevalue));
2423 * Shrinks the colour value is possible.
2425 * @param string $value Shrinks the current value to an optimial form if possible
2426 * @return string
2428 public static function shrink_value($value) {
2429 if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) {
2430 return '#'.$matches[1].$matches[2].$matches[3];
2432 return $value;
2436 * Returns true if the value is a valid colour.
2438 * @return bool
2440 public function is_valid() {
2441 return css_is_colour($this->value);
2446 * A width style
2448 * @package core
2449 * @subpackage cssoptimiser
2450 * @copyright 2012 Sam Hemelryk
2451 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2453 class css_style_width extends css_style {
2456 * Checks if the width is valid
2457 * @return bool
2459 public function is_valid() {
2460 return css_is_width($this->value);
2464 * Cleans the provided value
2466 * @param mixed $value Cleans the provided value optimising it if possible
2467 * @return string
2469 protected function clean_value($value) {
2470 if (!css_is_width($value)) {
2471 // Note we don't actually change the value to something valid. That
2472 // would be bad for futureproofing.
2473 $this->set_error('Invalid width specified for '.$this->name);
2474 } else if (preg_match('#^0\D+$#', $value)) {
2475 $value = 0;
2477 return trim($value);
2481 * Initialises a new width style
2483 * @param mixed $value The value this style has
2484 * @return css_style_width
2486 public static function init($value) {
2487 return new css_style_width('width', $value);
2492 * A margin style
2494 * @package core
2495 * @subpackage cssoptimiser
2496 * @copyright 2012 Sam Hemelryk
2497 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2499 class css_style_margin extends css_style_width implements core_css_consolidatable_style {
2502 * Initialises a margin style.
2504 * In this case we split the margin into several other margin styles so that
2505 * we can properly condense overrides and then reconsolidate them later into
2506 * an optimal form.
2508 * @param string $value The value the style has
2509 * @return array An array of margin values that can later be consolidated
2511 public static function init($value) {
2512 $important = '';
2513 if (strpos($value, '!important') !== false) {
2514 $important = ' !important';
2515 $value = str_replace('!important', '', $value);
2518 $value = preg_replace('#\s+#', ' ', trim($value));
2519 $bits = explode(' ', $value, 4);
2521 $top = $right = $bottom = $left = null;
2522 if (count($bits) > 0) {
2523 $top = $right = $bottom = $left = array_shift($bits);
2525 if (count($bits) > 0) {
2526 $right = $left = array_shift($bits);
2528 if (count($bits) > 0) {
2529 $bottom = array_shift($bits);
2531 if (count($bits) > 0) {
2532 $left = array_shift($bits);
2534 return array(
2535 new css_style_margintop('margin-top', $top.$important),
2536 new css_style_marginright('margin-right', $right.$important),
2537 new css_style_marginbottom('margin-bottom', $bottom.$important),
2538 new css_style_marginleft('margin-left', $left.$important)
2543 * Consolidates individual margin styles into a single margin style
2545 * @param css_style[] $styles
2546 * @return css_style[] An array of consolidated styles
2548 public static function consolidate(array $styles) {
2549 if (count($styles) != 4) {
2550 return $styles;
2553 $someimportant = false;
2554 $allimportant = null;
2555 $notimportantequal = null;
2556 $firstvalue = null;
2557 foreach ($styles as $style) {
2558 if ($style->is_important()) {
2559 $someimportant = true;
2560 if ($allimportant === null) {
2561 $allimportant = true;
2563 } else {
2564 if ($allimportant === true) {
2565 $allimportant = false;
2567 if ($firstvalue == null) {
2568 $firstvalue = $style->get_value(false);
2569 $notimportantequal = true;
2570 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
2571 $notimportantequal = false;
2576 if ($someimportant && !$allimportant && !$notimportantequal) {
2577 return $styles;
2580 if ($someimportant && !$allimportant && $notimportantequal) {
2581 $return = array(
2582 new css_style_margin('margin', $firstvalue)
2584 foreach ($styles as $style) {
2585 if ($style->is_important()) {
2586 $return[] = $style;
2589 return $return;
2590 } else {
2591 $top = null;
2592 $right = null;
2593 $bottom = null;
2594 $left = null;
2595 foreach ($styles as $style) {
2596 switch ($style->get_name()) {
2597 case 'margin-top' :
2598 $top = $style->get_value(false);
2599 break;
2600 case 'margin-right' :
2601 $right = $style->get_value(false);
2602 break;
2603 case 'margin-bottom' :
2604 $bottom = $style->get_value(false);
2605 break;
2606 case 'margin-left' :
2607 $left = $style->get_value(false);
2608 break;
2611 if ($top == $bottom && $left == $right) {
2612 if ($top == $left) {
2613 $returnstyle = new css_style_margin('margin', $top);
2614 } else {
2615 $returnstyle = new css_style_margin('margin', "{$top} {$left}");
2617 } else if ($left == $right) {
2618 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom}");
2619 } else {
2620 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}");
2622 if ($allimportant) {
2623 $returnstyle->set_important();
2625 return array($returnstyle);
2631 * A margin top style
2633 * @package core
2634 * @subpackage cssoptimiser
2635 * @copyright 2012 Sam Hemelryk
2636 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2638 class css_style_margintop extends css_style_margin {
2641 * A simple init, just a single style
2643 * @param string $value The value the style has
2644 * @return css_style_margintop
2646 public static function init($value) {
2647 return new css_style_margintop('margin-top', $value);
2651 * This style can be consolidated into a single margin style
2653 * @return string
2655 public function consolidate_to() {
2656 return 'margin';
2661 * A margin right style
2663 * @package core
2664 * @subpackage cssoptimiser
2665 * @copyright 2012 Sam Hemelryk
2666 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2668 class css_style_marginright extends css_style_margin {
2671 * A simple init, just a single style
2673 * @param string $value The value the style has
2674 * @return css_style_margintop
2676 public static function init($value) {
2677 return new css_style_marginright('margin-right', $value);
2681 * This style can be consolidated into a single margin style
2683 * @return string
2685 public function consolidate_to() {
2686 return 'margin';
2691 * A margin bottom style
2693 * @package core
2694 * @subpackage cssoptimiser
2695 * @copyright 2012 Sam Hemelryk
2696 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2698 class css_style_marginbottom extends css_style_margin {
2701 * A simple init, just a single style
2703 * @param string $value The value the style has
2704 * @return css_style_margintop
2706 public static function init($value) {
2707 return new css_style_marginbottom('margin-bottom', $value);
2711 * This style can be consolidated into a single margin style
2713 * @return string
2715 public function consolidate_to() {
2716 return 'margin';
2721 * A margin left style
2723 * @package core
2724 * @subpackage cssoptimiser
2725 * @copyright 2012 Sam Hemelryk
2726 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2728 class css_style_marginleft extends css_style_margin {
2731 * A simple init, just a single style
2733 * @param string $value The value the style has
2734 * @return css_style_margintop
2736 public static function init($value) {
2737 return new css_style_marginleft('margin-left', $value);
2741 * This style can be consolidated into a single margin style
2743 * @return string
2745 public function consolidate_to() {
2746 return 'margin';
2751 * A border style
2753 * @package core
2754 * @subpackage cssoptimiser
2755 * @copyright 2012 Sam Hemelryk
2756 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2758 class css_style_border extends css_style implements core_css_consolidatable_style {
2761 * Initalises the border style into an array of individual style compontents
2763 * @param string $value The value the style has
2764 * @return css_style_bordercolor
2766 public static function init($value) {
2767 $value = preg_replace('#\s+#', ' ', $value);
2768 $bits = explode(' ', $value, 3);
2770 $return = array();
2771 if (count($bits) > 0) {
2772 $width = array_shift($bits);
2773 if (!css_style_borderwidth::is_border_width($width)) {
2774 $width = '0';
2776 $return[] = css_style_bordertopwidth::init($width);
2777 $return[] = css_style_borderrightwidth::init($width);
2778 $return[] = css_style_borderbottomwidth::init($width);
2779 $return[] = css_style_borderleftwidth::init($width);
2781 if (count($bits) > 0) {
2782 $style = array_shift($bits);
2783 $return[] = css_style_bordertopstyle::init($style);
2784 $return[] = css_style_borderrightstyle::init($style);
2785 $return[] = css_style_borderbottomstyle::init($style);
2786 $return[] = css_style_borderleftstyle::init($style);
2788 if (count($bits) > 0) {
2789 $colour = array_shift($bits);
2790 $return[] = css_style_bordertopcolor::init($colour);
2791 $return[] = css_style_borderrightcolor::init($colour);
2792 $return[] = css_style_borderbottomcolor::init($colour);
2793 $return[] = css_style_borderleftcolor::init($colour);
2795 return $return;
2799 * Consolidates all border styles into a single style
2801 * @param css_style[] $styles An array of border styles
2802 * @return css_style[] An optimised array of border styles
2804 public static function consolidate(array $styles) {
2806 $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2807 $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2808 $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2810 foreach ($styles as $style) {
2811 switch ($style->get_name()) {
2812 case 'border-top-width':
2813 $borderwidths['top'] = $style->get_value();
2814 break;
2815 case 'border-right-width':
2816 $borderwidths['right'] = $style->get_value();
2817 break;
2818 case 'border-bottom-width':
2819 $borderwidths['bottom'] = $style->get_value();
2820 break;
2821 case 'border-left-width':
2822 $borderwidths['left'] = $style->get_value();
2823 break;
2825 case 'border-top-style':
2826 $borderstyles['top'] = $style->get_value();
2827 break;
2828 case 'border-right-style':
2829 $borderstyles['right'] = $style->get_value();
2830 break;
2831 case 'border-bottom-style':
2832 $borderstyles['bottom'] = $style->get_value();
2833 break;
2834 case 'border-left-style':
2835 $borderstyles['left'] = $style->get_value();
2836 break;
2838 case 'border-top-color':
2839 $bordercolors['top'] = css_style_color::shrink_value($style->get_value());
2840 break;
2841 case 'border-right-color':
2842 $bordercolors['right'] = css_style_color::shrink_value($style->get_value());
2843 break;
2844 case 'border-bottom-color':
2845 $bordercolors['bottom'] = css_style_color::shrink_value($style->get_value());
2846 break;
2847 case 'border-left-color':
2848 $bordercolors['left'] = css_style_color::shrink_value($style->get_value());
2849 break;
2853 $uniquewidths = count(array_unique($borderwidths));
2854 $uniquestyles = count(array_unique($borderstyles));
2855 $uniquecolors = count(array_unique($bordercolors));
2857 $nullwidths = in_array(null, $borderwidths, true);
2858 $nullstyles = in_array(null, $borderstyles, true);
2859 $nullcolors = in_array(null, $bordercolors, true);
2861 $allwidthsthesame = ($uniquewidths === 1)?1:0;
2862 $allstylesthesame = ($uniquestyles === 1)?1:0;
2863 $allcolorsthesame = ($uniquecolors === 1)?1:0;
2865 $allwidthsnull = $allwidthsthesame && $nullwidths;
2866 $allstylesnull = $allstylesthesame && $nullstyles;
2867 $allcolorsnull = $allcolorsthesame && $nullcolors;
2869 /* @var css_style[] $return */
2870 $return = array();
2871 if ($allwidthsnull && $allstylesnull && $allcolorsnull) {
2872 // Everything is null still... boo.
2873 return array(new css_style_border('border', ''));
2875 } else if ($allwidthsnull && $allstylesnull) {
2877 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2878 return $return;
2880 } else if ($allwidthsnull && $allcolorsnull) {
2882 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2883 return $return;
2885 } else if ($allcolorsnull && $allstylesnull) {
2887 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2888 return $return;
2892 if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 3) {
2894 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']);
2896 } else if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 2) {
2898 if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) {
2900 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']);
2901 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2903 } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) {
2905 $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']);
2906 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2908 } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) {
2910 $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']);
2911 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2913 } else {
2914 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2915 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2916 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2919 } else if (!$nullwidths && !$nullcolors && !$nullstyles &&
2920 max(array_count_values($borderwidths)) == 3 &&
2921 max(array_count_values($borderstyles)) == 3 &&
2922 max(array_count_values($bordercolors)) == 3) {
2924 $widthkeys = array();
2925 $stylekeys = array();
2926 $colorkeys = array();
2928 foreach ($borderwidths as $key => $value) {
2929 if (!array_key_exists($value, $widthkeys)) {
2930 $widthkeys[$value] = array();
2932 $widthkeys[$value][] = $key;
2934 usort($widthkeys, 'css_sort_by_count');
2935 $widthkeys = array_values($widthkeys);
2937 foreach ($borderstyles as $key => $value) {
2938 if (!array_key_exists($value, $stylekeys)) {
2939 $stylekeys[$value] = array();
2941 $stylekeys[$value][] = $key;
2943 usort($stylekeys, 'css_sort_by_count');
2944 $stylekeys = array_values($stylekeys);
2946 foreach ($bordercolors as $key => $value) {
2947 if (!array_key_exists($value, $colorkeys)) {
2948 $colorkeys[$value] = array();
2950 $colorkeys[$value][] = $key;
2952 usort($colorkeys, 'css_sort_by_count');
2953 $colorkeys = array_values($colorkeys);
2955 if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) {
2956 $key = $widthkeys[0][0];
2957 self::build_style_string($return, 'css_style_border', 'border',
2958 $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2959 $key = $widthkeys[1][0];
2960 self::build_style_string($return, 'css_style_border'.$key, 'border-'.$key,
2961 $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2962 } else {
2963 self::build_style_string($return, 'css_style_bordertop', 'border-top',
2964 $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2965 self::build_style_string($return, 'css_style_borderright', 'border-right',
2966 $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2967 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
2968 $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2969 self::build_style_string($return, 'css_style_borderleft', 'border-left',
2970 $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
2972 } else {
2973 self::build_style_string($return, 'css_style_bordertop', 'border-top',
2974 $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2975 self::build_style_string($return, 'css_style_borderright', 'border-right',
2976 $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2977 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
2978 $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2979 self::build_style_string($return, 'css_style_borderleft', 'border-left',
2980 $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
2982 foreach ($return as $key => $style) {
2983 if ($style->get_value() == '') {
2984 unset($return[$key]);
2987 return $return;
2991 * Border styles get consolidated to a single border style.
2993 * @return string
2995 public function consolidate_to() {
2996 return 'border';
3000 * Consolidates a series of border styles into an optimised array of border
3001 * styles by looking at the direction of the border and prioritising that
3002 * during the optimisation.
3004 * @param array $array An array to add styles into during consolidation. Passed by reference.
3005 * @param string $class The class type to initalise
3006 * @param string $style The style to create
3007 * @param string|array $top The top value
3008 * @param string $right The right value
3009 * @param string $bottom The bottom value
3010 * @param string $left The left value
3011 * @return bool
3013 public static function consolidate_styles_by_direction(&$array, $class, $style,
3014 $top, $right = null, $bottom = null, $left = null) {
3015 if (is_array($top)) {
3016 $right = $top['right'];
3017 $bottom = $top['bottom'];
3018 $left = $top['left'];
3019 $top = $top['top'];
3022 if ($top == $bottom && $left == $right && $top == $left) {
3023 if (is_null($top)) {
3024 $array[] = new $class($style, '');
3025 } else {
3026 $array[] = new $class($style, $top);
3028 } else if ($top == null || $right == null || $bottom == null || $left == null) {
3029 if ($top !== null) {
3030 $array[] = new $class(str_replace('border-', 'border-top-', $style), $top);
3032 if ($right !== null) {
3033 $array[] = new $class(str_replace('border-', 'border-right-', $style), $right);
3035 if ($bottom !== null) {
3036 $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom);
3038 if ($left !== null) {
3039 $array[] = new $class(str_replace('border-', 'border-left-', $style), $left);
3041 } else if ($top == $bottom && $left == $right) {
3042 $array[] = new $class($style, $top.' '.$right);
3043 } else if ($left == $right) {
3044 $array[] = new $class($style, $top.' '.$right.' '.$bottom);
3045 } else {
3046 $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left);
3048 return true;
3052 * Builds a border style for a set of width, style, and colour values
3054 * @param array $array An array into which the generated style is added
3055 * @param string $class The class type to initialise
3056 * @param string $cssstyle The style to use
3057 * @param string $width The width of the border
3058 * @param string $style The style of the border
3059 * @param string $color The colour of the border
3060 * @return bool
3062 public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) {
3063 if (!is_null($width) && !is_null($style) && !is_null($color)) {
3064 $array[] = new $class($cssstyle, $width.' '.$style.' '.$color);
3065 } else if (!is_null($width) && !is_null($style) && is_null($color)) {
3066 $array[] = new $class($cssstyle, $width.' '.$style);
3067 } else if (!is_null($width) && is_null($style) && is_null($color)) {
3068 $array[] = new $class($cssstyle, $width);
3069 } else {
3070 if (!is_null($width)) {
3071 $array[] = new $class($cssstyle, $width);
3073 if (!is_null($style)) {
3074 $array[] = new $class($cssstyle, $style);
3076 if (!is_null($color)) {
3077 $array[] = new $class($cssstyle, $color);
3080 return true;
3085 * A border colour style
3087 * @package core
3088 * @subpackage cssoptimiser
3089 * @copyright 2012 Sam Hemelryk
3090 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3092 class css_style_bordercolor extends css_style_color {
3095 * Creates a new border colour style
3097 * Based upon the colour style
3099 * @param mixed $value
3100 * @return Array of css_style_bordercolor
3102 public static function init($value) {
3103 $value = preg_replace('#\s+#', ' ', $value);
3104 $bits = explode(' ', $value, 4);
3106 $top = $right = $bottom = $left = null;
3107 if (count($bits) > 0) {
3108 $top = $right = $bottom = $left = array_shift($bits);
3110 if (count($bits) > 0) {
3111 $right = $left = array_shift($bits);
3113 if (count($bits) > 0) {
3114 $bottom = array_shift($bits);
3116 if (count($bits) > 0) {
3117 $left = array_shift($bits);
3119 return array(
3120 css_style_bordertopcolor::init($top),
3121 css_style_borderrightcolor::init($right),
3122 css_style_borderbottomcolor::init($bottom),
3123 css_style_borderleftcolor::init($left)
3128 * Consolidate this to a single border style
3130 * @return string
3132 public function consolidate_to() {
3133 return 'border';
3137 * Cleans the value
3139 * @param string $value Cleans the provided value optimising it if possible
3140 * @return string
3142 protected function clean_value($value) {
3143 $values = explode(' ', $value);
3144 $values = array_map('parent::clean_value', $values);
3145 return join (' ', $values);
3149 * Outputs this style
3151 * @param string $overridevalue
3152 * @return string
3154 public function out($overridevalue = null) {
3155 if ($overridevalue === null) {
3156 $overridevalue = $this->value;
3158 $values = explode(' ', $overridevalue);
3159 $values = array_map('css_style_color::shrink_value', $values);
3160 return parent::out(join (' ', $values));
3165 * A border left style
3167 * @package core
3168 * @subpackage cssoptimiser
3169 * @copyright 2012 Sam Hemelryk
3170 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3172 class css_style_borderleft extends css_style_generic {
3175 * Initialises the border left style into individual components
3177 * @param string $value
3178 * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor
3180 public static function init($value) {
3181 $value = preg_replace('#\s+#', ' ', $value);
3182 $bits = explode(' ', $value, 3);
3184 $return = array();
3185 if (count($bits) > 0) {
3186 $return[] = css_style_borderleftwidth::init(array_shift($bits));
3188 if (count($bits) > 0) {
3189 $return[] = css_style_borderleftstyle::init(array_shift($bits));
3191 if (count($bits) > 0) {
3192 $return[] = css_style_borderleftcolor::init(array_shift($bits));
3194 return $return;
3198 * Consolidate this to a single border style
3200 * @return string
3202 public function consolidate_to() {
3203 return 'border';
3208 * A border right style
3210 * @package core
3211 * @subpackage cssoptimiser
3212 * @copyright 2012 Sam Hemelryk
3213 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3215 class css_style_borderright extends css_style_generic {
3218 * Initialises the border right style into individual components
3220 * @param string $value The value of the style
3221 * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor
3223 public static function init($value) {
3224 $value = preg_replace('#\s+#', ' ', $value);
3225 $bits = explode(' ', $value, 3);
3227 $return = array();
3228 if (count($bits) > 0) {
3229 $return[] = css_style_borderrightwidth::init(array_shift($bits));
3231 if (count($bits) > 0) {
3232 $return[] = css_style_borderrightstyle::init(array_shift($bits));
3234 if (count($bits) > 0) {
3235 $return[] = css_style_borderrightcolor::init(array_shift($bits));
3237 return $return;
3241 * Consolidate this to a single border style
3243 * @return string
3245 public function consolidate_to() {
3246 return 'border';
3251 * A border top style
3253 * @package core
3254 * @subpackage cssoptimiser
3255 * @copyright 2012 Sam Hemelryk
3256 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3258 class css_style_bordertop extends css_style_generic {
3261 * Initialises the border top style into individual components
3263 * @param string $value The value of the style
3264 * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor
3266 public static function init($value) {
3267 $value = preg_replace('#\s+#', ' ', $value);
3268 $bits = explode(' ', $value, 3);
3270 $return = array();
3271 if (count($bits) > 0) {
3272 $return[] = css_style_bordertopwidth::init(array_shift($bits));
3274 if (count($bits) > 0) {
3275 $return[] = css_style_bordertopstyle::init(array_shift($bits));
3277 if (count($bits) > 0) {
3278 $return[] = css_style_bordertopcolor::init(array_shift($bits));
3280 return $return;
3284 * Consolidate this to a single border style
3286 * @return string
3288 public function consolidate_to() {
3289 return 'border';
3294 * A border bottom style
3296 * @package core
3297 * @subpackage cssoptimiser
3298 * @copyright 2012 Sam Hemelryk
3299 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3301 class css_style_borderbottom extends css_style_generic {
3304 * Initialises the border bottom style into individual components
3306 * @param string $value The value of the style
3307 * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor
3309 public static function init($value) {
3310 $value = preg_replace('#\s+#', ' ', $value);
3311 $bits = explode(' ', $value, 3);
3313 $return = array();
3314 if (count($bits) > 0) {
3315 $return[] = css_style_borderbottomwidth::init(array_shift($bits));
3317 if (count($bits) > 0) {
3318 $return[] = css_style_borderbottomstyle::init(array_shift($bits));
3320 if (count($bits) > 0) {
3321 $return[] = css_style_borderbottomcolor::init(array_shift($bits));
3323 return $return;
3327 * Consolidate this to a single border style
3329 * @return string
3331 public function consolidate_to() {
3332 return 'border';
3337 * A border width style
3339 * @package core
3340 * @subpackage cssoptimiser
3341 * @copyright 2012 Sam Hemelryk
3342 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3344 class css_style_borderwidth extends css_style_width {
3347 * Creates a new border colour style
3349 * Based upon the colour style
3351 * @param string $value The value of the style
3352 * @return array Array of css_style_border*width
3354 public static function init($value) {
3355 $value = preg_replace('#\s+#', ' ', $value);
3356 $bits = explode(' ', $value, 4);
3358 $top = $right = $bottom = $left = null;
3359 if (count($bits) > 0) {
3360 $top = $right = $bottom = $left = array_shift($bits);
3362 if (count($bits) > 0) {
3363 $right = $left = array_shift($bits);
3365 if (count($bits) > 0) {
3366 $bottom = array_shift($bits);
3368 if (count($bits) > 0) {
3369 $left = array_shift($bits);
3371 return array(
3372 css_style_bordertopwidth::init($top),
3373 css_style_borderrightwidth::init($right),
3374 css_style_borderbottomwidth::init($bottom),
3375 css_style_borderleftwidth::init($left)
3380 * Consolidate this to a single border style
3382 * @return string
3384 public function consolidate_to() {
3385 return 'border';
3389 * Checks if the width is valid
3390 * @return bool
3392 public function is_valid() {
3393 return self::is_border_width($this->value);
3397 * Cleans the provided value
3399 * @param mixed $value Cleans the provided value optimising it if possible
3400 * @return string
3402 protected function clean_value($value) {
3403 $isvalid = self::is_border_width($value);
3404 if (!$isvalid) {
3405 $this->set_error('Invalid width specified for '.$this->name);
3406 } else if (preg_match('#^0\D+$#', $value)) {
3407 return '0';
3409 return trim($value);
3413 * Returns true if the provided value is a permitted border width
3414 * @param string $value The value to check
3415 * @return bool
3417 public static function is_border_width($value) {
3418 $altwidthvalues = array('thin', 'medium', 'thick');
3419 return css_is_width($value) || in_array($value, $altwidthvalues);
3424 * A border style style
3426 * @package core
3427 * @subpackage cssoptimiser
3428 * @copyright 2012 Sam Hemelryk
3429 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3431 class css_style_borderstyle extends css_style_generic {
3434 * Creates a new border colour style
3436 * Based upon the colour style
3438 * @param string $value The value of the style
3439 * @return array Array of css_style_border*style
3441 public static function init($value) {
3442 $value = preg_replace('#\s+#', ' ', $value);
3443 $bits = explode(' ', $value, 4);
3445 $top = $right = $bottom = $left = null;
3446 if (count($bits) > 0) {
3447 $top = $right = $bottom = $left = array_shift($bits);
3449 if (count($bits) > 0) {
3450 $right = $left = array_shift($bits);
3452 if (count($bits) > 0) {
3453 $bottom = array_shift($bits);
3455 if (count($bits) > 0) {
3456 $left = array_shift($bits);
3458 return array(
3459 css_style_bordertopstyle::init($top),
3460 css_style_borderrightstyle::init($right),
3461 css_style_borderbottomstyle::init($bottom),
3462 css_style_borderleftstyle::init($left)
3467 * Consolidate this to a single border style
3469 * @return string
3471 public function consolidate_to() {
3472 return 'border';
3477 * A border top colour style
3479 * @package core
3480 * @subpackage cssoptimiser
3481 * @copyright 2012 Sam Hemelryk
3482 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3484 class css_style_bordertopcolor extends css_style_bordercolor {
3487 * Initialises this style object
3489 * @param string $value The value of the style
3490 * @return css_style_bordertopcolor
3492 public static function init($value) {
3493 return new css_style_bordertopcolor('border-top-color', $value);
3497 * Consolidate this to a single border style
3499 * @return string
3501 public function consolidate_to() {
3502 return 'border';
3507 * A border left colour style
3509 * @package core
3510 * @subpackage cssoptimiser
3511 * @copyright 2012 Sam Hemelryk
3512 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3514 class css_style_borderleftcolor extends css_style_bordercolor {
3517 * Initialises this style object
3519 * @param string $value The value of the style
3520 * @return css_style_borderleftcolor
3522 public static function init($value) {
3523 return new css_style_borderleftcolor('border-left-color', $value);
3527 * Consolidate this to a single border style
3529 * @return string
3531 public function consolidate_to() {
3532 return 'border';
3537 * A border right colour style
3539 * @package core
3540 * @subpackage cssoptimiser
3541 * @copyright 2012 Sam Hemelryk
3542 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3544 class css_style_borderrightcolor extends css_style_bordercolor {
3547 * Initialises this style object
3549 * @param string $value The value of the style
3550 * @return css_style_borderrightcolor
3552 public static function init($value) {
3553 return new css_style_borderrightcolor('border-right-color', $value);
3557 * Consolidate this to a single border style
3559 * @return string
3561 public function consolidate_to() {
3562 return 'border';
3567 * A border bottom colour style
3569 * @package core
3570 * @subpackage cssoptimiser
3571 * @copyright 2012 Sam Hemelryk
3572 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3574 class css_style_borderbottomcolor extends css_style_bordercolor {
3577 * Initialises this style object
3579 * @param string $value The value of the style
3580 * @return css_style_borderbottomcolor
3582 public static function init($value) {
3583 return new css_style_borderbottomcolor('border-bottom-color', $value);
3587 * Consolidate this to a single border style
3589 * @return string
3591 public function consolidate_to() {
3592 return 'border';
3597 * A border width top style
3599 * @package core
3600 * @subpackage cssoptimiser
3601 * @copyright 2012 Sam Hemelryk
3602 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3604 class css_style_bordertopwidth extends css_style_borderwidth {
3607 * Initialises this style object
3609 * @param string $value The value of the style
3610 * @return css_style_bordertopwidth
3612 public static function init($value) {
3613 return new css_style_bordertopwidth('border-top-width', $value);
3617 * Consolidate this to a single border style
3619 * @return string
3621 public function consolidate_to() {
3622 return 'border';
3627 * A border width left style
3629 * @package core
3630 * @subpackage cssoptimiser
3631 * @copyright 2012 Sam Hemelryk
3632 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3634 class css_style_borderleftwidth extends css_style_borderwidth {
3637 * Initialises this style object
3639 * @param string $value The value of the style
3640 * @return css_style_borderleftwidth
3642 public static function init($value) {
3643 return new css_style_borderleftwidth('border-left-width', $value);
3647 * Consolidate this to a single border style
3649 * @return string
3651 public function consolidate_to() {
3652 return 'border';
3657 * A border width right style
3659 * @package core
3660 * @subpackage cssoptimiser
3661 * @copyright 2012 Sam Hemelryk
3662 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3664 class css_style_borderrightwidth extends css_style_borderwidth {
3667 * Initialises this style object
3669 * @param string $value The value of the style
3670 * @return css_style_borderrightwidth
3672 public static function init($value) {
3673 return new css_style_borderrightwidth('border-right-width', $value);
3677 * Consolidate this to a single border style
3679 * @return string
3681 public function consolidate_to() {
3682 return 'border';
3687 * A border width bottom style
3689 * @package core
3690 * @subpackage cssoptimiser
3691 * @copyright 2012 Sam Hemelryk
3692 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3694 class css_style_borderbottomwidth extends css_style_borderwidth {
3697 * Initialises this style object
3699 * @param string $value The value of the style
3700 * @return css_style_borderbottomwidth
3702 public static function init($value) {
3703 return new css_style_borderbottomwidth('border-bottom-width', $value);
3707 * Consolidate this to a single border style
3709 * @return string
3711 public function consolidate_to() {
3712 return 'border';
3717 * A border top style
3719 * @package core
3720 * @subpackage cssoptimiser
3721 * @copyright 2012 Sam Hemelryk
3722 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3724 class css_style_bordertopstyle extends css_style_borderstyle {
3727 * Initialises this style object
3729 * @param string $value The value of the style
3730 * @return css_style_bordertopstyle
3732 public static function init($value) {
3733 return new css_style_bordertopstyle('border-top-style', $value);
3737 * Consolidate this to a single border style
3739 * @return string
3741 public function consolidate_to() {
3742 return 'border';
3747 * A border left style
3749 * @package core
3750 * @subpackage cssoptimiser
3751 * @copyright 2012 Sam Hemelryk
3752 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3754 class css_style_borderleftstyle extends css_style_borderstyle {
3757 * Initialises this style object
3759 * @param string $value The value of the style
3760 * @return css_style_borderleftstyle
3762 public static function init($value) {
3763 return new css_style_borderleftstyle('border-left-style', $value);
3767 * Consolidate this to a single border style
3769 * @return string
3771 public function consolidate_to() {
3772 return 'border';
3777 * A border right style
3779 * @package core
3780 * @subpackage cssoptimiser
3781 * @copyright 2012 Sam Hemelryk
3782 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3784 class css_style_borderrightstyle extends css_style_borderstyle {
3787 * Initialises this style object
3789 * @param string $value The value of the style
3790 * @return css_style_borderrightstyle
3792 public static function init($value) {
3793 return new css_style_borderrightstyle('border-right-style', $value);
3797 * Consolidate this to a single border style
3799 * @return string
3801 public function consolidate_to() {
3802 return 'border';
3807 * A border bottom style
3809 * @package core
3810 * @subpackage cssoptimiser
3811 * @copyright 2012 Sam Hemelryk
3812 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3814 class css_style_borderbottomstyle extends css_style_borderstyle {
3817 * Initialises this style object
3819 * @param string $value The value for the style
3820 * @return css_style_borderbottomstyle
3822 public static function init($value) {
3823 return new css_style_borderbottomstyle('border-bottom-style', $value);
3827 * Consolidate this to a single border style
3829 * @return string
3831 public function consolidate_to() {
3832 return 'border';
3837 * A background style
3839 * @package core
3840 * @subpackage cssoptimiser
3841 * @copyright 2012 Sam Hemelryk
3842 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3844 class css_style_background extends css_style implements core_css_consolidatable_style {
3847 * Initialises a background style
3849 * @param string $value The value of the style
3850 * @return array An array of background component.
3852 public static function init($value) {
3853 // Colour - image - repeat - attachment - position.
3854 $imageurl = null;
3855 if (preg_match('#url\(([^\)]+)\)#', $value, $matches)) {
3856 $imageurl = trim($matches[1]);
3857 $value = str_replace($matches[1], '', $value);
3860 // Switch out the brackets so that they don't get messed up when we explode.
3861 $brackets = array();
3862 $bracketcount = 0;
3863 while (preg_match('#\([^\)\(]+\)#', $value, $matches)) {
3864 $key = "##BRACKET-{$bracketcount}##";
3865 $bracketcount++;
3866 $brackets[$key] = $matches[0];
3867 $value = str_replace($matches[0], $key, $value);
3870 $important = (stripos($value, '!important') !== false);
3871 if ($important) {
3872 // Great some genius put !important in the background shorthand property.
3873 $value = str_replace('!important', '', $value);
3876 $value = preg_replace('#\s+#', ' ', $value);
3877 $bits = explode(' ', $value);
3879 foreach ($bits as $key => $bit) {
3880 $bits[$key] = self::replace_bracket_placeholders($bit, $brackets);
3882 unset($bracketcount);
3883 unset($brackets);
3885 $repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit');
3886 $attachments = array('scroll' , 'fixed', 'inherit');
3887 $positions = array('top', 'left', 'bottom', 'right', 'center');
3889 /* @var css_style_background[] $return */
3890 $return = array();
3891 $unknownbits = array();
3893 $color = self::NULL_VALUE;
3894 if (count($bits) > 0 && css_is_colour(reset($bits))) {
3895 $color = array_shift($bits);
3898 $image = self::NULL_VALUE;
3899 if (count($bits) > 0 && preg_match('#^\s*(none|inherit|url\(\))\s*$#', reset($bits))) {
3900 $image = array_shift($bits);
3901 if ($image == 'url()') {
3902 $image = "url({$imageurl})";
3906 $repeat = self::NULL_VALUE;
3907 if (count($bits) > 0 && in_array(reset($bits), $repeats)) {
3908 $repeat = array_shift($bits);
3911 $attachment = self::NULL_VALUE;
3912 if (count($bits) > 0 && in_array(reset($bits), $attachments)) {
3913 // Scroll , fixed, inherit.
3914 $attachment = array_shift($bits);
3917 $position = self::NULL_VALUE;
3918 if (count($bits) > 0) {
3919 $widthbits = array();
3920 foreach ($bits as $bit) {
3921 if (in_array($bit, $positions) || css_is_width($bit)) {
3922 $widthbits[] = $bit;
3923 } else {
3924 $unknownbits[] = $bit;
3927 if (count($widthbits)) {
3928 $position = join(' ', $widthbits);
3932 if (count($unknownbits)) {
3933 foreach ($unknownbits as $bit) {
3934 $bit = trim($bit);
3935 if ($color === self::NULL_VALUE && css_is_colour($bit)) {
3936 $color = $bit;
3937 } else if ($repeat === self::NULL_VALUE && in_array($bit, $repeats)) {
3938 $repeat = $bit;
3939 } else if ($attachment === self::NULL_VALUE && in_array($bit, $attachments)) {
3940 $attachment = $bit;
3941 } else if ($bit !== '') {
3942 $advanced = css_style_background_advanced::init($bit);
3943 if ($important) {
3944 $advanced->set_important();
3946 $return[] = $advanced;
3951 if ($color === self::NULL_VALUE &&
3952 $image === self::NULL_VALUE &&
3953 $repeat === self::NULL_VALUE && $attachment === self::NULL_VALUE &&
3954 $position === self::NULL_VALUE) {
3955 // All primaries are null, return without doing anything else. There may be advanced madness there.
3956 return $return;
3959 $return[] = css_style_backgroundcolor::init($color);
3960 $return[] = css_style_backgroundimage::init($image);
3961 $return[] = css_style_backgroundrepeat::init($repeat);
3962 $return[] = css_style_backgroundattachment::init($attachment);
3963 $return[] = css_style_backgroundposition::init($position);
3965 if ($important) {
3966 foreach ($return as $style) {
3967 $style->set_important();
3971 return $return;
3975 * Static helper method to switch in bracket replacements
3977 * @param string $value
3978 * @param array $placeholders
3979 * @return string
3981 protected static function replace_bracket_placeholders($value, array $placeholders) {
3982 while (preg_match('/##BRACKET-\d+##/', $value, $matches)) {
3983 $value = str_replace($matches[0], $placeholders[$matches[0]], $value);
3985 return $value;
3989 * Consolidates background styles into a single background style
3991 * @param css_style_background[] $styles Consolidates the provided array of background styles
3992 * @return css_style[] Consolidated optimised background styles
3994 public static function consolidate(array $styles) {
3996 if (empty($styles)) {
3997 return $styles;
4000 $color = null;
4001 $image = null;
4002 $repeat = null;
4003 $attachment = null;
4004 $position = null;
4005 $size = null;
4006 $origin = null;
4007 $clip = null;
4009 $someimportant = false;
4010 $allimportant = null;
4011 foreach ($styles as $style) {
4012 if ($style instanceof css_style_backgroundimage_advanced) {
4013 continue;
4015 if ($style->is_important()) {
4016 $someimportant = true;
4017 if ($allimportant === null) {
4018 $allimportant = true;
4020 } else if ($allimportant === true) {
4021 $allimportant = false;
4025 /* @var css_style[] $organisedstyles */
4026 $organisedstyles = array();
4027 /* @var css_style[] $advancedstyles */
4028 $advancedstyles = array();
4029 /* @var css_style[] $importantstyles */
4030 $importantstyles = array();
4031 foreach ($styles as $style) {
4032 if ($style instanceof css_style_backgroundimage_advanced) {
4033 $advancedstyles[] = $style;
4034 continue;
4036 if ($someimportant && !$allimportant && $style->is_important()) {
4037 $importantstyles[] = $style;
4038 continue;
4040 $organisedstyles[$style->get_name()] = $style;
4041 switch ($style->get_name()) {
4042 case 'background-color' :
4043 $color = css_style_color::shrink_value($style->get_value(false));
4044 break;
4045 case 'background-image' :
4046 $image = $style->get_value(false);
4047 break;
4048 case 'background-repeat' :
4049 $repeat = $style->get_value(false);
4050 break;
4051 case 'background-attachment' :
4052 $attachment = $style->get_value(false);
4053 break;
4054 case 'background-position' :
4055 $position = $style->get_value(false);
4056 break;
4057 case 'background-clip' :
4058 $clip = $style->get_value();
4059 break;
4060 case 'background-origin' :
4061 $origin = $style->get_value();
4062 break;
4063 case 'background-size' :
4064 $size = $style->get_value();
4065 break;
4069 /* @var css_style[] $consolidatetosingle */
4070 $consolidatetosingle = array();
4071 if (!is_null($color) && !is_null($image) && !is_null($repeat) && !is_null($attachment) && !is_null($position)) {
4072 // We can use the shorthand background-style!
4073 if (!$organisedstyles['background-color']->is_special_empty_value()) {
4074 $consolidatetosingle[] = $color;
4076 if (!$organisedstyles['background-image']->is_special_empty_value()) {
4077 $consolidatetosingle[] = $image;
4079 if (!$organisedstyles['background-repeat']->is_special_empty_value()) {
4080 $consolidatetosingle[] = $repeat;
4082 if (!$organisedstyles['background-attachment']->is_special_empty_value()) {
4083 $consolidatetosingle[] = $attachment;
4085 if (!$organisedstyles['background-position']->is_special_empty_value()) {
4086 $consolidatetosingle[] = $position;
4088 // Reset them all to null so we don't use them again.
4089 $color = null;
4090 $image = null;
4091 $repeat = null;
4092 $attachment = null;
4093 $position = null;
4096 $return = array();
4097 // Single background style needs to come first.
4098 if (count($consolidatetosingle) > 0) {
4099 $returnstyle = new css_style_background('background', join(' ', $consolidatetosingle));
4100 if ($allimportant) {
4101 $returnstyle->set_important();
4103 $return[] = $returnstyle;
4105 foreach ($styles as $style) {
4106 $value = null;
4107 switch ($style->get_name()) {
4108 case 'background-color' :
4109 $value = $color;
4110 break;
4111 case 'background-image' :
4112 $value = $image;
4113 break;
4114 case 'background-repeat' :
4115 $value = $repeat;
4116 break;
4117 case 'background-attachment' :
4118 $value = $attachment;
4119 break;
4120 case 'background-position' :
4121 $value = $position;
4122 break;
4123 case 'background-clip' :
4124 $value = $clip;
4125 break;
4126 case 'background-origin':
4127 $value = $origin;
4128 break;
4129 case 'background-size':
4130 $value = $size;
4131 break;
4133 if (!is_null($value)) {
4134 $return[] = $style;
4137 $return = array_merge($return, $importantstyles, $advancedstyles);
4138 return $return;
4143 * A advanced background style that allows multiple values to preserve unknown entities
4145 * @package core
4146 * @subpackage cssoptimiser
4147 * @copyright 2012 Sam Hemelryk
4148 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4150 class css_style_background_advanced extends css_style_generic {
4152 * Creates a new background colour style
4154 * @param string $value The value of the style
4155 * @return css_style_backgroundimage
4157 public static function init($value) {
4158 $value = preg_replace('#\s+#', ' ', $value);
4159 return new css_style_background_advanced('background', $value);
4163 * Returns true because the advanced background image supports multiple values.
4164 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4166 * @return boolean
4168 public function allows_multiple_values() {
4169 return true;
4174 * A background colour style.
4176 * Based upon the colour style.
4178 * @package core
4179 * @subpackage cssoptimiser
4180 * @copyright 2012 Sam Hemelryk
4181 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4183 class css_style_backgroundcolor extends css_style_color {
4186 * Creates a new background colour style
4188 * @param string $value The value of the style
4189 * @return css_style_backgroundcolor
4191 public static function init($value) {
4192 return new css_style_backgroundcolor('background-color', $value);
4196 * css_style_backgroundcolor consolidates to css_style_background
4198 * @return string
4200 public function consolidate_to() {
4201 return 'background';
4205 * Returns true if the value for this style is the special null value.
4207 * This occurs if the shorthand background property was used but no proper value
4208 * was specified for this style.
4209 * This leads to a null value being used unless otherwise overridden.
4211 * @return bool
4213 public function is_special_empty_value() {
4214 return ($this->value === self::NULL_VALUE);
4218 * Returns true if the value for this style is valid
4219 * @return bool
4221 public function is_valid() {
4222 return $this->is_special_empty_value() || parent::is_valid();
4227 * A background image style.
4229 * @package core
4230 * @subpackage cssoptimiser
4231 * @copyright 2012 Sam Hemelryk
4232 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4234 class css_style_backgroundimage extends css_style_generic {
4237 * Creates a new background image style
4239 * @param string $value The value of the style
4240 * @return css_style_backgroundimage
4242 public static function init($value) {
4243 if ($value !== self::NULL_VALUE && !preg_match('#^\s*(none|inherit|url\()#i', $value)) {
4244 return css_style_backgroundimage_advanced::init($value);
4246 return new css_style_backgroundimage('background-image', $value);
4250 * Consolidates this style into a single background style
4252 * @return string
4254 public function consolidate_to() {
4255 return 'background';
4259 * Returns true if the value for this style is the special null value.
4261 * This occurs if the shorthand background property was used but no proper value
4262 * was specified for this style.
4263 * This leads to a null value being used unless otherwise overridden.
4265 * @return bool
4267 public function is_special_empty_value() {
4268 return ($this->value === self::NULL_VALUE);
4272 * Returns true if the value for this style is valid
4273 * @return bool
4275 public function is_valid() {
4276 return $this->is_special_empty_value() || parent::is_valid();
4281 * A background image style that supports multiple values and masquerades as a background-image
4283 * @package core
4284 * @subpackage cssoptimiser
4285 * @copyright 2012 Sam Hemelryk
4286 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4288 class css_style_backgroundimage_advanced extends css_style_generic {
4290 * Creates a new background colour style
4292 * @param string $value The value of the style
4293 * @return css_style_backgroundimage
4295 public static function init($value) {
4296 $value = preg_replace('#\s+#', ' ', $value);
4297 return new css_style_backgroundimage_advanced('background-image', $value);
4301 * Returns true because the advanced background image supports multiple values.
4302 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4304 * @return boolean
4306 public function allows_multiple_values() {
4307 return true;
4312 * A background repeat style.
4314 * @package core
4315 * @subpackage cssoptimiser
4316 * @copyright 2012 Sam Hemelryk
4317 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4319 class css_style_backgroundrepeat extends css_style_generic {
4322 * Creates a new background colour style
4324 * @param string $value The value of the style
4325 * @return css_style_backgroundrepeat
4327 public static function init($value) {
4328 return new css_style_backgroundrepeat('background-repeat', $value);
4332 * Consolidates this style into a single background style
4334 * @return string
4336 public function consolidate_to() {
4337 return 'background';
4341 * Returns true if the value for this style is the special null value.
4343 * This occurs if the shorthand background property was used but no proper value
4344 * was specified for this style.
4345 * This leads to a null value being used unless otherwise overridden.
4347 * @return bool
4349 public function is_special_empty_value() {
4350 return ($this->value === self::NULL_VALUE);
4354 * Returns true if the value for this style is valid
4355 * @return bool
4357 public function is_valid() {
4358 return $this->is_special_empty_value() || parent::is_valid();
4363 * A background attachment style.
4365 * @package core
4366 * @subpackage cssoptimiser
4367 * @copyright 2012 Sam Hemelryk
4368 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4370 class css_style_backgroundattachment extends css_style_generic {
4373 * Creates a new background colour style
4375 * @param string $value The value of the style
4376 * @return css_style_backgroundattachment
4378 public static function init($value) {
4379 return new css_style_backgroundattachment('background-attachment', $value);
4383 * Consolidates this style into a single background style
4385 * @return string
4387 public function consolidate_to() {
4388 return 'background';
4392 * Returns true if the value for this style is the special null value.
4394 * This occurs if the shorthand background property was used but no proper value
4395 * was specified for this style.
4396 * This leads to a null value being used unless otherwise overridden.
4398 * @return bool
4400 public function is_special_empty_value() {
4401 return ($this->value === self::NULL_VALUE);
4405 * Returns true if the value for this style is valid
4406 * @return bool
4408 public function is_valid() {
4409 return $this->is_special_empty_value() || parent::is_valid();
4414 * A background position style.
4416 * @package core
4417 * @subpackage cssoptimiser
4418 * @copyright 2012 Sam Hemelryk
4419 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4421 class css_style_backgroundposition extends css_style_generic {
4424 * Creates a new background colour style
4426 * @param string $value The value of the style
4427 * @return css_style_backgroundposition
4429 public static function init($value) {
4430 return new css_style_backgroundposition('background-position', $value);
4434 * Consolidates this style into a single background style
4436 * @return string
4438 public function consolidate_to() {
4439 return 'background';
4443 * Returns true if the value for this style is the special null value.
4445 * This occurs if the shorthand background property was used but no proper value
4446 * was specified for this style.
4447 * This leads to a null value being used unless otherwise overridden.
4449 * @return bool
4451 public function is_special_empty_value() {
4452 return ($this->value === self::NULL_VALUE);
4456 * Returns true if the value for this style is valid
4457 * @return bool
4459 public function is_valid() {
4460 return $this->is_special_empty_value() || parent::is_valid();
4465 * A background size style.
4467 * @package core
4468 * @subpackage cssoptimiser
4469 * @copyright 2012 Sam Hemelryk
4470 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4472 class css_style_backgroundsize extends css_style_generic {
4475 * Creates a new background size style
4477 * @param string $value The value of the style
4478 * @return css_style_backgroundposition
4480 public static function init($value) {
4481 return new css_style_backgroundsize('background-size', $value);
4485 * Consolidates this style into a single background style
4487 * @return string
4489 public function consolidate_to() {
4490 return 'background';
4495 * A background clip style.
4497 * @package core
4498 * @subpackage cssoptimiser
4499 * @copyright 2012 Sam Hemelryk
4500 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4502 class css_style_backgroundclip extends css_style_generic {
4505 * Creates a new background clip style
4507 * @param string $value The value of the style
4508 * @return css_style_backgroundposition
4510 public static function init($value) {
4511 return new css_style_backgroundclip('background-clip', $value);
4515 * Consolidates this style into a single background style
4517 * @return string
4519 public function consolidate_to() {
4520 return 'background';
4525 * A background origin style.
4527 * @package core
4528 * @subpackage cssoptimiser
4529 * @copyright 2012 Sam Hemelryk
4530 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4532 class css_style_backgroundorigin extends css_style_generic {
4535 * Creates a new background origin style
4537 * @param string $value The value of the style
4538 * @return css_style_backgroundposition
4540 public static function init($value) {
4541 return new css_style_backgroundorigin('background-origin', $value);
4545 * Consolidates this style into a single background style
4547 * @return string
4549 public function consolidate_to() {
4550 return 'background';
4555 * A padding style.
4557 * @package core
4558 * @subpackage cssoptimiser
4559 * @copyright 2012 Sam Hemelryk
4560 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4562 class css_style_padding extends css_style_width implements core_css_consolidatable_style {
4565 * Initialises this padding style into several individual padding styles
4567 * @param string $value The value fo the style
4568 * @return array An array of padding styles
4570 public static function init($value) {
4571 $important = '';
4572 if (strpos($value, '!important') !== false) {
4573 $important = ' !important';
4574 $value = str_replace('!important', '', $value);
4577 $value = preg_replace('#\s+#', ' ', trim($value));
4578 $bits = explode(' ', $value, 4);
4580 $top = $right = $bottom = $left = null;
4581 if (count($bits) > 0) {
4582 $top = $right = $bottom = $left = array_shift($bits);
4584 if (count($bits) > 0) {
4585 $right = $left = array_shift($bits);
4587 if (count($bits) > 0) {
4588 $bottom = array_shift($bits);
4590 if (count($bits) > 0) {
4591 $left = array_shift($bits);
4593 return array(
4594 new css_style_paddingtop('padding-top', $top.$important),
4595 new css_style_paddingright('padding-right', $right.$important),
4596 new css_style_paddingbottom('padding-bottom', $bottom.$important),
4597 new css_style_paddingleft('padding-left', $left.$important)
4602 * Consolidates several padding styles into a single style.
4604 * @param css_style_padding[] $styles Array of padding styles
4605 * @return css_style[] Optimised+consolidated array of padding styles
4607 public static function consolidate(array $styles) {
4608 if (count($styles) != 4) {
4609 return $styles;
4612 $someimportant = false;
4613 $allimportant = null;
4614 $notimportantequal = null;
4615 $firstvalue = null;
4616 foreach ($styles as $style) {
4617 if ($style->is_important()) {
4618 $someimportant = true;
4619 if ($allimportant === null) {
4620 $allimportant = true;
4622 } else {
4623 if ($allimportant === true) {
4624 $allimportant = false;
4626 if ($firstvalue == null) {
4627 $firstvalue = $style->get_value(false);
4628 $notimportantequal = true;
4629 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
4630 $notimportantequal = false;
4635 if ($someimportant && !$allimportant && !$notimportantequal) {
4636 return $styles;
4639 if ($someimportant && !$allimportant && $notimportantequal) {
4640 $return = array(
4641 new css_style_padding('padding', $firstvalue)
4643 foreach ($styles as $style) {
4644 if ($style->is_important()) {
4645 $return[] = $style;
4648 return $return;
4649 } else {
4650 $top = null;
4651 $right = null;
4652 $bottom = null;
4653 $left = null;
4654 foreach ($styles as $style) {
4655 switch ($style->get_name()) {
4656 case 'padding-top' :
4657 $top = $style->get_value(false);
4658 break;
4659 case 'padding-right' :
4660 $right = $style->get_value(false);
4661 break;
4662 case 'padding-bottom' :
4663 $bottom = $style->get_value(false);
4664 break;
4665 case 'padding-left' :
4666 $left = $style->get_value(false);
4667 break;
4670 if ($top == $bottom && $left == $right) {
4671 if ($top == $left) {
4672 $returnstyle = new css_style_padding('padding', $top);
4673 } else {
4674 $returnstyle = new css_style_padding('padding', "{$top} {$left}");
4676 } else if ($left == $right) {
4677 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom}");
4678 } else {
4679 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom} {$left}");
4681 if ($allimportant) {
4682 $returnstyle->set_important();
4684 return array($returnstyle);
4690 * A padding top style.
4692 * @package core
4693 * @subpackage cssoptimiser
4694 * @copyright 2012 Sam Hemelryk
4695 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4697 class css_style_paddingtop extends css_style_padding {
4700 * Initialises this style
4702 * @param string $value The value of the style
4703 * @return css_style_paddingtop
4705 public static function init($value) {
4706 return new css_style_paddingtop('padding-top', $value);
4710 * Consolidates this style into a single padding style
4712 * @return string
4714 public function consolidate_to() {
4715 return 'padding';
4720 * A padding right style.
4722 * @package core
4723 * @subpackage cssoptimiser
4724 * @copyright 2012 Sam Hemelryk
4725 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4727 class css_style_paddingright extends css_style_padding {
4730 * Initialises this style
4732 * @param string $value The value of the style
4733 * @return css_style_paddingright
4735 public static function init($value) {
4736 return new css_style_paddingright('padding-right', $value);
4740 * Consolidates this style into a single padding style
4742 * @return string
4744 public function consolidate_to() {
4745 return 'padding';
4750 * A padding bottom style.
4752 * @package core
4753 * @subpackage cssoptimiser
4754 * @copyright 2012 Sam Hemelryk
4755 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4757 class css_style_paddingbottom extends css_style_padding {
4760 * Initialises this style
4762 * @param string $value The value of the style
4763 * @return css_style_paddingbottom
4765 public static function init($value) {
4766 return new css_style_paddingbottom('padding-bottom', $value);
4770 * Consolidates this style into a single padding style
4772 * @return string
4774 public function consolidate_to() {
4775 return 'padding';
4780 * A padding left style.
4782 * @package core
4783 * @subpackage cssoptimiser
4784 * @copyright 2012 Sam Hemelryk
4785 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4787 class css_style_paddingleft extends css_style_padding {
4790 * Initialises this style
4792 * @param string $value The value of the style
4793 * @return css_style_paddingleft
4795 public static function init($value) {
4796 return new css_style_paddingleft('padding-left', $value);
4800 * Consolidates this style into a single padding style
4802 * @return string
4804 public function consolidate_to() {
4805 return 'padding';
4810 * A cursor style.
4812 * @package core
4813 * @subpackage cssoptimiser
4814 * @copyright 2012 Sam Hemelryk
4815 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4817 class css_style_cursor extends css_style_generic {
4819 * Initialises a new cursor style
4820 * @param string $value
4821 * @return css_style_cursor
4823 public static function init($value) {
4824 return new css_style_cursor('cursor', $value);
4827 * Cleans the given value and returns it.
4829 * @param string $value
4830 * @return string
4832 protected function clean_value($value) {
4833 // Allowed values for the cursor style.
4834 $allowed = array('auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize',
4835 'pointer', 'progress', 's-resize', 'se-resize', 'sw-resize', 'text', 'w-resize', 'wait', 'inherit');
4836 // Has to be one of the allowed values of an image to use. Loosely match the image... doesn't need to be thorough.
4837 if (!in_array($value, $allowed) && !preg_match('#\.[a-zA-Z0-9_\-]{1,5}$#', $value)) {
4838 $this->set_error('Invalid or unexpected cursor value specified: '.$value);
4840 return trim($value);
4845 * A vertical alignment style.
4847 * @package core
4848 * @subpackage cssoptimiser
4849 * @copyright 2012 Sam Hemelryk
4850 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4852 class css_style_verticalalign extends css_style_generic {
4854 * Initialises a new vertical alignment style
4855 * @param string $value
4856 * @return css_style_verticalalign
4858 public static function init($value) {
4859 return new css_style_verticalalign('vertical-align', $value);
4862 * Cleans the given value and returns it.
4864 * @param string $value
4865 * @return string
4867 protected function clean_value($value) {
4868 $allowed = array('baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'inherit');
4869 if (!css_is_width($value) && !in_array($value, $allowed)) {
4870 $this->set_error('Invalid vertical-align value specified: '.$value);
4872 return trim($value);
4877 * A float style.
4879 * @package core
4880 * @subpackage cssoptimiser
4881 * @copyright 2012 Sam Hemelryk
4882 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4884 class css_style_float extends css_style_generic {
4886 * Initialises a new float style
4887 * @param string $value
4888 * @return css_style_float
4890 public static function init($value) {
4891 return new css_style_float('float', $value);
4894 * Cleans the given value and returns it.
4896 * @param string $value
4897 * @return string
4899 protected function clean_value($value) {
4900 $allowed = array('left', 'right', 'none', 'inherit');
4901 if (!css_is_width($value) && !in_array($value, $allowed)) {
4902 $this->set_error('Invalid float value specified: '.$value);
4904 return trim($value);