Merge branch 'wip-MDL-50923-master' of git://github.com/marinaglancy/moodle
[moodle.git] / lib / csslib.php
blob2bd94d6c1a8795c3318381964ed9953bbfca572d
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 * NOTE: these functions are not expected to be used from any addons.
24 * @package core
25 * @subpackage cssoptimiser
26 * @copyright 2012 Sam Hemelryk
27 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
30 defined('MOODLE_INTERNAL') || die();
32 if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) {
33 // This can be also set in config.php file,
34 // it needs to be higher than the time it takes to generate all CSS content.
35 define('THEME_DESIGNER_CACHE_LIFETIME', 10);
38 /**
39 * Stores CSS in a file at the given path.
41 * This function either succeeds or throws an exception.
43 * @param theme_config $theme The theme that the CSS belongs to.
44 * @param string $csspath The path to store the CSS at.
45 * @param string $csscontent the complete CSS in one string
46 * @param bool $chunk If set to true these files will be chunked to ensure
47 * that no one file contains more than 4095 selectors.
48 * @param string $chunkurl If the CSS is be chunked then we need to know the URL
49 * to use for the chunked files.
51 function css_store_css(theme_config $theme, $csspath, $csscontent, $chunk = false, $chunkurl = null) {
52 global $CFG;
54 clearstatcache();
55 if (!file_exists(dirname($csspath))) {
56 @mkdir(dirname($csspath), $CFG->directorypermissions, true);
59 // Prevent serving of incomplete file from concurrent request,
60 // the rename() should be more atomic than fwrite().
61 ignore_user_abort(true);
63 // First up write out the single file for all those using decent browsers.
64 css_write_file($csspath, $csscontent);
66 if ($chunk) {
67 // If we need to chunk the CSS for browsers that are sub-par.
68 $css = css_chunk_by_selector_count($csscontent, $chunkurl);
69 $files = count($css);
70 $count = 1;
71 foreach ($css as $content) {
72 if ($count === $files) {
73 // If there is more than one file and this IS the last file.
74 $filename = preg_replace('#\.css$#', '.0.css', $csspath);
75 } else {
76 // If there is more than one file and this is not the last file.
77 $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
79 $count++;
80 css_write_file($filename, $content);
84 ignore_user_abort(false);
85 if (connection_aborted()) {
86 die;
90 /**
91 * Writes a CSS file.
93 * @param string $filename
94 * @param string $content
96 function css_write_file($filename, $content) {
97 global $CFG;
98 if ($fp = fopen($filename.'.tmp', 'xb')) {
99 fwrite($fp, $content);
100 fclose($fp);
101 rename($filename.'.tmp', $filename);
102 @chmod($filename, $CFG->filepermissions);
103 @unlink($filename.'.tmp'); // Just in case anything fails.
108 * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
110 * The chunking will not split a group of selectors, or a media query. That means that
111 * if n > $maxselectors and there are n selectors grouped together,
112 * they will not be chunked and you could end up with more selectors than desired.
113 * The same applies for a media query that has more than n selectors.
115 * Also, as we do not split group of selectors or media queries, the chunking might
116 * not be as optimal as it could be, having files with less selectors than it could
117 * potentially contain.
119 * String functions used here are not compliant with unicode characters. But that is
120 * not an issue as the syntax of CSS is using ASCII codes. Even if we have unicode
121 * characters in comments, or in the property 'content: ""', it will behave correcly.
123 * Please note that this strips out the comments if chunking happens.
125 * @param string $css The CSS to chunk.
126 * @param string $importurl The URL to use for import statements.
127 * @param int $maxselectors The number of selectors to limit a chunk to.
128 * @param int $buffer Not used any more.
129 * @return array An array of CSS chunks.
131 function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
133 // Check if we need to chunk this CSS file.
134 $count = substr_count($css, ',') + substr_count($css, '{');
135 if ($count < $maxselectors) {
136 // The number of selectors is less then the max - we're fine.
137 return array($css);
140 $chunks = array(); // The final chunks.
141 $offsets = array(); // The indexes to chunk at.
142 $offset = 0; // The current offset.
143 $selectorcount = 0; // The number of selectors since the last split.
144 $lastvalidoffset = 0; // The last valid index to split at.
145 $lastvalidoffsetselectorcount = 0; // The number of selectors used at the time were could split.
146 $inrule = 0; // The number of rules we are in, should not be greater than 1.
147 $inmedia = false; // Whether or not we are in a media query.
148 $mediacoming = false; // Whether or not we are expeting a media query.
149 $currentoffseterror = null; // Not null when we have recorded an error for the current split.
150 $offseterrors = array(); // The offsets where we found errors.
152 // Remove the comments. Because it's easier, safer and probably a lot of other good reasons.
153 $css = preg_replace('#/\*(.*?)\*/#s', '', $css);
154 $strlen = strlen($css);
156 // Walk through the CSS content character by character.
157 for ($i = 1; $i <= $strlen; $i++) {
158 $char = $css[$i - 1];
159 $offset = $i;
161 // Is that a media query that I see coming towards us?
162 if ($char === '@') {
163 if (!$inmedia && substr($css, $offset, 5) === 'media') {
164 $mediacoming = true;
168 // So we are entering a rule or a media query...
169 if ($char === '{') {
170 if ($mediacoming) {
171 $inmedia = true;
172 $mediacoming = false;
173 } else {
174 $inrule++;
175 $selectorcount++;
179 // Let's count the number of selectors, but only if we are not in a rule, or in
180 // the definition of a media query, as they can contain commas too.
181 if (!$mediacoming && !$inrule && $char === ',') {
182 $selectorcount++;
185 // We reached the end of something.
186 if ($char === '}') {
187 // Oh, we are in a media query.
188 if ($inmedia) {
189 if (!$inrule) {
190 // This is the end of the media query.
191 $inmedia = false;
192 } else {
193 // We were in a rule, in the media query.
194 $inrule--;
196 } else {
197 $inrule--;
198 // Handle stupid broken CSS where there are too many } brackets,
199 // as this can cause it to break (with chunking) where it would
200 // coincidentally have worked otherwise.
201 if ($inrule < 0) {
202 $inrule = 0;
206 // We are not in a media query, and there is no pending rule, it is safe to split here.
207 if (!$inmedia && !$inrule) {
208 $lastvalidoffset = $offset;
209 $lastvalidoffsetselectorcount = $selectorcount;
213 // Alright, this is splitting time...
214 if ($selectorcount > $maxselectors) {
215 if (!$lastvalidoffset) {
216 // We must have reached more selectors into one set than we were allowed. That means that either
217 // the chunk size value is too small, or that we have a gigantic group of selectors, or that a media
218 // query contains more selectors than the chunk size. We have to ignore this because we do not
219 // support split inside a group of selectors or media query.
220 if ($currentoffseterror === null) {
221 $currentoffseterror = $offset;
222 $offseterrors[] = $currentoffseterror;
224 } else {
225 // We identify the offset to split at and reset the number of selectors found from there.
226 $offsets[] = $lastvalidoffset;
227 $selectorcount = $selectorcount - $lastvalidoffsetselectorcount;
228 $lastvalidoffset = 0;
229 $currentoffseterror = null;
234 // Report offset errors.
235 if (!empty($offseterrors)) {
236 debugging('Could not find a safe place to split at offset(s): ' . implode(', ', $offseterrors) . '. Those were ignored.',
237 DEBUG_DEVELOPER);
240 // Now that we have got the offets, we can chunk the CSS.
241 $offsetcount = count($offsets);
242 foreach ($offsets as $key => $index) {
243 $start = 0;
244 if ($key > 0) {
245 $start = $offsets[$key - 1];
247 // From somewhere up to the offset.
248 $chunks[] = substr($css, $start, $index - $start);
250 // Add the last chunk (if there is one), from the last offset to the end of the string.
251 if (end($offsets) != $strlen) {
252 $chunks[] = substr($css, end($offsets));
255 // The array $chunks now contains CSS split into perfect sized chunks.
256 // Import statements can only appear at the very top of a CSS file.
257 // Imported sheets are applied in the the order they are imported and
258 // are followed by the contents of the CSS.
259 // This is terrible for performance.
260 // It means we must put the import statements at the top of the last chunk
261 // to ensure that things are always applied in the correct order.
262 // This way the chunked files are included in the order they were chunked
263 // followed by the contents of the final chunk in the actual sheet.
264 $importcss = '';
265 $slashargs = strpos($importurl, '.php?') === false;
266 $parts = count($chunks);
267 for ($i = 1; $i < $parts; $i++) {
268 if ($slashargs) {
269 $importcss .= "@import url({$importurl}/chunk{$i});\n";
270 } else {
271 $importcss .= "@import url({$importurl}&chunk={$i});\n";
274 $importcss .= end($chunks);
275 $chunks[key($chunks)] = $importcss;
277 return $chunks;
281 * Sends a cached CSS file
283 * This function sends the cached CSS file. Remember it is generated on the first
284 * request, then optimised/minified, and finally cached for serving.
286 * @param string $csspath The path to the CSS file we want to serve.
287 * @param string $etag The revision to make sure we utilise any caches.
289 function css_send_cached_css($csspath, $etag) {
290 // 60 days only - the revision may get incremented quite often.
291 $lifetime = 60*60*24*60;
293 header('Etag: "'.$etag.'"');
294 header('Content-Disposition: inline; filename="styles.php"');
295 header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
296 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
297 header('Pragma: ');
298 header('Cache-Control: public, max-age='.$lifetime);
299 header('Accept-Ranges: none');
300 header('Content-Type: text/css; charset=utf-8');
301 if (!min_enable_zlib_compression()) {
302 header('Content-Length: '.filesize($csspath));
305 readfile($csspath);
306 die;
310 * Sends a cached CSS content
312 * @param string $csscontent The actual CSS markup.
313 * @param string $etag The revision to make sure we utilise any caches.
315 function css_send_cached_css_content($csscontent, $etag) {
316 // 60 days only - the revision may get incremented quite often.
317 $lifetime = 60*60*24*60;
319 header('Etag: "'.$etag.'"');
320 header('Content-Disposition: inline; filename="styles.php"');
321 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
322 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
323 header('Pragma: ');
324 header('Cache-Control: public, max-age='.$lifetime);
325 header('Accept-Ranges: none');
326 header('Content-Type: text/css; charset=utf-8');
327 if (!min_enable_zlib_compression()) {
328 header('Content-Length: '.strlen($csscontent));
331 echo($csscontent);
332 die;
336 * Sends CSS directly without caching it.
338 * This function takes a raw CSS string, optimises it if required, and then
339 * serves it.
340 * Turning both themedesignermode and CSS optimiser on at the same time is awful
341 * for performance because of the optimiser running here. However it was done so
342 * that theme designers could utilise the optimised output during development to
343 * help them optimise their CSS... not that they should write lazy CSS.
345 * @param string $css
347 function css_send_uncached_css($css) {
348 header('Content-Disposition: inline; filename="styles_debug.php"');
349 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
350 header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT');
351 header('Pragma: ');
352 header('Accept-Ranges: none');
353 header('Content-Type: text/css; charset=utf-8');
355 if (is_array($css)) {
356 $css = implode("\n\n", $css);
358 echo $css;
359 die;
363 * Send file not modified headers
365 * @param int $lastmodified
366 * @param string $etag
368 function css_send_unmodified($lastmodified, $etag) {
369 // 60 days only - the revision may get incremented quite often.
370 $lifetime = 60*60*24*60;
371 header('HTTP/1.1 304 Not Modified');
372 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
373 header('Cache-Control: public, max-age='.$lifetime);
374 header('Content-Type: text/css; charset=utf-8');
375 header('Etag: "'.$etag.'"');
376 if ($lastmodified) {
377 header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
379 die;
383 * Sends a 404 message about CSS not being found.
385 function css_send_css_not_found() {
386 header('HTTP/1.0 404 not found');
387 die('CSS was not found, sorry.');
391 * Determines if the given value is a valid CSS colour.
393 * A CSS colour can be one of the following:
394 * - Hex colour: #AA66BB
395 * - RGB colour: rgb(0-255, 0-255, 0-255)
396 * - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1)
397 * - HSL colour: hsl(0-360, 0-100%, 0-100%)
398 * - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1)
400 * Or a recognised browser colour mapping {@link css_optimiser::$htmlcolours}
402 * @param string $value The colour value to check
403 * @return bool
405 function css_is_colour($value) {
406 $value = trim($value);
408 $hex = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/';
409 $rgb = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i';
410 $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
411 $hsl = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i';
412 $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
414 if (in_array(strtolower($value), array('inherit'))) {
415 return true;
416 } else if (preg_match($hex, $value)) {
417 return true;
418 } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
419 return true;
420 } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
421 // It is an RGB colour.
422 return true;
423 } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
424 // It is an RGBA colour.
425 return true;
426 } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
427 // It is an HSL colour.
428 return true;
429 } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
430 // It is an HSLA colour.
431 return true;
433 // Doesn't look like a colour.
434 return false;
438 * Returns true is the passed value looks like a CSS width.
439 * In order to pass this test the value must be purely numerical or end with a
440 * valid CSS unit term.
442 * @param string|int $value
443 * @return boolean
445 function css_is_width($value) {
446 $value = trim($value);
447 if (in_array(strtolower($value), array('auto', 'inherit'))) {
448 return true;
450 if ((string)$value === '0' || preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) {
451 return true;
453 return false;
457 * A simple sorting function to sort two array values on the number of items they contain
459 * @param array $a
460 * @param array $b
461 * @return int
463 function css_sort_by_count(array $a, array $b) {
464 $a = count($a);
465 $b = count($b);
466 if ($a == $b) {
467 return 0;
469 return ($a > $b) ? -1 : 1;
473 * A basic CSS optimiser that strips out unwanted things and then processes CSS organising and cleaning styles.
475 * This CSS optimiser works by reading through a CSS string one character at a
476 * time and building an object structure of the CSS.
477 * As part of that processing styles are expanded out as much as they can be to
478 * ensure we collect all mappings, at the end of the processing those styles are
479 * then combined into an optimised form to keep them as short as possible.
481 * @package core
482 * @subpackage cssoptimiser
483 * @copyright 2012 Sam Hemelryk
484 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
486 class css_optimiser {
489 * Used when the processor is about to start processing.
490 * Processing states. Used internally.
492 const PROCESSING_START = 0;
495 * Used when the processor is currently processing a selector.
496 * Processing states. Used internally.
498 const PROCESSING_SELECTORS = 0;
501 * Used when the processor is currently processing a style.
502 * Processing states. Used internally.
504 const PROCESSING_STYLES = 1;
507 * Used when the processor is currently processing a comment.
508 * Processing states. Used internally.
510 const PROCESSING_COMMENT = 2;
513 * Used when the processor is currently processing an @ rule.
514 * Processing states. Used internally.
516 const PROCESSING_ATRULE = 3;
519 * The raw string length before optimisation.
520 * Stats variables set during and after processing
521 * @var int
523 protected $rawstrlen = 0;
526 * The number of comments that were removed during optimisation.
527 * Stats variables set during and after processing
528 * @var int
530 protected $commentsincss = 0;
533 * The number of rules in the CSS before optimisation.
534 * Stats variables set during and after processing
535 * @var int
537 protected $rawrules = 0;
540 * The number of selectors using in CSS rules before optimisation.
541 * Stats variables set during and after processing
542 * @var int
544 protected $rawselectors = 0;
547 * The string length after optimisation.
548 * Stats variables set during and after processing
549 * @var int
551 protected $optimisedstrlen = 0;
554 * The number of rules after optimisation.
555 * Stats variables set during and after processing
556 * @var int
558 protected $optimisedrules = 0;
561 * The number of selectors used in rules after optimisation.
562 * Stats variables set during and after processing
563 * @var int
565 protected $optimisedselectors = 0;
568 * The start time of the optimisation.
569 * Stats variables set during and after processing
570 * @var int
572 protected $timestart = 0;
575 * The end time of the optimisation.
576 * Stats variables set during and after processing
577 * @var int
579 protected $timecomplete = 0;
582 * Will be set to any errors that may have occured during processing.
583 * This is updated only at the end of processing NOT during.
585 * @var array
587 protected $errors = array();
590 * Processes incoming CSS optimising it and then returning it.
592 * @param string $css The raw CSS to optimise
593 * @return string The optimised CSS
595 public function process($css) {
596 // Easiest win there is.
597 $css = trim($css);
599 $this->reset_stats();
600 $this->timestart = microtime(true);
601 $this->rawstrlen = strlen($css);
603 // Don't try to process files with no content... it just doesn't make sense.
604 // But we should produce an error for them, an empty CSS file will lead to a
605 // useless request for those running theme designer mode.
606 if ($this->rawstrlen === 0) {
607 $this->errors[] = 'Skipping file as it has no content.';
608 return '';
611 // First up we need to remove all line breaks - this allows us to instantly
612 // reduce our processing requirements and as we will process everything
613 // into a new structure there's really nothing lost.
614 $css = preg_replace('#\r?\n#', ' ', $css);
616 // Next remove the comments... no need to them in an optimised world and
617 // knowing they're all gone allows us to REALLY make our processing simpler.
618 $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
620 $medias = array(
621 'all' => new css_media()
623 $imports = array();
624 $charset = false;
625 // Keyframes are used for CSS animation they will be processed right at the very end.
626 $keyframes = array();
628 $currentprocess = self::PROCESSING_START;
629 $currentrule = css_rule::init();
630 $currentselector = css_selector::init();
631 $inquotes = false; // ' or "
632 $inbraces = false; // {
633 $inbrackets = false; // [
634 $inparenthesis = false; // (
635 /* @var css_media $currentmedia */
636 $currentmedia = $medias['all'];
637 $currentatrule = null;
638 $suspectatrule = false;
640 $buffer = '';
641 $char = null;
643 // Next we are going to iterate over every single character in $css.
644 // This is why we removed line breaks and comments!
645 for ($i = 0; $i < $this->rawstrlen; $i++) {
646 $lastchar = $char;
647 $char = substr($css, $i, 1);
648 if ($char == '@' && $buffer == '') {
649 $suspectatrule = true;
651 switch ($currentprocess) {
652 // Start processing an @ rule e.g. @media, @page, @keyframes.
653 case self::PROCESSING_ATRULE:
654 switch ($char) {
655 case ';':
656 if (!$inbraces) {
657 $buffer .= $char;
658 if ($currentatrule == 'import') {
659 $imports[] = $buffer;
660 $currentprocess = self::PROCESSING_SELECTORS;
661 } else if ($currentatrule == 'charset') {
662 $charset = $buffer;
663 $currentprocess = self::PROCESSING_SELECTORS;
666 if ($currentatrule !== 'media') {
667 $buffer = '';
668 $currentatrule = false;
670 // Continue 1: The switch processing chars
671 // Continue 2: The switch processing the state
672 // Continue 3: The for loop.
673 continue 3;
674 case '{':
675 $regexmediabasic = '#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#';
676 $regexadvmedia = '#\s*@media\s*([^{]+)#';
677 $regexkeyframes = '#@((\-moz\-|\-webkit\-|\-ms\-|\-o\-)?keyframes)\s*([^\s]+)#';
679 if ($currentatrule == 'media' && preg_match($regexmediabasic, $buffer, $matches)) {
680 // Basic media declaration.
681 $mediatypes = str_replace(' ', '', $matches[1]);
682 if (!array_key_exists($mediatypes, $medias)) {
683 $medias[$mediatypes] = new css_media($mediatypes);
685 $currentmedia = $medias[$mediatypes];
686 $currentprocess = self::PROCESSING_SELECTORS;
687 $buffer = '';
688 } else if ($currentatrule == 'media' && preg_match($regexadvmedia, $buffer, $matches)) {
689 // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/.
690 $mediatypes = $matches[1];
691 $hash = md5($mediatypes);
692 $medias[$hash] = new css_media($mediatypes);
693 $currentmedia = $medias[$hash];
694 $currentprocess = self::PROCESSING_SELECTORS;
695 $buffer = '';
696 } else if ($currentatrule == 'keyframes' && preg_match($regexkeyframes, $buffer, $matches)) {
697 // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow
698 // them to be overridden to ensure we don't mess anything up. (means we keep everything in order).
699 $keyframefor = $matches[1];
700 $keyframename = $matches[3];
701 $keyframe = new css_keyframe($keyframefor, $keyframename);
702 $keyframes[] = $keyframe;
703 $currentmedia = $keyframe;
704 $currentprocess = self::PROCESSING_SELECTORS;
705 $buffer = '';
707 // Continue 1: The switch processing chars
708 // Continue 2: The switch processing the state
709 // Continue 3: The for loop.
710 continue 3;
712 break;
713 // Start processing selectors.
714 case self::PROCESSING_START:
715 case self::PROCESSING_SELECTORS:
716 $regexatrule = '#@(media|import|charset|(\-moz\-|\-webkit\-|\-ms\-|\-o\-)?(keyframes))\s*#';
717 switch ($char) {
718 case '[':
719 $inbrackets ++;
720 $buffer .= $char;
721 // Continue 1: The switch processing chars
722 // Continue 2: The switch processing the state
723 // Continue 3: The for loop.
724 continue 3;
725 case ']':
726 $inbrackets --;
727 $buffer .= $char;
728 // Continue 1: The switch processing chars
729 // Continue 2: The switch processing the state
730 // Continue 3: The for loop.
731 continue 3;
732 case ' ':
733 if ($inbrackets) {
734 // Continue 1: The switch processing chars
735 // Continue 2: The switch processing the state
736 // Continue 3: The for loop.
737 continue 3;
739 if (!empty($buffer)) {
740 // Check for known @ rules.
741 if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
742 $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
743 $currentprocess = self::PROCESSING_ATRULE;
744 $buffer .= $char;
745 } else {
746 $currentselector->add($buffer);
747 $buffer = '';
750 $suspectatrule = false;
751 // Continue 1: The switch processing chars
752 // Continue 2: The switch processing the state
753 // Continue 3: The for loop.
754 continue 3;
755 case '{':
756 if ($inbrackets) {
757 // Continue 1: The switch processing chars
758 // Continue 2: The switch processing the state
759 // Continue 3: The for loop.
760 continue 3;
762 // Check for known @ rules.
763 if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
764 // Ahh we've been in an @rule, lets rewind one and have the @rule case process this.
765 $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
766 $currentprocess = self::PROCESSING_ATRULE;
767 $i--;
768 $suspectatrule = false;
769 // Continue 1: The switch processing chars
770 // Continue 2: The switch processing the state
771 // Continue 3: The for loop.
772 continue 3;
774 if ($buffer !== '') {
775 $currentselector->add($buffer);
777 $currentrule->add_selector($currentselector);
778 $currentselector = css_selector::init();
779 $currentprocess = self::PROCESSING_STYLES;
781 $buffer = '';
782 // Continue 1: The switch processing chars
783 // Continue 2: The switch processing the state
784 // Continue 3: The for loop.
785 continue 3;
786 case '}':
787 if ($inbrackets) {
788 // Continue 1: The switch processing chars
789 // Continue 2: The switch processing the state
790 // Continue 3: The for loop.
791 continue 3;
793 if ($currentatrule == 'media') {
794 $currentmedia = $medias['all'];
795 $currentatrule = false;
796 $buffer = '';
797 } else if (strpos($currentatrule, 'keyframes') !== false) {
798 $currentmedia = $medias['all'];
799 $currentatrule = false;
800 $buffer = '';
802 // Continue 1: The switch processing chars
803 // Continue 2: The switch processing the state
804 // Continue 3: The for loop.
805 continue 3;
806 case ',':
807 if ($inbrackets) {
808 // Continue 1: The switch processing chars
809 // Continue 2: The switch processing the state
810 // Continue 3: The for loop.
811 continue 3;
813 $currentselector->add($buffer);
814 $currentrule->add_selector($currentselector);
815 $currentselector = css_selector::init();
816 $buffer = '';
817 // Continue 1: The switch processing chars
818 // Continue 2: The switch processing the state
819 // Continue 3: The for loop.
820 continue 3;
822 break;
823 // Start processing styles.
824 case self::PROCESSING_STYLES:
825 if ($char == '"' || $char == "'") {
826 if ($inquotes === false) {
827 $inquotes = $char;
829 if ($inquotes === $char && $lastchar !== '\\') {
830 $inquotes = false;
833 if ($inquotes) {
834 $buffer .= $char;
835 continue 2;
837 switch ($char) {
838 case ';':
839 if ($inparenthesis) {
840 $buffer .= $char;
841 // Continue 1: The switch processing chars
842 // Continue 2: The switch processing the state
843 // Continue 3: The for loop.
844 continue 3;
846 $currentrule->add_style($buffer);
847 $buffer = '';
848 $inquotes = false;
849 // Continue 1: The switch processing chars
850 // Continue 2: The switch processing the state
851 // Continue 3: The for loop.
852 continue 3;
853 case '}':
854 $currentrule->add_style($buffer);
855 $this->rawselectors += $currentrule->get_selector_count();
857 $currentmedia->add_rule($currentrule);
859 $currentrule = css_rule::init();
860 $currentprocess = self::PROCESSING_SELECTORS;
861 $this->rawrules++;
862 $buffer = '';
863 $inquotes = false;
864 $inparenthesis = false;
865 // Continue 1: The switch processing chars
866 // Continue 2: The switch processing the state
867 // Continue 3: The for loop.
868 continue 3;
869 case '(':
870 $inparenthesis = true;
871 $buffer .= $char;
872 // Continue 1: The switch processing chars
873 // Continue 2: The switch processing the state
874 // Continue 3: The for loop.
875 continue 3;
876 case ')':
877 $inparenthesis = false;
878 $buffer .= $char;
879 // Continue 1: The switch processing chars
880 // Continue 2: The switch processing the state
881 // Continue 3: The for loop.
882 continue 3;
884 break;
886 $buffer .= $char;
889 foreach ($medias as $media) {
890 $this->optimise($media);
892 $css = $this->produce_css($charset, $imports, $medias, $keyframes);
894 $this->timecomplete = microtime(true);
895 return trim($css);
899 * Produces CSS for the given charset, imports, media, and keyframes
900 * @param string $charset
901 * @param array $imports
902 * @param css_media[] $medias
903 * @param css_keyframe[] $keyframes
904 * @return string
906 protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
907 $css = '';
908 if (!empty($charset)) {
909 $imports[] = $charset;
911 if (!empty($imports)) {
912 $css .= implode("\n", $imports);
913 $css .= "\n\n";
916 $cssreset = array();
917 $cssstandard = array();
918 $csskeyframes = array();
920 // Process each media declaration individually.
921 foreach ($medias as $media) {
922 // If this declaration applies to all media types.
923 if (in_array('all', $media->get_types())) {
924 // Collect all rules that represet reset rules and remove them from the media object at the same time.
925 // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they
926 // can't end up out of order because of optimisation.
927 $resetrules = $media->get_reset_rules(true);
928 if (!empty($resetrules)) {
929 $cssreset[] = css_writer::media('all', $resetrules);
932 // Get the standard cSS.
933 $cssstandard[] = $media->out();
936 // Finally if there are any keyframe declarations process them now.
937 if (count($keyframes) > 0) {
938 foreach ($keyframes as $keyframe) {
939 $this->optimisedrules += $keyframe->count_rules();
940 $this->optimisedselectors += $keyframe->count_selectors();
941 if ($keyframe->has_errors()) {
942 $this->errors += $keyframe->get_errors();
944 $csskeyframes[] = $keyframe->out();
948 // Join it all together.
949 $css .= join('', $cssreset);
950 $css .= join('', $cssstandard);
951 $css .= join('', $csskeyframes);
953 // Record the strlenght of the now optimised CSS.
954 $this->optimisedstrlen = strlen($css);
956 // Return the now produced CSS.
957 return $css;
961 * Optimises the CSS rules within a rule collection of one form or another
963 * @param css_rule_collection $media
964 * @return void This function acts in reference
966 protected function optimise(css_rule_collection $media) {
967 $media->organise_rules_by_selectors();
968 $this->optimisedrules += $media->count_rules();
969 $this->optimisedselectors += $media->count_selectors();
970 if ($media->has_errors()) {
971 $this->errors += $media->get_errors();
976 * Returns an array of stats from the last processing run
977 * @return string
979 public function get_stats() {
980 $stats = array(
981 'timestart' => $this->timestart,
982 'timecomplete' => $this->timecomplete,
983 'timetaken' => round($this->timecomplete - $this->timestart, 4),
984 'commentsincss' => $this->commentsincss,
985 'rawstrlen' => $this->rawstrlen,
986 'rawselectors' => $this->rawselectors,
987 'rawrules' => $this->rawrules,
988 'optimisedstrlen' => $this->optimisedstrlen,
989 'optimisedrules' => $this->optimisedrules,
990 'optimisedselectors' => $this->optimisedselectors,
991 'improvementstrlen' => '-',
992 'improvementrules' => '-',
993 'improvementselectors' => '-',
995 // Avoid division by 0 errors by checking we have valid raw values.
996 if ($this->rawstrlen > 0) {
997 $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%';
999 if ($this->rawrules > 0) {
1000 $stats['improvementrules'] = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%';
1002 if ($this->rawselectors > 0) {
1003 $stats['improvementselectors'] = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%';
1005 return $stats;
1009 * Returns true if any errors have occured during processing
1011 * @return bool
1013 public function has_errors() {
1014 return !empty($this->errors);
1018 * Returns an array of errors that have occured
1020 * @param bool $clear If set to true the errors will be cleared after being returned.
1021 * @return array
1023 public function get_errors($clear = false) {
1024 $errors = $this->errors;
1025 if ($clear) {
1026 // Reset the error array.
1027 $this->errors = array();
1029 return $errors;
1033 * Returns any errors as a string that can be included in CSS.
1035 * @return string
1037 public function output_errors_css() {
1038 $computedcss = "/****************************************\n";
1039 $computedcss .= " *--- Errors found during processing ----\n";
1040 foreach ($this->errors as $error) {
1041 $computedcss .= preg_replace('#^#m', '* ', $error);
1043 $computedcss .= " ****************************************/\n\n";
1044 return $computedcss;
1048 * Returns a string to display stats about the last generation within CSS output
1050 * @return string
1052 public function output_stats_css() {
1054 $computedcss = "/****************************************\n";
1055 $computedcss .= " *------- CSS Optimisation stats --------\n";
1057 if ($this->rawstrlen === 0) {
1058 $computedcss .= " File not processed as it has no content /\n\n";
1059 $computedcss .= " ****************************************/\n\n";
1060 return $computedcss;
1061 } else if ($this->rawrules === 0) {
1062 $computedcss .= " File contained no rules to be processed /\n\n";
1063 $computedcss .= " ****************************************/\n\n";
1064 return $computedcss;
1067 $stats = $this->get_stats();
1069 $computedcss .= " * ".date('r')."\n";
1070 $computedcss .= " * {$stats['commentsincss']} \t comments removed\n";
1071 $computedcss .= " * Optimisation took {$stats['timetaken']} seconds\n";
1072 $computedcss .= " *--------------- before ----------------\n";
1073 $computedcss .= " * {$stats['rawstrlen']} \t chars read in\n";
1074 $computedcss .= " * {$stats['rawrules']} \t rules read in\n";
1075 $computedcss .= " * {$stats['rawselectors']} \t total selectors\n";
1076 $computedcss .= " *---------------- after ----------------\n";
1077 $computedcss .= " * {$stats['optimisedstrlen']} \t chars once optimized\n";
1078 $computedcss .= " * {$stats['optimisedrules']} \t optimized rules\n";
1079 $computedcss .= " * {$stats['optimisedselectors']} \t total selectors once optimized\n";
1080 $computedcss .= " *---------------- stats ----------------\n";
1081 $computedcss .= " * {$stats['improvementstrlen']} \t reduction in chars\n";
1082 $computedcss .= " * {$stats['improvementrules']} \t reduction in rules\n";
1083 $computedcss .= " * {$stats['improvementselectors']} \t reduction in selectors\n";
1084 $computedcss .= " ****************************************/\n\n";
1086 return $computedcss;
1090 * Resets the stats ready for another fresh processing
1092 public function reset_stats() {
1093 $this->commentsincss = 0;
1094 $this->optimisedrules = 0;
1095 $this->optimisedselectors = 0;
1096 $this->optimisedstrlen = 0;
1097 $this->rawrules = 0;
1098 $this->rawselectors = 0;
1099 $this->rawstrlen = 0;
1100 $this->timecomplete = 0;
1101 $this->timestart = 0;
1105 * An array of the common HTML colours that are supported by most browsers.
1107 * This reference table is used to allow us to unify colours, and will aid
1108 * us in identifying buggy CSS using unsupported colours.
1110 * @var string[]
1112 public static $htmlcolours = array(
1113 'aliceblue' => '#F0F8FF',
1114 'antiquewhite' => '#FAEBD7',
1115 'aqua' => '#00FFFF',
1116 'aquamarine' => '#7FFFD4',
1117 'azure' => '#F0FFFF',
1118 'beige' => '#F5F5DC',
1119 'bisque' => '#FFE4C4',
1120 'black' => '#000000',
1121 'blanchedalmond' => '#FFEBCD',
1122 'blue' => '#0000FF',
1123 'blueviolet' => '#8A2BE2',
1124 'brown' => '#A52A2A',
1125 'burlywood' => '#DEB887',
1126 'cadetblue' => '#5F9EA0',
1127 'chartreuse' => '#7FFF00',
1128 'chocolate' => '#D2691E',
1129 'coral' => '#FF7F50',
1130 'cornflowerblue' => '#6495ED',
1131 'cornsilk' => '#FFF8DC',
1132 'crimson' => '#DC143C',
1133 'cyan' => '#00FFFF',
1134 'darkblue' => '#00008B',
1135 'darkcyan' => '#008B8B',
1136 'darkgoldenrod' => '#B8860B',
1137 'darkgray' => '#A9A9A9',
1138 'darkgrey' => '#A9A9A9',
1139 'darkgreen' => '#006400',
1140 'darkKhaki' => '#BDB76B',
1141 'darkmagenta' => '#8B008B',
1142 'darkolivegreen' => '#556B2F',
1143 'arkorange' => '#FF8C00',
1144 'darkorchid' => '#9932CC',
1145 'darkred' => '#8B0000',
1146 'darksalmon' => '#E9967A',
1147 'darkseagreen' => '#8FBC8F',
1148 'darkslateblue' => '#483D8B',
1149 'darkslategray' => '#2F4F4F',
1150 'darkslategrey' => '#2F4F4F',
1151 'darkturquoise' => '#00CED1',
1152 'darkviolet' => '#9400D3',
1153 'deeppink' => '#FF1493',
1154 'deepskyblue' => '#00BFFF',
1155 'dimgray' => '#696969',
1156 'dimgrey' => '#696969',
1157 'dodgerblue' => '#1E90FF',
1158 'firebrick' => '#B22222',
1159 'floralwhite' => '#FFFAF0',
1160 'forestgreen' => '#228B22',
1161 'fuchsia' => '#FF00FF',
1162 'gainsboro' => '#DCDCDC',
1163 'ghostwhite' => '#F8F8FF',
1164 'gold' => '#FFD700',
1165 'goldenrod' => '#DAA520',
1166 'gray' => '#808080',
1167 'grey' => '#808080',
1168 'green' => '#008000',
1169 'greenyellow' => '#ADFF2F',
1170 'honeydew' => '#F0FFF0',
1171 'hotpink' => '#FF69B4',
1172 'indianred ' => '#CD5C5C',
1173 'indigo ' => '#4B0082',
1174 'ivory' => '#FFFFF0',
1175 'khaki' => '#F0E68C',
1176 'lavender' => '#E6E6FA',
1177 'lavenderblush' => '#FFF0F5',
1178 'lawngreen' => '#7CFC00',
1179 'lemonchiffon' => '#FFFACD',
1180 'lightblue' => '#ADD8E6',
1181 'lightcoral' => '#F08080',
1182 'lightcyan' => '#E0FFFF',
1183 'lightgoldenrodyellow' => '#FAFAD2',
1184 'lightgray' => '#D3D3D3',
1185 'lightgrey' => '#D3D3D3',
1186 'lightgreen' => '#90EE90',
1187 'lightpink' => '#FFB6C1',
1188 'lightsalmon' => '#FFA07A',
1189 'lightseagreen' => '#20B2AA',
1190 'lightskyblue' => '#87CEFA',
1191 'lightslategray' => '#778899',
1192 'lightslategrey' => '#778899',
1193 'lightsteelblue' => '#B0C4DE',
1194 'lightyellow' => '#FFFFE0',
1195 'lime' => '#00FF00',
1196 'limegreen' => '#32CD32',
1197 'linen' => '#FAF0E6',
1198 'magenta' => '#FF00FF',
1199 'maroon' => '#800000',
1200 'mediumaquamarine' => '#66CDAA',
1201 'mediumblue' => '#0000CD',
1202 'mediumorchid' => '#BA55D3',
1203 'mediumpurple' => '#9370D8',
1204 'mediumseagreen' => '#3CB371',
1205 'mediumslateblue' => '#7B68EE',
1206 'mediumspringgreen' => '#00FA9A',
1207 'mediumturquoise' => '#48D1CC',
1208 'mediumvioletred' => '#C71585',
1209 'midnightblue' => '#191970',
1210 'mintcream' => '#F5FFFA',
1211 'mistyrose' => '#FFE4E1',
1212 'moccasin' => '#FFE4B5',
1213 'navajowhite' => '#FFDEAD',
1214 'navy' => '#000080',
1215 'oldlace' => '#FDF5E6',
1216 'olive' => '#808000',
1217 'olivedrab' => '#6B8E23',
1218 'orange' => '#FFA500',
1219 'orangered' => '#FF4500',
1220 'orchid' => '#DA70D6',
1221 'palegoldenrod' => '#EEE8AA',
1222 'palegreen' => '#98FB98',
1223 'paleturquoise' => '#AFEEEE',
1224 'palevioletred' => '#D87093',
1225 'papayawhip' => '#FFEFD5',
1226 'peachpuff' => '#FFDAB9',
1227 'peru' => '#CD853F',
1228 'pink' => '#FFC0CB',
1229 'plum' => '#DDA0DD',
1230 'powderblue' => '#B0E0E6',
1231 'purple' => '#800080',
1232 'red' => '#FF0000',
1233 'rosybrown' => '#BC8F8F',
1234 'royalblue' => '#4169E1',
1235 'saddlebrown' => '#8B4513',
1236 'salmon' => '#FA8072',
1237 'sandybrown' => '#F4A460',
1238 'seagreen' => '#2E8B57',
1239 'seashell' => '#FFF5EE',
1240 'sienna' => '#A0522D',
1241 'silver' => '#C0C0C0',
1242 'skyblue' => '#87CEEB',
1243 'slateblue' => '#6A5ACD',
1244 'slategray' => '#708090',
1245 'slategrey' => '#708090',
1246 'snow' => '#FFFAFA',
1247 'springgreen' => '#00FF7F',
1248 'steelblue' => '#4682B4',
1249 'tan' => '#D2B48C',
1250 'teal' => '#008080',
1251 'thistle' => '#D8BFD8',
1252 'tomato' => '#FF6347',
1253 'transparent' => 'transparent',
1254 'turquoise' => '#40E0D0',
1255 'violet' => '#EE82EE',
1256 'wheat' => '#F5DEB3',
1257 'white' => '#FFFFFF',
1258 'whitesmoke' => '#F5F5F5',
1259 'yellow' => '#FFFF00',
1260 'yellowgreen' => '#9ACD32'
1265 * Used to prepare CSS strings
1267 * @package core
1268 * @subpackage cssoptimiser
1269 * @copyright 2012 Sam Hemelryk
1270 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1272 abstract class css_writer {
1275 * The current indent level
1276 * @var int
1278 protected static $indent = 0;
1281 * Returns true if the output should still maintain minimum formatting.
1282 * @return bool
1284 protected static function is_pretty() {
1285 global $CFG;
1286 return (!empty($CFG->cssoptimiserpretty));
1290 * Returns the indenting char to use for indenting things nicely.
1291 * @return string
1293 protected static function get_indent() {
1294 if (self::is_pretty()) {
1295 return str_repeat(" ", self::$indent);
1297 return '';
1301 * Increases the current indent
1303 protected static function increase_indent() {
1304 self::$indent++;
1308 * Decreases the current indent
1310 protected static function decrease_indent() {
1311 self::$indent--;
1315 * Returns the string to use as a separator
1316 * @return string
1318 protected static function get_separator() {
1319 return (self::is_pretty())?"\n":' ';
1323 * Returns CSS for media
1325 * @param string $typestring
1326 * @param css_rule[] $rules An array of css_rule objects
1327 * @return string
1329 public static function media($typestring, array &$rules) {
1330 $nl = self::get_separator();
1332 $output = '';
1333 if ($typestring !== 'all') {
1334 $output .= "\n@media {$typestring} {".$nl;
1335 self::increase_indent();
1337 foreach ($rules as $rule) {
1338 $output .= $rule->out().$nl;
1340 if ($typestring !== 'all') {
1341 self::decrease_indent();
1342 $output .= '}';
1344 return $output;
1348 * Returns CSS for a keyframe
1350 * @param string $for The desired declaration. e.g. keyframes, -moz-keyframes, -webkit-keyframes
1351 * @param string $name The name for the keyframe
1352 * @param css_rule[] $rules An array of rules belonging to the keyframe
1353 * @return string
1355 public static function keyframe($for, $name, array &$rules) {
1356 $output = "\n@{$for} {$name} {";
1357 foreach ($rules as $rule) {
1358 $output .= $rule->out();
1360 $output .= '}';
1361 return $output;
1365 * Returns CSS for a rule
1367 * @param string $selector
1368 * @param string $styles
1369 * @return string
1371 public static function rule($selector, $styles) {
1372 $css = self::get_indent()."{$selector}{{$styles}}";
1373 return $css;
1377 * Returns CSS for the selectors of a rule
1379 * @param css_selector[] $selectors Array of css_selector objects
1380 * @return string
1382 public static function selectors(array $selectors) {
1383 $nl = self::get_separator();
1384 $selectorstrings = array();
1385 foreach ($selectors as $selector) {
1386 $selectorstrings[] = $selector->out();
1388 return join(','.$nl, $selectorstrings);
1392 * Returns a selector given the components that make it up.
1394 * @param array $components
1395 * @return string
1397 public static function selector(array $components) {
1398 return trim(join(' ', $components));
1402 * Returns a CSS string for the provided styles
1404 * @param css_style[] $styles Array of css_style objects
1405 * @return string
1407 public static function styles(array $styles) {
1408 $bits = array();
1409 foreach ($styles as $style) {
1410 // Check if the style is an array. If it is then we are outputing an advanced style.
1411 // An advanced style is a style with one or more values, and can occur in situations like background-image
1412 // where browse specific values are being used.
1413 if (is_array($style)) {
1414 /* @var css_style[] $style */
1415 foreach ($style as $advstyle) {
1416 $bits[] = $advstyle->out();
1418 continue;
1420 $bits[] = $style->out();
1422 return join('', $bits);
1426 * Returns a style CSS
1428 * @param string $name
1429 * @param string $value
1430 * @param bool $important
1431 * @return string
1433 public static function style($name, $value, $important = false) {
1434 $value = trim($value);
1435 if ($important && strpos($value, '!important') === false) {
1436 $value .= ' !important';
1438 return "{$name}:{$value};";
1443 * A consolidatable style interface.
1445 * Class that implement this have a short-hand notation for specifying multiple styles.
1447 * @package core
1448 * @subpackage cssoptimiser
1449 * @copyright 2012 Sam Hemelryk
1450 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1452 interface core_css_consolidatable_style {
1454 * Used to consolidate several styles into a single "short-hand" style.
1455 * @param array $styles
1456 * @return mixed
1458 public static function consolidate(array $styles);
1462 * A structure to represent a CSS selector.
1464 * The selector is the classes, id, elements, and psuedo bits that make up a CSS
1465 * rule.
1467 * @package core
1468 * @subpackage cssoptimiser
1469 * @copyright 2012 Sam Hemelryk
1470 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1472 class css_selector {
1475 * An array of selector bits
1476 * @var array
1478 protected $selectors = array();
1481 * The number of selectors.
1482 * @var int
1484 protected $count = 0;
1487 * Is null if there are no selectors, true if all selectors are basic and false otherwise.
1488 * A basic selector is one that consists of just the element type. e.g. div, span, td, a
1489 * @var bool|null
1491 protected $isbasic = null;
1494 * Initialises a new CSS selector
1495 * @return css_selector
1497 public static function init() {
1498 return new css_selector();
1502 * CSS selectors can only be created through the init method above.
1504 protected function __construct() {
1505 // Nothing to do here by default.
1509 * Adds a selector to the end of the current selector
1510 * @param string $selector
1512 public function add($selector) {
1513 $selector = trim($selector);
1514 $count = 0;
1515 $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
1516 if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
1517 $count ++;
1519 // If its already false then no need to continue, its not basic.
1520 if ($this->isbasic !== false) {
1521 // If theres more than one part making up this selector its not basic.
1522 if ($count > 1) {
1523 $this->isbasic = false;
1524 } else {
1525 // Check whether it is a basic element (a-z+) with possible psuedo selector.
1526 $this->isbasic = (bool)preg_match('#^[a-z]+(:[a-zA-Z]+)?$#', $selector);
1529 $this->count = $count;
1530 $this->selectors[] = $selector;
1533 * Returns the number of individual components that make up this selector
1534 * @return int
1536 public function get_selector_count() {
1537 return $this->count;
1541 * Returns the selector for use in a CSS rule
1542 * @return string
1544 public function out() {
1545 return css_writer::selector($this->selectors);
1549 * Returns true is all of the selectors act only upon basic elements (no classes/ids)
1550 * @return bool
1552 public function is_basic() {
1553 return ($this->isbasic === true);
1558 * A structure to represent a CSS rule.
1560 * @package core
1561 * @subpackage cssoptimiser
1562 * @copyright 2012 Sam Hemelryk
1563 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1565 class css_rule {
1568 * An array of CSS selectors {@link css_selector}
1569 * @var css_selector[]
1571 protected $selectors = array();
1574 * An array of CSS styles {@link css_style}
1575 * @var css_style[]
1577 protected $styles = array();
1580 * Created a new CSS rule. This is the only way to create a new CSS rule externally.
1581 * @return css_rule
1583 public static function init() {
1584 return new css_rule();
1588 * Constructs a new css rule.
1590 * @param string $selector The selector or array of selectors that make up this rule.
1591 * @param css_style[] $styles An array of styles that belong to this rule.
1593 protected function __construct($selector = null, array $styles = array()) {
1594 if ($selector != null) {
1595 if (is_array($selector)) {
1596 $this->selectors = $selector;
1597 } else {
1598 $this->selectors = array($selector);
1600 $this->add_styles($styles);
1605 * Adds a new CSS selector to this rule
1607 * e.g. $rule->add_selector('.one #two.two');
1609 * @param css_selector $selector Adds a CSS selector to this rule.
1611 public function add_selector(css_selector $selector) {
1612 $this->selectors[] = $selector;
1616 * Adds a new CSS style to this rule.
1618 * @param css_style|string $style Adds a new style to this rule
1620 public function add_style($style) {
1621 if (is_string($style)) {
1622 $style = trim($style);
1623 if (empty($style)) {
1624 return;
1626 $bits = explode(':', $style, 2);
1627 if (count($bits) == 2) {
1628 list($name, $value) = array_map('trim', $bits);
1630 if (isset($name) && isset($value) && $name !== '' && $value !== '') {
1631 $style = css_style::init_automatic($name, $value);
1633 } else if ($style instanceof css_style) {
1634 // Clone the style as it may be coming from another rule and we don't
1635 // want references as it will likely be overwritten by proceeding
1636 // rules.
1637 $style = clone($style);
1639 if ($style instanceof css_style) {
1640 $name = $style->get_name();
1641 $exists = array_key_exists($name, $this->styles);
1642 // We need to find out if the current style support multiple values, or whether the style
1643 // is already set up to record multiple values. This can happen with background images which can have single
1644 // and multiple values.
1645 if ($style->allows_multiple_values() || ($exists && is_array($this->styles[$name]))) {
1646 if (!$exists) {
1647 $this->styles[$name] = array();
1648 } else if ($this->styles[$name] instanceof css_style) {
1649 $this->styles[$name] = array($this->styles[$name]);
1651 $this->styles[$name][] = $style;
1652 } else if ($exists) {
1653 $this->styles[$name]->set_value($style->get_value());
1654 } else {
1655 $this->styles[$name] = $style;
1657 } else if (is_array($style)) {
1658 // We probably shouldn't worry about processing styles here but to
1659 // be truthful it doesn't hurt.
1660 foreach ($style as $astyle) {
1661 $this->add_style($astyle);
1667 * An easy method of adding several styles at once. Just calls add_style.
1669 * This method simply iterates over the array and calls {@link css_rule::add_style()}
1670 * with each.
1672 * @param css_style[] $styles Adds an array of styles
1674 public function add_styles(array $styles) {
1675 foreach ($styles as $style) {
1676 $this->add_style($style);
1681 * Returns the array of selectors
1683 * @return css_selector[]
1685 public function get_selectors() {
1686 return $this->selectors;
1690 * Returns the array of styles
1692 * @return css_style[]
1694 public function get_styles() {
1695 return $this->styles;
1699 * Outputs this rule as a fragment of CSS
1701 * @return string
1703 public function out() {
1704 $selectors = css_writer::selectors($this->selectors);
1705 $styles = css_writer::styles($this->get_consolidated_styles());
1706 return css_writer::rule($selectors, $styles);
1710 * Consolidates all styles associated with this rule
1712 * @return css_style[] An array of consolidated styles
1714 public function get_consolidated_styles() {
1715 /* @var css_style[] $organisedstyles */
1716 $organisedstyles = array();
1717 /* @var css_style[] $finalstyles */
1718 $finalstyles = array();
1719 /* @var core_css_consolidatable_style[] $consolidate */
1720 $consolidate = array();
1721 /* @var css_style[] $advancedstyles */
1722 $advancedstyles = array();
1723 foreach ($this->styles as $style) {
1724 // If the style is an array then we are processing an advanced style. An advanced style is a style that can have
1725 // one or more values. Background-image is one such example as it can have browser specific styles.
1726 if (is_array($style)) {
1727 $single = null;
1728 $count = 0;
1729 foreach ($style as $advstyle) {
1730 /* @var css_style $advstyle */
1731 $key = $count++;
1732 $advancedstyles[$key] = $advstyle;
1733 if (!$advstyle->allows_multiple_values()) {
1734 if (!is_null($single)) {
1735 unset($advancedstyles[$single]);
1737 $single = $key;
1740 if (!is_null($single)) {
1741 $style = $advancedstyles[$single];
1743 $consolidatetoclass = $style->consolidate_to();
1744 if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1745 class_exists('css_style_'.$consolidatetoclass)) {
1746 $class = 'css_style_'.$consolidatetoclass;
1747 if (!array_key_exists($class, $consolidate)) {
1748 $consolidate[$class] = array();
1749 $organisedstyles[$class] = true;
1751 $consolidate[$class][] = $style;
1752 unset($advancedstyles[$single]);
1756 continue;
1758 $consolidatetoclass = $style->consolidate_to();
1759 if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1760 class_exists('css_style_'.$consolidatetoclass)) {
1761 $class = 'css_style_'.$consolidatetoclass;
1762 if (!array_key_exists($class, $consolidate)) {
1763 $consolidate[$class] = array();
1764 $organisedstyles[$class] = true;
1766 $consolidate[$class][] = $style;
1767 } else {
1768 $organisedstyles[$style->get_name()] = $style;
1772 foreach ($consolidate as $class => $styles) {
1773 $organisedstyles[$class] = call_user_func(array($class, 'consolidate'), $styles);
1776 foreach ($organisedstyles as $style) {
1777 if (is_array($style)) {
1778 foreach ($style as $s) {
1779 $finalstyles[] = $s;
1781 } else {
1782 $finalstyles[] = $style;
1785 $finalstyles = array_merge($finalstyles, $advancedstyles);
1786 return $finalstyles;
1790 * Splits this rules into an array of CSS rules. One for each of the selectors
1791 * that make up this rule.
1793 * @return css_rule[]
1795 public function split_by_selector() {
1796 $return = array();
1797 foreach ($this->selectors as $selector) {
1798 $return[] = new css_rule($selector, $this->styles);
1800 return $return;
1804 * Splits this rule into an array of rules. One for each of the styles that
1805 * make up this rule
1807 * @return css_rule[] Array of css_rule objects
1809 public function split_by_style() {
1810 $return = array();
1811 foreach ($this->styles as $style) {
1812 if (is_array($style)) {
1813 $return[] = new css_rule($this->selectors, $style);
1814 continue;
1816 $return[] = new css_rule($this->selectors, array($style));
1818 return $return;
1822 * Gets a hash for the styles of this rule
1824 * @return string
1826 public function get_style_hash() {
1827 return md5(css_writer::styles($this->styles));
1831 * Gets a hash for the selectors of this rule
1833 * @return string
1835 public function get_selector_hash() {
1836 return md5(css_writer::selectors($this->selectors));
1840 * Gets the number of selectors that make up this rule.
1842 * @return int
1844 public function get_selector_count() {
1845 $count = 0;
1846 foreach ($this->selectors as $selector) {
1847 $count += $selector->get_selector_count();
1849 return $count;
1853 * Returns true if there are any errors with this rule.
1855 * @return bool
1857 public function has_errors() {
1858 foreach ($this->styles as $style) {
1859 if (is_array($style)) {
1860 /* @var css_style[] $style */
1861 foreach ($style as $advstyle) {
1862 if ($advstyle->has_error()) {
1863 return true;
1866 continue;
1868 if ($style->has_error()) {
1869 return true;
1872 return false;
1876 * Returns the error strings that were recorded when processing this rule.
1878 * Before calling this function you should first call {@link css_rule::has_errors()}
1879 * to make sure there are errors (hopefully there arn't).
1881 * @return string
1883 public function get_error_string() {
1884 $css = $this->out();
1885 $errors = array();
1886 foreach ($this->styles as $style) {
1887 if (is_array($style)) {
1888 /* @var css_style[] $style */
1889 foreach ($style as $advstyle) {
1890 if ($advstyle instanceof css_style && $advstyle->has_error()) {
1891 $errors[] = " * ".$advstyle->get_last_error();
1894 } else if ($style instanceof css_style && $style->has_error()) {
1895 $errors[] = " * ".$style->get_last_error();
1898 return $css." has the following errors:\n".join("\n", $errors);
1902 * Returns true if this rule could be considered a reset rule.
1904 * A reset rule is a rule that acts upon an HTML element and does not include any other parts to its selector.
1906 * @return bool
1908 public function is_reset_rule() {
1909 foreach ($this->selectors as $selector) {
1910 if (!$selector->is_basic()) {
1911 return false;
1914 return true;
1919 * An abstract CSS rule collection class.
1921 * This class is extended by things such as media and keyframe declaration. They are declarations that
1922 * group rules together for a purpose.
1923 * When no declaration is specified rules accumulate into @media all.
1925 * @package core
1926 * @subpackage cssoptimiser
1927 * @copyright 2012 Sam Hemelryk
1928 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1930 abstract class css_rule_collection {
1932 * An array of rules within this collection instance
1933 * @var css_rule[]
1935 protected $rules = array();
1938 * The collection must be able to print itself.
1940 abstract public function out();
1943 * Adds a new CSS rule to this collection instance
1945 * @param css_rule $newrule
1947 public function add_rule(css_rule $newrule) {
1948 foreach ($newrule->split_by_selector() as $rule) {
1949 $hash = $rule->get_selector_hash();
1950 if (!array_key_exists($hash, $this->rules)) {
1951 $this->rules[$hash] = $rule;
1952 } else {
1953 $this->rules[$hash]->add_styles($rule->get_styles());
1959 * Returns the rules used by this collection
1961 * @return css_rule[]
1963 public function get_rules() {
1964 return $this->rules;
1968 * Organises rules by gropuing selectors based upon the styles and consolidating
1969 * those selectors into single rules.
1971 * @return bool True if the CSS was optimised by this method
1973 public function organise_rules_by_selectors() {
1974 /* @var css_rule[] $optimisedrules */
1975 $optimisedrules = array();
1976 $beforecount = count($this->rules);
1977 $lasthash = null;
1978 /* @var css_rule $lastrule */
1979 $lastrule = null;
1980 foreach ($this->rules as $rule) {
1981 $hash = $rule->get_style_hash();
1982 if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) {
1983 foreach ($rule->get_selectors() as $selector) {
1984 $lastrule->add_selector($selector);
1986 continue;
1988 $lastrule = clone($rule);
1989 $lasthash = $hash;
1990 $optimisedrules[] = $lastrule;
1992 $this->rules = array();
1993 foreach ($optimisedrules as $optimised) {
1994 $this->rules[$optimised->get_selector_hash()] = $optimised;
1996 $aftercount = count($this->rules);
1997 return ($beforecount < $aftercount);
2001 * Returns the total number of rules that exist within this collection
2003 * @return int
2005 public function count_rules() {
2006 return count($this->rules);
2010 * Returns the total number of selectors that exist within this collection
2012 * @return int
2014 public function count_selectors() {
2015 $count = 0;
2016 foreach ($this->rules as $rule) {
2017 $count += $rule->get_selector_count();
2019 return $count;
2023 * Returns true if the collection has any rules that have errors
2025 * @return boolean
2027 public function has_errors() {
2028 foreach ($this->rules as $rule) {
2029 if ($rule->has_errors()) {
2030 return true;
2033 return false;
2037 * Returns any errors that have happened within rules in this collection.
2039 * @return string[]
2041 public function get_errors() {
2042 $errors = array();
2043 foreach ($this->rules as $rule) {
2044 if ($rule->has_errors()) {
2045 $errors[] = $rule->get_error_string();
2048 return $errors;
2053 * A media class to organise rules by the media they apply to.
2055 * @package core
2056 * @subpackage cssoptimiser
2057 * @copyright 2012 Sam Hemelryk
2058 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2060 class css_media extends css_rule_collection {
2063 * An array of the different media types this instance applies to.
2064 * @var array
2066 protected $types = array();
2069 * Initalises a new media instance
2071 * @param string $for The media that the contained rules are destined for.
2073 public function __construct($for = 'all') {
2074 $types = explode(',', $for);
2075 $this->types = array_map('trim', $types);
2079 * Returns the CSS for this media and all of its rules.
2081 * @return string
2083 public function out() {
2084 return css_writer::media(join(',', $this->types), $this->rules);
2088 * Returns an array of media that this media instance applies to
2090 * @return array
2092 public function get_types() {
2093 return $this->types;
2097 * Returns all of the reset rules known by this media set.
2098 * @param bool $remove If set to true reset rules will be removed before being returned.
2099 * @return array
2101 public function get_reset_rules($remove = false) {
2102 $resetrules = array();
2103 foreach ($this->rules as $key => $rule) {
2104 if ($rule->is_reset_rule()) {
2105 $resetrules[] = clone $rule;
2106 if ($remove) {
2107 unset($this->rules[$key]);
2111 return $resetrules;
2116 * A media class to organise rules by the media they apply to.
2118 * @package core
2119 * @subpackage cssoptimiser
2120 * @copyright 2012 Sam Hemelryk
2121 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2123 class css_keyframe extends css_rule_collection {
2126 * The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2127 * @var string
2129 protected $for;
2132 * The name for the keyframes
2133 * @var string
2135 protected $name;
2137 * Constructs a new keyframe
2139 * @param string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2140 * @param string $name The name for the keyframes
2142 public function __construct($for, $name) {
2143 $this->for = $for;
2144 $this->name = $name;
2147 * Returns the directive of this keyframe
2149 * e.g. keyframes, -moz-keyframes, -webkit-keyframes
2150 * @return string
2152 public function get_for() {
2153 return $this->for;
2156 * Returns the name of this keyframe
2157 * @return string
2159 public function get_name() {
2160 return $this->name;
2163 * Returns the CSS for this collection of keyframes and all of its rules.
2165 * @return string
2167 public function out() {
2168 return css_writer::keyframe($this->for, $this->name, $this->rules);
2173 * An absract class to represent CSS styles
2175 * @package core
2176 * @subpackage cssoptimiser
2177 * @copyright 2012 Sam Hemelryk
2178 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2180 abstract class css_style {
2182 /** Constant used for recongise a special empty value in a CSS style */
2183 const NULL_VALUE = '@@$NULL$@@';
2186 * The name of the style
2187 * @var string
2189 protected $name;
2192 * The value for the style
2193 * @var mixed
2195 protected $value;
2198 * If set to true this style was defined with the !important rule.
2199 * Only trolls use !important.
2200 * Don't hide under bridges.. its not good for your skin. Do the proper thing
2201 * and fix the issue don't just force a fix that will undoubtedly one day
2202 * lead to further frustration.
2203 * @var bool
2205 protected $important = false;
2208 * Gets set to true if this style has an error
2209 * @var bool
2211 protected $error = false;
2214 * The last error message that occured
2215 * @var string
2217 protected $errormessage = null;
2220 * Initialises a new style.
2222 * This is the only public way to create a style to ensure they that appropriate
2223 * style class is used if it exists.
2225 * @param string $name The name of the style.
2226 * @param string $value The value of the style.
2227 * @return css_style_generic
2229 public static function init_automatic($name, $value) {
2230 $cleanedname = preg_replace('#[^a-zA-Z0-9]+#', '', $name);
2231 $specificclass = 'css_style_'.$cleanedname;
2232 if (class_exists($specificclass)) {
2233 $style = call_user_func(array($specificclass, 'init'), $value);
2234 if ($cleanedname !== $name && !is_array($style)) {
2235 $style->set_actual_name($name);
2237 return $style;
2239 return new css_style_generic($name, $value);
2243 * Creates a new style when given its name and value
2245 * @param string $name The name of the style.
2246 * @param string $value The value of the style.
2248 protected function __construct($name, $value) {
2249 $this->name = $name;
2250 $this->set_value($value);
2254 * Sets the value for the style
2256 * @param string $value
2258 final public function set_value($value) {
2259 $value = trim($value);
2260 $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
2261 if ($important) {
2262 $value = substr($value, 0, -(strlen($matches[1])));
2263 $value = rtrim($value);
2265 if (!$this->important || $important) {
2266 $this->value = $this->clean_value($value);
2267 $this->important = $important;
2269 if (!$this->is_valid()) {
2270 $this->set_error('Invalid value for '.$this->name);
2275 * Returns true if the value associated with this style is valid
2277 * @return bool
2279 public function is_valid() {
2280 return true;
2284 * Returns the name for the style
2286 * @return string
2288 public function get_name() {
2289 return $this->name;
2293 * Returns the value for the style
2295 * @param bool $includeimportant If set to true and the rule is important !important postfix will be used.
2296 * @return string
2298 public function get_value($includeimportant = true) {
2299 $value = $this->value;
2300 if ($includeimportant && $this->important) {
2301 $value .= ' !important';
2303 return $value;
2307 * Returns the style ready for use in CSS
2309 * @param string|null $value A value to use to override the value for this style.
2310 * @return string
2312 public function out($value = null) {
2313 if (is_null($value)) {
2314 $value = $this->get_value();
2316 return css_writer::style($this->name, $value, $this->important);
2320 * This can be overridden by a specific style allowing it to clean its values
2321 * consistently.
2323 * @param mixed $value
2324 * @return mixed
2326 protected function clean_value($value) {
2327 return $value;
2331 * If this particular style can be consolidated into another style this function
2332 * should return the style that it can be consolidated into.
2334 * @return string|null
2336 public function consolidate_to() {
2337 return null;
2341 * Sets the last error message.
2343 * @param string $message
2345 protected function set_error($message) {
2346 $this->error = true;
2347 $this->errormessage = $message;
2351 * Returns true if an error has occured
2353 * @return bool
2355 public function has_error() {
2356 return $this->error;
2360 * Returns the last error that occured or null if no errors have happened.
2362 * @return string
2364 public function get_last_error() {
2365 return $this->errormessage;
2369 * Returns true if the value for this style is the special null value.
2371 * This should only be overriden in circumstances where a shorthand style can lead
2372 * to move explicit styles being overwritten. Not a common place occurenace.
2374 * Example:
2375 * This occurs if the shorthand background property was used but no proper value
2376 * was specified for this style.
2377 * This leads to a null value being used unless otherwise overridden.
2379 * @return bool
2381 public function is_special_empty_value() {
2382 return false;
2386 * Returns true if this style permits multiple values.
2388 * This occurs for styles such as background image that can have browser specific values that need to be maintained because
2389 * of course we don't know what browser the user is using, and optimisation occurs before caching.
2390 * Thus we must always server all values we encounter in the order we encounter them for when this is set to true.
2392 * @return boolean False by default, true if the style supports muliple values.
2394 public function allows_multiple_values() {
2395 return false;
2399 * Returns true if this style was marked important.
2400 * @return bool
2402 public function is_important() {
2403 return !empty($this->important);
2407 * Sets the important flag for this style and its current value.
2408 * @param bool $important
2410 public function set_important($important = true) {
2411 $this->important = (bool) $important;
2415 * Sets the actual name used within the style.
2417 * This method allows us to support browser hacks like *width:0;
2419 * @param string $name
2421 public function set_actual_name($name) {
2422 $this->name = $name;
2427 * A generic CSS style class to use when a more specific class does not exist.
2429 * @package core
2430 * @subpackage cssoptimiser
2431 * @copyright 2012 Sam Hemelryk
2432 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2434 class css_style_generic extends css_style {
2437 * Cleans incoming values for typical things that can be optimised.
2439 * @param mixed $value Cleans the provided value optimising it if possible
2440 * @return string
2442 protected function clean_value($value) {
2443 if (trim($value) == '0px') {
2444 $value = 0;
2445 } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
2446 $value = '#'.strtoupper($matches[1]);
2448 return $value;
2453 * A colour CSS style
2455 * @package core
2456 * @subpackage cssoptimiser
2457 * @copyright 2012 Sam Hemelryk
2458 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2460 class css_style_color extends css_style {
2463 * Creates a new colour style
2465 * @param mixed $value Initialises a new colour style
2466 * @return css_style_color
2468 public static function init($value) {
2469 return new css_style_color('color', $value);
2473 * Cleans the colour unifing it to a 6 char hash colour if possible
2474 * Doing this allows us to associate identical colours being specified in
2475 * different ways. e.g. Red, red, #F00, and #F00000
2477 * @param mixed $value Cleans the provided value optimising it if possible
2478 * @return string
2480 protected function clean_value($value) {
2481 $value = trim($value);
2482 if (css_is_colour($value)) {
2483 if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
2484 $value = '#'.strtoupper($matches[1]);
2485 } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
2486 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
2487 $value = '#'.strtoupper($value);
2488 } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
2489 $value = css_optimiser::$htmlcolours[strtolower($value)];
2492 return $value;
2496 * Returns the colour style for use within CSS.
2497 * Will return an optimised hash colour.
2499 * e.g #123456
2500 * #123 instead of #112233
2501 * #F00 instead of red
2503 * @param string $overridevalue If provided then this value will be used instead
2504 * of the styles current value.
2505 * @return string
2507 public function out($overridevalue = null) {
2508 if ($overridevalue === null) {
2509 $overridevalue = $this->value;
2511 return parent::out(self::shrink_value($overridevalue));
2515 * Shrinks the colour value is possible.
2517 * @param string $value Shrinks the current value to an optimial form if possible
2518 * @return string
2520 public static function shrink_value($value) {
2521 if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) {
2522 return '#'.$matches[1].$matches[2].$matches[3];
2524 return $value;
2528 * Returns true if the value is a valid colour.
2530 * @return bool
2532 public function is_valid() {
2533 return css_is_colour($this->value);
2538 * A width style
2540 * @package core
2541 * @subpackage cssoptimiser
2542 * @copyright 2012 Sam Hemelryk
2543 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2545 class css_style_width extends css_style {
2548 * Checks if the width is valid
2549 * @return bool
2551 public function is_valid() {
2552 return css_is_width($this->value);
2556 * Cleans the provided value
2558 * @param mixed $value Cleans the provided value optimising it if possible
2559 * @return string
2561 protected function clean_value($value) {
2562 if (!css_is_width($value)) {
2563 // Note we don't actually change the value to something valid. That
2564 // would be bad for futureproofing.
2565 $this->set_error('Invalid width specified for '.$this->name);
2566 } else if (preg_match('#^0\D+$#', $value)) {
2567 $value = 0;
2569 return trim($value);
2573 * Initialises a new width style
2575 * @param mixed $value The value this style has
2576 * @return css_style_width
2578 public static function init($value) {
2579 return new css_style_width('width', $value);
2584 * A margin style
2586 * @package core
2587 * @subpackage cssoptimiser
2588 * @copyright 2012 Sam Hemelryk
2589 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2591 class css_style_margin extends css_style_width implements core_css_consolidatable_style {
2594 * Initialises a margin style.
2596 * In this case we split the margin into several other margin styles so that
2597 * we can properly condense overrides and then reconsolidate them later into
2598 * an optimal form.
2600 * @param string $value The value the style has
2601 * @return array An array of margin values that can later be consolidated
2603 public static function init($value) {
2604 $important = '';
2605 if (strpos($value, '!important') !== false) {
2606 $important = ' !important';
2607 $value = str_replace('!important', '', $value);
2610 $value = preg_replace('#\s+#', ' ', trim($value));
2611 $bits = explode(' ', $value, 4);
2613 $top = $right = $bottom = $left = null;
2614 if (count($bits) > 0) {
2615 $top = $right = $bottom = $left = array_shift($bits);
2617 if (count($bits) > 0) {
2618 $right = $left = array_shift($bits);
2620 if (count($bits) > 0) {
2621 $bottom = array_shift($bits);
2623 if (count($bits) > 0) {
2624 $left = array_shift($bits);
2626 return array(
2627 new css_style_margintop('margin-top', $top.$important),
2628 new css_style_marginright('margin-right', $right.$important),
2629 new css_style_marginbottom('margin-bottom', $bottom.$important),
2630 new css_style_marginleft('margin-left', $left.$important)
2635 * Consolidates individual margin styles into a single margin style
2637 * @param css_style[] $styles
2638 * @return css_style[] An array of consolidated styles
2640 public static function consolidate(array $styles) {
2641 if (count($styles) != 4) {
2642 return $styles;
2645 $someimportant = false;
2646 $allimportant = null;
2647 $notimportantequal = null;
2648 $firstvalue = null;
2649 foreach ($styles as $style) {
2650 if ($style->is_important()) {
2651 $someimportant = true;
2652 if ($allimportant === null) {
2653 $allimportant = true;
2655 } else {
2656 if ($allimportant === true) {
2657 $allimportant = false;
2659 if ($firstvalue == null) {
2660 $firstvalue = $style->get_value(false);
2661 $notimportantequal = true;
2662 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
2663 $notimportantequal = false;
2668 if ($someimportant && !$allimportant && !$notimportantequal) {
2669 return $styles;
2672 if ($someimportant && !$allimportant && $notimportantequal) {
2673 $return = array(
2674 new css_style_margin('margin', $firstvalue)
2676 foreach ($styles as $style) {
2677 if ($style->is_important()) {
2678 $return[] = $style;
2681 return $return;
2682 } else {
2683 $top = null;
2684 $right = null;
2685 $bottom = null;
2686 $left = null;
2687 foreach ($styles as $style) {
2688 switch ($style->get_name()) {
2689 case 'margin-top' :
2690 $top = $style->get_value(false);
2691 break;
2692 case 'margin-right' :
2693 $right = $style->get_value(false);
2694 break;
2695 case 'margin-bottom' :
2696 $bottom = $style->get_value(false);
2697 break;
2698 case 'margin-left' :
2699 $left = $style->get_value(false);
2700 break;
2703 if ($top == $bottom && $left == $right) {
2704 if ($top == $left) {
2705 $returnstyle = new css_style_margin('margin', $top);
2706 } else {
2707 $returnstyle = new css_style_margin('margin', "{$top} {$left}");
2709 } else if ($left == $right) {
2710 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom}");
2711 } else {
2712 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}");
2714 if ($allimportant) {
2715 $returnstyle->set_important();
2717 return array($returnstyle);
2723 * A margin top style
2725 * @package core
2726 * @subpackage cssoptimiser
2727 * @copyright 2012 Sam Hemelryk
2728 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2730 class css_style_margintop extends css_style_margin {
2733 * A simple init, just a single style
2735 * @param string $value The value the style has
2736 * @return css_style_margintop
2738 public static function init($value) {
2739 return new css_style_margintop('margin-top', $value);
2743 * This style can be consolidated into a single margin style
2745 * @return string
2747 public function consolidate_to() {
2748 return 'margin';
2753 * A margin right style
2755 * @package core
2756 * @subpackage cssoptimiser
2757 * @copyright 2012 Sam Hemelryk
2758 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2760 class css_style_marginright extends css_style_margin {
2763 * A simple init, just a single style
2765 * @param string $value The value the style has
2766 * @return css_style_margintop
2768 public static function init($value) {
2769 return new css_style_marginright('margin-right', $value);
2773 * This style can be consolidated into a single margin style
2775 * @return string
2777 public function consolidate_to() {
2778 return 'margin';
2783 * A margin bottom style
2785 * @package core
2786 * @subpackage cssoptimiser
2787 * @copyright 2012 Sam Hemelryk
2788 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2790 class css_style_marginbottom extends css_style_margin {
2793 * A simple init, just a single style
2795 * @param string $value The value the style has
2796 * @return css_style_margintop
2798 public static function init($value) {
2799 return new css_style_marginbottom('margin-bottom', $value);
2803 * This style can be consolidated into a single margin style
2805 * @return string
2807 public function consolidate_to() {
2808 return 'margin';
2813 * A margin left style
2815 * @package core
2816 * @subpackage cssoptimiser
2817 * @copyright 2012 Sam Hemelryk
2818 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2820 class css_style_marginleft extends css_style_margin {
2823 * A simple init, just a single style
2825 * @param string $value The value the style has
2826 * @return css_style_margintop
2828 public static function init($value) {
2829 return new css_style_marginleft('margin-left', $value);
2833 * This style can be consolidated into a single margin style
2835 * @return string
2837 public function consolidate_to() {
2838 return 'margin';
2843 * A border style
2845 * @package core
2846 * @subpackage cssoptimiser
2847 * @copyright 2012 Sam Hemelryk
2848 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2850 class css_style_border extends css_style implements core_css_consolidatable_style {
2853 * Initalises the border style into an array of individual style compontents
2855 * @param string $value The value the style has
2856 * @return css_style_bordercolor
2858 public static function init($value) {
2859 $value = preg_replace('#\s+#', ' ', $value);
2860 $bits = explode(' ', $value, 3);
2862 $return = array();
2863 if (count($bits) > 0) {
2864 $width = array_shift($bits);
2865 if (!css_style_borderwidth::is_border_width($width)) {
2866 $width = '0';
2868 $return[] = css_style_bordertopwidth::init($width);
2869 $return[] = css_style_borderrightwidth::init($width);
2870 $return[] = css_style_borderbottomwidth::init($width);
2871 $return[] = css_style_borderleftwidth::init($width);
2873 if (count($bits) > 0) {
2874 $style = array_shift($bits);
2875 $return[] = css_style_bordertopstyle::init($style);
2876 $return[] = css_style_borderrightstyle::init($style);
2877 $return[] = css_style_borderbottomstyle::init($style);
2878 $return[] = css_style_borderleftstyle::init($style);
2880 if (count($bits) > 0) {
2881 $colour = array_shift($bits);
2882 $return[] = css_style_bordertopcolor::init($colour);
2883 $return[] = css_style_borderrightcolor::init($colour);
2884 $return[] = css_style_borderbottomcolor::init($colour);
2885 $return[] = css_style_borderleftcolor::init($colour);
2887 return $return;
2891 * Consolidates all border styles into a single style
2893 * @param css_style[] $styles An array of border styles
2894 * @return css_style[] An optimised array of border styles
2896 public static function consolidate(array $styles) {
2898 $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2899 $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2900 $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2902 foreach ($styles as $style) {
2903 switch ($style->get_name()) {
2904 case 'border-top-width':
2905 $borderwidths['top'] = $style->get_value();
2906 break;
2907 case 'border-right-width':
2908 $borderwidths['right'] = $style->get_value();
2909 break;
2910 case 'border-bottom-width':
2911 $borderwidths['bottom'] = $style->get_value();
2912 break;
2913 case 'border-left-width':
2914 $borderwidths['left'] = $style->get_value();
2915 break;
2917 case 'border-top-style':
2918 $borderstyles['top'] = $style->get_value();
2919 break;
2920 case 'border-right-style':
2921 $borderstyles['right'] = $style->get_value();
2922 break;
2923 case 'border-bottom-style':
2924 $borderstyles['bottom'] = $style->get_value();
2925 break;
2926 case 'border-left-style':
2927 $borderstyles['left'] = $style->get_value();
2928 break;
2930 case 'border-top-color':
2931 $bordercolors['top'] = css_style_color::shrink_value($style->get_value());
2932 break;
2933 case 'border-right-color':
2934 $bordercolors['right'] = css_style_color::shrink_value($style->get_value());
2935 break;
2936 case 'border-bottom-color':
2937 $bordercolors['bottom'] = css_style_color::shrink_value($style->get_value());
2938 break;
2939 case 'border-left-color':
2940 $bordercolors['left'] = css_style_color::shrink_value($style->get_value());
2941 break;
2945 $uniquewidths = count(array_unique($borderwidths));
2946 $uniquestyles = count(array_unique($borderstyles));
2947 $uniquecolors = count(array_unique($bordercolors));
2949 $nullwidths = in_array(null, $borderwidths, true);
2950 $nullstyles = in_array(null, $borderstyles, true);
2951 $nullcolors = in_array(null, $bordercolors, true);
2953 $allwidthsthesame = ($uniquewidths === 1)?1:0;
2954 $allstylesthesame = ($uniquestyles === 1)?1:0;
2955 $allcolorsthesame = ($uniquecolors === 1)?1:0;
2957 $allwidthsnull = $allwidthsthesame && $nullwidths;
2958 $allstylesnull = $allstylesthesame && $nullstyles;
2959 $allcolorsnull = $allcolorsthesame && $nullcolors;
2961 /* @var css_style[] $return */
2962 $return = array();
2963 if ($allwidthsnull && $allstylesnull && $allcolorsnull) {
2964 // Everything is null still... boo.
2965 return array(new css_style_border('border', ''));
2967 } else if ($allwidthsnull && $allstylesnull) {
2969 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2970 return $return;
2972 } else if ($allwidthsnull && $allcolorsnull) {
2974 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2975 return $return;
2977 } else if ($allcolorsnull && $allstylesnull) {
2979 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2980 return $return;
2984 if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 3) {
2986 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']);
2988 } else if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 2) {
2990 if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) {
2992 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']);
2993 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2995 } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) {
2997 $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']);
2998 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
3000 } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) {
3002 $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']);
3003 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
3005 } else {
3006 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
3007 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
3008 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
3011 } else if (!$nullwidths && !$nullcolors && !$nullstyles &&
3012 max(array_count_values($borderwidths)) == 3 &&
3013 max(array_count_values($borderstyles)) == 3 &&
3014 max(array_count_values($bordercolors)) == 3) {
3016 $widthkeys = array();
3017 $stylekeys = array();
3018 $colorkeys = array();
3020 foreach ($borderwidths as $key => $value) {
3021 if (!array_key_exists($value, $widthkeys)) {
3022 $widthkeys[$value] = array();
3024 $widthkeys[$value][] = $key;
3026 usort($widthkeys, 'css_sort_by_count');
3027 $widthkeys = array_values($widthkeys);
3029 foreach ($borderstyles as $key => $value) {
3030 if (!array_key_exists($value, $stylekeys)) {
3031 $stylekeys[$value] = array();
3033 $stylekeys[$value][] = $key;
3035 usort($stylekeys, 'css_sort_by_count');
3036 $stylekeys = array_values($stylekeys);
3038 foreach ($bordercolors as $key => $value) {
3039 if (!array_key_exists($value, $colorkeys)) {
3040 $colorkeys[$value] = array();
3042 $colorkeys[$value][] = $key;
3044 usort($colorkeys, 'css_sort_by_count');
3045 $colorkeys = array_values($colorkeys);
3047 if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) {
3048 $key = $widthkeys[0][0];
3049 self::build_style_string($return, 'css_style_border', 'border',
3050 $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
3051 $key = $widthkeys[1][0];
3052 self::build_style_string($return, 'css_style_border'.$key, 'border-'.$key,
3053 $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
3054 } else {
3055 self::build_style_string($return, 'css_style_bordertop', 'border-top',
3056 $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
3057 self::build_style_string($return, 'css_style_borderright', 'border-right',
3058 $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
3059 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
3060 $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
3061 self::build_style_string($return, 'css_style_borderleft', 'border-left',
3062 $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
3064 } else {
3065 self::build_style_string($return, 'css_style_bordertop', 'border-top',
3066 $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
3067 self::build_style_string($return, 'css_style_borderright', 'border-right',
3068 $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
3069 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
3070 $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
3071 self::build_style_string($return, 'css_style_borderleft', 'border-left',
3072 $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
3074 foreach ($return as $key => $style) {
3075 if ($style->get_value() == '') {
3076 unset($return[$key]);
3079 return $return;
3083 * Border styles get consolidated to a single border style.
3085 * @return string
3087 public function consolidate_to() {
3088 return 'border';
3092 * Consolidates a series of border styles into an optimised array of border
3093 * styles by looking at the direction of the border and prioritising that
3094 * during the optimisation.
3096 * @param array $array An array to add styles into during consolidation. Passed by reference.
3097 * @param string $class The class type to initalise
3098 * @param string $style The style to create
3099 * @param string|array $top The top value
3100 * @param string $right The right value
3101 * @param string $bottom The bottom value
3102 * @param string $left The left value
3103 * @return bool
3105 public static function consolidate_styles_by_direction(&$array, $class, $style,
3106 $top, $right = null, $bottom = null, $left = null) {
3107 if (is_array($top)) {
3108 $right = $top['right'];
3109 $bottom = $top['bottom'];
3110 $left = $top['left'];
3111 $top = $top['top'];
3114 if ($top == $bottom && $left == $right && $top == $left) {
3115 if (is_null($top)) {
3116 $array[] = new $class($style, '');
3117 } else {
3118 $array[] = new $class($style, $top);
3120 } else if ($top == null || $right == null || $bottom == null || $left == null) {
3121 if ($top !== null) {
3122 $array[] = new $class(str_replace('border-', 'border-top-', $style), $top);
3124 if ($right !== null) {
3125 $array[] = new $class(str_replace('border-', 'border-right-', $style), $right);
3127 if ($bottom !== null) {
3128 $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom);
3130 if ($left !== null) {
3131 $array[] = new $class(str_replace('border-', 'border-left-', $style), $left);
3133 } else if ($top == $bottom && $left == $right) {
3134 $array[] = new $class($style, $top.' '.$right);
3135 } else if ($left == $right) {
3136 $array[] = new $class($style, $top.' '.$right.' '.$bottom);
3137 } else {
3138 $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left);
3140 return true;
3144 * Builds a border style for a set of width, style, and colour values
3146 * @param array $array An array into which the generated style is added
3147 * @param string $class The class type to initialise
3148 * @param string $cssstyle The style to use
3149 * @param string $width The width of the border
3150 * @param string $style The style of the border
3151 * @param string $color The colour of the border
3152 * @return bool
3154 public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) {
3155 if (!is_null($width) && !is_null($style) && !is_null($color)) {
3156 $array[] = new $class($cssstyle, $width.' '.$style.' '.$color);
3157 } else if (!is_null($width) && !is_null($style) && is_null($color)) {
3158 $array[] = new $class($cssstyle, $width.' '.$style);
3159 } else if (!is_null($width) && is_null($style) && is_null($color)) {
3160 $array[] = new $class($cssstyle, $width);
3161 } else {
3162 if (!is_null($width)) {
3163 $array[] = new $class($cssstyle, $width);
3165 if (!is_null($style)) {
3166 $array[] = new $class($cssstyle, $style);
3168 if (!is_null($color)) {
3169 $array[] = new $class($cssstyle, $color);
3172 return true;
3177 * A border colour style
3179 * @package core
3180 * @subpackage cssoptimiser
3181 * @copyright 2012 Sam Hemelryk
3182 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3184 class css_style_bordercolor extends css_style_color {
3187 * Creates a new border colour style
3189 * Based upon the colour style
3191 * @param mixed $value
3192 * @return Array of css_style_bordercolor
3194 public static function init($value) {
3195 $value = preg_replace('#\s+#', ' ', $value);
3196 $bits = explode(' ', $value, 4);
3198 $top = $right = $bottom = $left = null;
3199 if (count($bits) > 0) {
3200 $top = $right = $bottom = $left = array_shift($bits);
3202 if (count($bits) > 0) {
3203 $right = $left = array_shift($bits);
3205 if (count($bits) > 0) {
3206 $bottom = array_shift($bits);
3208 if (count($bits) > 0) {
3209 $left = array_shift($bits);
3211 return array(
3212 css_style_bordertopcolor::init($top),
3213 css_style_borderrightcolor::init($right),
3214 css_style_borderbottomcolor::init($bottom),
3215 css_style_borderleftcolor::init($left)
3220 * Consolidate this to a single border style
3222 * @return string
3224 public function consolidate_to() {
3225 return 'border';
3229 * Cleans the value
3231 * @param string $value Cleans the provided value optimising it if possible
3232 * @return string
3234 protected function clean_value($value) {
3235 $values = explode(' ', $value);
3236 $values = array_map('parent::clean_value', $values);
3237 return join (' ', $values);
3241 * Outputs this style
3243 * @param string $overridevalue
3244 * @return string
3246 public function out($overridevalue = null) {
3247 if ($overridevalue === null) {
3248 $overridevalue = $this->value;
3250 $values = explode(' ', $overridevalue);
3251 $values = array_map('css_style_color::shrink_value', $values);
3252 return parent::out(join (' ', $values));
3257 * A border left style
3259 * @package core
3260 * @subpackage cssoptimiser
3261 * @copyright 2012 Sam Hemelryk
3262 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3264 class css_style_borderleft extends css_style_generic {
3267 * Initialises the border left style into individual components
3269 * @param string $value
3270 * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor
3272 public static function init($value) {
3273 $value = preg_replace('#\s+#', ' ', $value);
3274 $bits = explode(' ', $value, 3);
3276 $return = array();
3277 if (count($bits) > 0) {
3278 $return[] = css_style_borderleftwidth::init(array_shift($bits));
3280 if (count($bits) > 0) {
3281 $return[] = css_style_borderleftstyle::init(array_shift($bits));
3283 if (count($bits) > 0) {
3284 $return[] = css_style_borderleftcolor::init(array_shift($bits));
3286 return $return;
3290 * Consolidate this to a single border style
3292 * @return string
3294 public function consolidate_to() {
3295 return 'border';
3300 * A border right style
3302 * @package core
3303 * @subpackage cssoptimiser
3304 * @copyright 2012 Sam Hemelryk
3305 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3307 class css_style_borderright extends css_style_generic {
3310 * Initialises the border right style into individual components
3312 * @param string $value The value of the style
3313 * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor
3315 public static function init($value) {
3316 $value = preg_replace('#\s+#', ' ', $value);
3317 $bits = explode(' ', $value, 3);
3319 $return = array();
3320 if (count($bits) > 0) {
3321 $return[] = css_style_borderrightwidth::init(array_shift($bits));
3323 if (count($bits) > 0) {
3324 $return[] = css_style_borderrightstyle::init(array_shift($bits));
3326 if (count($bits) > 0) {
3327 $return[] = css_style_borderrightcolor::init(array_shift($bits));
3329 return $return;
3333 * Consolidate this to a single border style
3335 * @return string
3337 public function consolidate_to() {
3338 return 'border';
3343 * A border top style
3345 * @package core
3346 * @subpackage cssoptimiser
3347 * @copyright 2012 Sam Hemelryk
3348 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3350 class css_style_bordertop extends css_style_generic {
3353 * Initialises the border top style into individual components
3355 * @param string $value The value of the style
3356 * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor
3358 public static function init($value) {
3359 $value = preg_replace('#\s+#', ' ', $value);
3360 $bits = explode(' ', $value, 3);
3362 $return = array();
3363 if (count($bits) > 0) {
3364 $return[] = css_style_bordertopwidth::init(array_shift($bits));
3366 if (count($bits) > 0) {
3367 $return[] = css_style_bordertopstyle::init(array_shift($bits));
3369 if (count($bits) > 0) {
3370 $return[] = css_style_bordertopcolor::init(array_shift($bits));
3372 return $return;
3376 * Consolidate this to a single border style
3378 * @return string
3380 public function consolidate_to() {
3381 return 'border';
3386 * A border bottom style
3388 * @package core
3389 * @subpackage cssoptimiser
3390 * @copyright 2012 Sam Hemelryk
3391 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3393 class css_style_borderbottom extends css_style_generic {
3396 * Initialises the border bottom style into individual components
3398 * @param string $value The value of the style
3399 * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor
3401 public static function init($value) {
3402 $value = preg_replace('#\s+#', ' ', $value);
3403 $bits = explode(' ', $value, 3);
3405 $return = array();
3406 if (count($bits) > 0) {
3407 $return[] = css_style_borderbottomwidth::init(array_shift($bits));
3409 if (count($bits) > 0) {
3410 $return[] = css_style_borderbottomstyle::init(array_shift($bits));
3412 if (count($bits) > 0) {
3413 $return[] = css_style_borderbottomcolor::init(array_shift($bits));
3415 return $return;
3419 * Consolidate this to a single border style
3421 * @return string
3423 public function consolidate_to() {
3424 return 'border';
3429 * A border width style
3431 * @package core
3432 * @subpackage cssoptimiser
3433 * @copyright 2012 Sam Hemelryk
3434 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3436 class css_style_borderwidth extends css_style_width {
3439 * Creates a new border colour style
3441 * Based upon the colour style
3443 * @param string $value The value of the style
3444 * @return array Array of css_style_border*width
3446 public static function init($value) {
3447 $value = preg_replace('#\s+#', ' ', $value);
3448 $bits = explode(' ', $value, 4);
3450 $top = $right = $bottom = $left = null;
3451 if (count($bits) > 0) {
3452 $top = $right = $bottom = $left = array_shift($bits);
3454 if (count($bits) > 0) {
3455 $right = $left = array_shift($bits);
3457 if (count($bits) > 0) {
3458 $bottom = array_shift($bits);
3460 if (count($bits) > 0) {
3461 $left = array_shift($bits);
3463 return array(
3464 css_style_bordertopwidth::init($top),
3465 css_style_borderrightwidth::init($right),
3466 css_style_borderbottomwidth::init($bottom),
3467 css_style_borderleftwidth::init($left)
3472 * Consolidate this to a single border style
3474 * @return string
3476 public function consolidate_to() {
3477 return 'border';
3481 * Checks if the width is valid
3482 * @return bool
3484 public function is_valid() {
3485 return self::is_border_width($this->value);
3489 * Cleans the provided value
3491 * @param mixed $value Cleans the provided value optimising it if possible
3492 * @return string
3494 protected function clean_value($value) {
3495 $isvalid = self::is_border_width($value);
3496 if (!$isvalid) {
3497 $this->set_error('Invalid width specified for '.$this->name);
3498 } else if (preg_match('#^0\D+$#', $value)) {
3499 return '0';
3501 return trim($value);
3505 * Returns true if the provided value is a permitted border width
3506 * @param string $value The value to check
3507 * @return bool
3509 public static function is_border_width($value) {
3510 $altwidthvalues = array('thin', 'medium', 'thick');
3511 return css_is_width($value) || in_array($value, $altwidthvalues);
3516 * A border style style
3518 * @package core
3519 * @subpackage cssoptimiser
3520 * @copyright 2012 Sam Hemelryk
3521 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3523 class css_style_borderstyle extends css_style_generic {
3526 * Creates a new border colour style
3528 * Based upon the colour style
3530 * @param string $value The value of the style
3531 * @return array Array of css_style_border*style
3533 public static function init($value) {
3534 $value = preg_replace('#\s+#', ' ', $value);
3535 $bits = explode(' ', $value, 4);
3537 $top = $right = $bottom = $left = null;
3538 if (count($bits) > 0) {
3539 $top = $right = $bottom = $left = array_shift($bits);
3541 if (count($bits) > 0) {
3542 $right = $left = array_shift($bits);
3544 if (count($bits) > 0) {
3545 $bottom = array_shift($bits);
3547 if (count($bits) > 0) {
3548 $left = array_shift($bits);
3550 return array(
3551 css_style_bordertopstyle::init($top),
3552 css_style_borderrightstyle::init($right),
3553 css_style_borderbottomstyle::init($bottom),
3554 css_style_borderleftstyle::init($left)
3559 * Consolidate this to a single border style
3561 * @return string
3563 public function consolidate_to() {
3564 return 'border';
3569 * A border top colour style
3571 * @package core
3572 * @subpackage cssoptimiser
3573 * @copyright 2012 Sam Hemelryk
3574 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3576 class css_style_bordertopcolor extends css_style_bordercolor {
3579 * Initialises this style object
3581 * @param string $value The value of the style
3582 * @return css_style_bordertopcolor
3584 public static function init($value) {
3585 return new css_style_bordertopcolor('border-top-color', $value);
3589 * Consolidate this to a single border style
3591 * @return string
3593 public function consolidate_to() {
3594 return 'border';
3599 * A border left colour style
3601 * @package core
3602 * @subpackage cssoptimiser
3603 * @copyright 2012 Sam Hemelryk
3604 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3606 class css_style_borderleftcolor extends css_style_bordercolor {
3609 * Initialises this style object
3611 * @param string $value The value of the style
3612 * @return css_style_borderleftcolor
3614 public static function init($value) {
3615 return new css_style_borderleftcolor('border-left-color', $value);
3619 * Consolidate this to a single border style
3621 * @return string
3623 public function consolidate_to() {
3624 return 'border';
3629 * A border right colour style
3631 * @package core
3632 * @subpackage cssoptimiser
3633 * @copyright 2012 Sam Hemelryk
3634 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3636 class css_style_borderrightcolor extends css_style_bordercolor {
3639 * Initialises this style object
3641 * @param string $value The value of the style
3642 * @return css_style_borderrightcolor
3644 public static function init($value) {
3645 return new css_style_borderrightcolor('border-right-color', $value);
3649 * Consolidate this to a single border style
3651 * @return string
3653 public function consolidate_to() {
3654 return 'border';
3659 * A border bottom colour style
3661 * @package core
3662 * @subpackage cssoptimiser
3663 * @copyright 2012 Sam Hemelryk
3664 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3666 class css_style_borderbottomcolor extends css_style_bordercolor {
3669 * Initialises this style object
3671 * @param string $value The value of the style
3672 * @return css_style_borderbottomcolor
3674 public static function init($value) {
3675 return new css_style_borderbottomcolor('border-bottom-color', $value);
3679 * Consolidate this to a single border style
3681 * @return string
3683 public function consolidate_to() {
3684 return 'border';
3689 * A border width top style
3691 * @package core
3692 * @subpackage cssoptimiser
3693 * @copyright 2012 Sam Hemelryk
3694 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3696 class css_style_bordertopwidth extends css_style_borderwidth {
3699 * Initialises this style object
3701 * @param string $value The value of the style
3702 * @return css_style_bordertopwidth
3704 public static function init($value) {
3705 return new css_style_bordertopwidth('border-top-width', $value);
3709 * Consolidate this to a single border style
3711 * @return string
3713 public function consolidate_to() {
3714 return 'border';
3719 * A border width left style
3721 * @package core
3722 * @subpackage cssoptimiser
3723 * @copyright 2012 Sam Hemelryk
3724 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3726 class css_style_borderleftwidth extends css_style_borderwidth {
3729 * Initialises this style object
3731 * @param string $value The value of the style
3732 * @return css_style_borderleftwidth
3734 public static function init($value) {
3735 return new css_style_borderleftwidth('border-left-width', $value);
3739 * Consolidate this to a single border style
3741 * @return string
3743 public function consolidate_to() {
3744 return 'border';
3749 * A border width right style
3751 * @package core
3752 * @subpackage cssoptimiser
3753 * @copyright 2012 Sam Hemelryk
3754 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3756 class css_style_borderrightwidth extends css_style_borderwidth {
3759 * Initialises this style object
3761 * @param string $value The value of the style
3762 * @return css_style_borderrightwidth
3764 public static function init($value) {
3765 return new css_style_borderrightwidth('border-right-width', $value);
3769 * Consolidate this to a single border style
3771 * @return string
3773 public function consolidate_to() {
3774 return 'border';
3779 * A border width bottom style
3781 * @package core
3782 * @subpackage cssoptimiser
3783 * @copyright 2012 Sam Hemelryk
3784 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3786 class css_style_borderbottomwidth extends css_style_borderwidth {
3789 * Initialises this style object
3791 * @param string $value The value of the style
3792 * @return css_style_borderbottomwidth
3794 public static function init($value) {
3795 return new css_style_borderbottomwidth('border-bottom-width', $value);
3799 * Consolidate this to a single border style
3801 * @return string
3803 public function consolidate_to() {
3804 return 'border';
3809 * A border top style
3811 * @package core
3812 * @subpackage cssoptimiser
3813 * @copyright 2012 Sam Hemelryk
3814 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3816 class css_style_bordertopstyle extends css_style_borderstyle {
3819 * Initialises this style object
3821 * @param string $value The value of the style
3822 * @return css_style_bordertopstyle
3824 public static function init($value) {
3825 return new css_style_bordertopstyle('border-top-style', $value);
3829 * Consolidate this to a single border style
3831 * @return string
3833 public function consolidate_to() {
3834 return 'border';
3839 * A border left style
3841 * @package core
3842 * @subpackage cssoptimiser
3843 * @copyright 2012 Sam Hemelryk
3844 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3846 class css_style_borderleftstyle extends css_style_borderstyle {
3849 * Initialises this style object
3851 * @param string $value The value of the style
3852 * @return css_style_borderleftstyle
3854 public static function init($value) {
3855 return new css_style_borderleftstyle('border-left-style', $value);
3859 * Consolidate this to a single border style
3861 * @return string
3863 public function consolidate_to() {
3864 return 'border';
3869 * A border right style
3871 * @package core
3872 * @subpackage cssoptimiser
3873 * @copyright 2012 Sam Hemelryk
3874 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3876 class css_style_borderrightstyle extends css_style_borderstyle {
3879 * Initialises this style object
3881 * @param string $value The value of the style
3882 * @return css_style_borderrightstyle
3884 public static function init($value) {
3885 return new css_style_borderrightstyle('border-right-style', $value);
3889 * Consolidate this to a single border style
3891 * @return string
3893 public function consolidate_to() {
3894 return 'border';
3899 * A border bottom style
3901 * @package core
3902 * @subpackage cssoptimiser
3903 * @copyright 2012 Sam Hemelryk
3904 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3906 class css_style_borderbottomstyle extends css_style_borderstyle {
3909 * Initialises this style object
3911 * @param string $value The value for the style
3912 * @return css_style_borderbottomstyle
3914 public static function init($value) {
3915 return new css_style_borderbottomstyle('border-bottom-style', $value);
3919 * Consolidate this to a single border style
3921 * @return string
3923 public function consolidate_to() {
3924 return 'border';
3929 * A background style
3931 * @package core
3932 * @subpackage cssoptimiser
3933 * @copyright 2012 Sam Hemelryk
3934 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3936 class css_style_background extends css_style implements core_css_consolidatable_style {
3939 * Initialises a background style
3941 * @param string $value The value of the style
3942 * @return array An array of background component.
3944 public static function init($value) {
3945 // Colour - image - repeat - attachment - position.
3946 $imageurl = null;
3947 if (preg_match('#url\(([^\)]+)\)#', $value, $matches)) {
3948 $imageurl = trim($matches[1]);
3949 $value = str_replace($matches[1], '', $value);
3952 // Switch out the brackets so that they don't get messed up when we explode.
3953 $brackets = array();
3954 $bracketcount = 0;
3955 while (preg_match('#\([^\)\(]+\)#', $value, $matches)) {
3956 $key = "##BRACKET-{$bracketcount}##";
3957 $bracketcount++;
3958 $brackets[$key] = $matches[0];
3959 $value = str_replace($matches[0], $key, $value);
3962 $important = (stripos($value, '!important') !== false);
3963 if ($important) {
3964 // Great some genius put !important in the background shorthand property.
3965 $value = str_replace('!important', '', $value);
3968 $value = preg_replace('#\s+#', ' ', $value);
3969 $bits = explode(' ', $value);
3971 foreach ($bits as $key => $bit) {
3972 $bits[$key] = self::replace_bracket_placeholders($bit, $brackets);
3974 unset($bracketcount);
3975 unset($brackets);
3977 $repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit');
3978 $attachments = array('scroll' , 'fixed', 'inherit');
3979 $positions = array('top', 'left', 'bottom', 'right', 'center');
3981 /* @var css_style_background[] $return */
3982 $return = array();
3983 $unknownbits = array();
3985 $color = self::NULL_VALUE;
3986 if (count($bits) > 0 && css_is_colour(reset($bits))) {
3987 $color = array_shift($bits);
3990 $image = self::NULL_VALUE;
3991 if (count($bits) > 0 && preg_match('#^\s*(none|inherit|url\(\))\s*$#', reset($bits))) {
3992 $image = array_shift($bits);
3993 if ($image == 'url()') {
3994 $image = "url({$imageurl})";
3998 $repeat = self::NULL_VALUE;
3999 if (count($bits) > 0 && in_array(reset($bits), $repeats)) {
4000 $repeat = array_shift($bits);
4003 $attachment = self::NULL_VALUE;
4004 if (count($bits) > 0 && in_array(reset($bits), $attachments)) {
4005 // Scroll , fixed, inherit.
4006 $attachment = array_shift($bits);
4009 $position = self::NULL_VALUE;
4010 if (count($bits) > 0) {
4011 $widthbits = array();
4012 foreach ($bits as $bit) {
4013 if (in_array($bit, $positions) || css_is_width($bit)) {
4014 $widthbits[] = $bit;
4015 } else {
4016 $unknownbits[] = $bit;
4019 if (count($widthbits)) {
4020 $position = join(' ', $widthbits);
4024 if (count($unknownbits)) {
4025 foreach ($unknownbits as $bit) {
4026 $bit = trim($bit);
4027 if ($color === self::NULL_VALUE && css_is_colour($bit)) {
4028 $color = $bit;
4029 } else if ($repeat === self::NULL_VALUE && in_array($bit, $repeats)) {
4030 $repeat = $bit;
4031 } else if ($attachment === self::NULL_VALUE && in_array($bit, $attachments)) {
4032 $attachment = $bit;
4033 } else if ($bit !== '') {
4034 $advanced = css_style_background_advanced::init($bit);
4035 if ($important) {
4036 $advanced->set_important();
4038 $return[] = $advanced;
4043 if ($color === self::NULL_VALUE &&
4044 $image === self::NULL_VALUE &&
4045 $repeat === self::NULL_VALUE && $attachment === self::NULL_VALUE &&
4046 $position === self::NULL_VALUE) {
4047 // All primaries are null, return without doing anything else. There may be advanced madness there.
4048 return $return;
4051 $return[] = css_style_backgroundcolor::init($color);
4052 $return[] = css_style_backgroundimage::init($image);
4053 $return[] = css_style_backgroundrepeat::init($repeat);
4054 $return[] = css_style_backgroundattachment::init($attachment);
4055 $return[] = css_style_backgroundposition::init($position);
4057 if ($important) {
4058 foreach ($return as $style) {
4059 $style->set_important();
4063 return $return;
4067 * Static helper method to switch in bracket replacements
4069 * @param string $value
4070 * @param array $placeholders
4071 * @return string
4073 protected static function replace_bracket_placeholders($value, array $placeholders) {
4074 while (preg_match('/##BRACKET-\d+##/', $value, $matches)) {
4075 $value = str_replace($matches[0], $placeholders[$matches[0]], $value);
4077 return $value;
4081 * Consolidates background styles into a single background style
4083 * @param css_style_background[] $styles Consolidates the provided array of background styles
4084 * @return css_style[] Consolidated optimised background styles
4086 public static function consolidate(array $styles) {
4088 if (empty($styles)) {
4089 return $styles;
4092 $color = null;
4093 $image = null;
4094 $repeat = null;
4095 $attachment = null;
4096 $position = null;
4097 $size = null;
4098 $origin = null;
4099 $clip = null;
4101 $someimportant = false;
4102 $allimportant = null;
4103 foreach ($styles as $style) {
4104 if ($style instanceof css_style_backgroundimage_advanced) {
4105 continue;
4107 if ($style->is_important()) {
4108 $someimportant = true;
4109 if ($allimportant === null) {
4110 $allimportant = true;
4112 } else if ($allimportant === true) {
4113 $allimportant = false;
4117 /* @var css_style[] $organisedstyles */
4118 $organisedstyles = array();
4119 /* @var css_style[] $advancedstyles */
4120 $advancedstyles = array();
4121 /* @var css_style[] $importantstyles */
4122 $importantstyles = array();
4123 foreach ($styles as $style) {
4124 if ($style instanceof css_style_backgroundimage_advanced) {
4125 $advancedstyles[] = $style;
4126 continue;
4128 if ($someimportant && !$allimportant && $style->is_important()) {
4129 $importantstyles[] = $style;
4130 continue;
4132 $organisedstyles[$style->get_name()] = $style;
4133 switch ($style->get_name()) {
4134 case 'background-color' :
4135 $color = css_style_color::shrink_value($style->get_value(false));
4136 break;
4137 case 'background-image' :
4138 $image = $style->get_value(false);
4139 break;
4140 case 'background-repeat' :
4141 $repeat = $style->get_value(false);
4142 break;
4143 case 'background-attachment' :
4144 $attachment = $style->get_value(false);
4145 break;
4146 case 'background-position' :
4147 $position = $style->get_value(false);
4148 break;
4149 case 'background-clip' :
4150 $clip = $style->get_value();
4151 break;
4152 case 'background-origin' :
4153 $origin = $style->get_value();
4154 break;
4155 case 'background-size' :
4156 $size = $style->get_value();
4157 break;
4161 /* @var css_style[] $consolidatetosingle */
4162 $consolidatetosingle = array();
4163 if (!is_null($color) && !is_null($image) && !is_null($repeat) && !is_null($attachment) && !is_null($position)) {
4164 // We can use the shorthand background-style!
4165 if (!$organisedstyles['background-color']->is_special_empty_value()) {
4166 $consolidatetosingle[] = $color;
4168 if (!$organisedstyles['background-image']->is_special_empty_value()) {
4169 $consolidatetosingle[] = $image;
4171 if (!$organisedstyles['background-repeat']->is_special_empty_value()) {
4172 $consolidatetosingle[] = $repeat;
4174 if (!$organisedstyles['background-attachment']->is_special_empty_value()) {
4175 $consolidatetosingle[] = $attachment;
4177 if (!$organisedstyles['background-position']->is_special_empty_value()) {
4178 $consolidatetosingle[] = $position;
4180 // Reset them all to null so we don't use them again.
4181 $color = null;
4182 $image = null;
4183 $repeat = null;
4184 $attachment = null;
4185 $position = null;
4188 $return = array();
4189 // Single background style needs to come first.
4190 if (count($consolidatetosingle) > 0) {
4191 $returnstyle = new css_style_background('background', join(' ', $consolidatetosingle));
4192 if ($allimportant) {
4193 $returnstyle->set_important();
4195 $return[] = $returnstyle;
4197 foreach ($styles as $style) {
4198 $value = null;
4199 switch ($style->get_name()) {
4200 case 'background-color' :
4201 $value = $color;
4202 break;
4203 case 'background-image' :
4204 $value = $image;
4205 break;
4206 case 'background-repeat' :
4207 $value = $repeat;
4208 break;
4209 case 'background-attachment' :
4210 $value = $attachment;
4211 break;
4212 case 'background-position' :
4213 $value = $position;
4214 break;
4215 case 'background-clip' :
4216 $value = $clip;
4217 break;
4218 case 'background-origin':
4219 $value = $origin;
4220 break;
4221 case 'background-size':
4222 $value = $size;
4223 break;
4225 if (!is_null($value)) {
4226 $return[] = $style;
4229 $return = array_merge($return, $importantstyles, $advancedstyles);
4230 return $return;
4235 * A advanced background style that allows multiple values to preserve unknown entities
4237 * @package core
4238 * @subpackage cssoptimiser
4239 * @copyright 2012 Sam Hemelryk
4240 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4242 class css_style_background_advanced extends css_style_generic {
4244 * Creates a new background colour style
4246 * @param string $value The value of the style
4247 * @return css_style_backgroundimage
4249 public static function init($value) {
4250 $value = preg_replace('#\s+#', ' ', $value);
4251 return new css_style_background_advanced('background', $value);
4255 * Returns true because the advanced background image supports multiple values.
4256 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4258 * @return boolean
4260 public function allows_multiple_values() {
4261 return true;
4266 * A background colour style.
4268 * Based upon the colour style.
4270 * @package core
4271 * @subpackage cssoptimiser
4272 * @copyright 2012 Sam Hemelryk
4273 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4275 class css_style_backgroundcolor extends css_style_color {
4278 * Creates a new background colour style
4280 * @param string $value The value of the style
4281 * @return css_style_backgroundcolor
4283 public static function init($value) {
4284 return new css_style_backgroundcolor('background-color', $value);
4288 * css_style_backgroundcolor consolidates to css_style_background
4290 * @return string
4292 public function consolidate_to() {
4293 return 'background';
4297 * Returns true if the value for this style is the special null value.
4299 * This occurs if the shorthand background property was used but no proper value
4300 * was specified for this style.
4301 * This leads to a null value being used unless otherwise overridden.
4303 * @return bool
4305 public function is_special_empty_value() {
4306 return ($this->value === self::NULL_VALUE);
4310 * Returns true if the value for this style is valid
4311 * @return bool
4313 public function is_valid() {
4314 return $this->is_special_empty_value() || parent::is_valid();
4319 * A background image style.
4321 * @package core
4322 * @subpackage cssoptimiser
4323 * @copyright 2012 Sam Hemelryk
4324 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4326 class css_style_backgroundimage extends css_style_generic {
4329 * Creates a new background image style
4331 * @param string $value The value of the style
4332 * @return css_style_backgroundimage
4334 public static function init($value) {
4335 if ($value !== self::NULL_VALUE && !preg_match('#^\s*(none|inherit|url\()#i', $value)) {
4336 return css_style_backgroundimage_advanced::init($value);
4338 return new css_style_backgroundimage('background-image', $value);
4342 * Consolidates this style into a single background style
4344 * @return string
4346 public function consolidate_to() {
4347 return 'background';
4351 * Returns true if the value for this style is the special null value.
4353 * This occurs if the shorthand background property was used but no proper value
4354 * was specified for this style.
4355 * This leads to a null value being used unless otherwise overridden.
4357 * @return bool
4359 public function is_special_empty_value() {
4360 return ($this->value === self::NULL_VALUE);
4364 * Returns true if the value for this style is valid
4365 * @return bool
4367 public function is_valid() {
4368 return $this->is_special_empty_value() || parent::is_valid();
4373 * A background image style that supports multiple values and masquerades as a background-image
4375 * @package core
4376 * @subpackage cssoptimiser
4377 * @copyright 2012 Sam Hemelryk
4378 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4380 class css_style_backgroundimage_advanced extends css_style_generic {
4382 * Creates a new background colour style
4384 * @param string $value The value of the style
4385 * @return css_style_backgroundimage
4387 public static function init($value) {
4388 $value = preg_replace('#\s+#', ' ', $value);
4389 return new css_style_backgroundimage_advanced('background-image', $value);
4393 * Returns true because the advanced background image supports multiple values.
4394 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4396 * @return boolean
4398 public function allows_multiple_values() {
4399 return true;
4404 * A background repeat style.
4406 * @package core
4407 * @subpackage cssoptimiser
4408 * @copyright 2012 Sam Hemelryk
4409 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4411 class css_style_backgroundrepeat extends css_style_generic {
4414 * Creates a new background colour style
4416 * @param string $value The value of the style
4417 * @return css_style_backgroundrepeat
4419 public static function init($value) {
4420 return new css_style_backgroundrepeat('background-repeat', $value);
4424 * Consolidates this style into a single background style
4426 * @return string
4428 public function consolidate_to() {
4429 return 'background';
4433 * Returns true if the value for this style is the special null value.
4435 * This occurs if the shorthand background property was used but no proper value
4436 * was specified for this style.
4437 * This leads to a null value being used unless otherwise overridden.
4439 * @return bool
4441 public function is_special_empty_value() {
4442 return ($this->value === self::NULL_VALUE);
4446 * Returns true if the value for this style is valid
4447 * @return bool
4449 public function is_valid() {
4450 return $this->is_special_empty_value() || parent::is_valid();
4455 * A background attachment style.
4457 * @package core
4458 * @subpackage cssoptimiser
4459 * @copyright 2012 Sam Hemelryk
4460 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4462 class css_style_backgroundattachment extends css_style_generic {
4465 * Creates a new background colour style
4467 * @param string $value The value of the style
4468 * @return css_style_backgroundattachment
4470 public static function init($value) {
4471 return new css_style_backgroundattachment('background-attachment', $value);
4475 * Consolidates this style into a single background style
4477 * @return string
4479 public function consolidate_to() {
4480 return 'background';
4484 * Returns true if the value for this style is the special null value.
4486 * This occurs if the shorthand background property was used but no proper value
4487 * was specified for this style.
4488 * This leads to a null value being used unless otherwise overridden.
4490 * @return bool
4492 public function is_special_empty_value() {
4493 return ($this->value === self::NULL_VALUE);
4497 * Returns true if the value for this style is valid
4498 * @return bool
4500 public function is_valid() {
4501 return $this->is_special_empty_value() || parent::is_valid();
4506 * A background position style.
4508 * @package core
4509 * @subpackage cssoptimiser
4510 * @copyright 2012 Sam Hemelryk
4511 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4513 class css_style_backgroundposition extends css_style_generic {
4516 * Creates a new background colour style
4518 * @param string $value The value of the style
4519 * @return css_style_backgroundposition
4521 public static function init($value) {
4522 return new css_style_backgroundposition('background-position', $value);
4526 * Consolidates this style into a single background style
4528 * @return string
4530 public function consolidate_to() {
4531 return 'background';
4535 * Returns true if the value for this style is the special null value.
4537 * This occurs if the shorthand background property was used but no proper value
4538 * was specified for this style.
4539 * This leads to a null value being used unless otherwise overridden.
4541 * @return bool
4543 public function is_special_empty_value() {
4544 return ($this->value === self::NULL_VALUE);
4548 * Returns true if the value for this style is valid
4549 * @return bool
4551 public function is_valid() {
4552 return $this->is_special_empty_value() || parent::is_valid();
4557 * A background size style.
4559 * @package core
4560 * @subpackage cssoptimiser
4561 * @copyright 2012 Sam Hemelryk
4562 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4564 class css_style_backgroundsize extends css_style_generic {
4567 * Creates a new background size style
4569 * @param string $value The value of the style
4570 * @return css_style_backgroundposition
4572 public static function init($value) {
4573 return new css_style_backgroundsize('background-size', $value);
4577 * Consolidates this style into a single background style
4579 * @return string
4581 public function consolidate_to() {
4582 return 'background';
4587 * A background clip style.
4589 * @package core
4590 * @subpackage cssoptimiser
4591 * @copyright 2012 Sam Hemelryk
4592 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4594 class css_style_backgroundclip extends css_style_generic {
4597 * Creates a new background clip style
4599 * @param string $value The value of the style
4600 * @return css_style_backgroundposition
4602 public static function init($value) {
4603 return new css_style_backgroundclip('background-clip', $value);
4607 * Consolidates this style into a single background style
4609 * @return string
4611 public function consolidate_to() {
4612 return 'background';
4617 * A background origin style.
4619 * @package core
4620 * @subpackage cssoptimiser
4621 * @copyright 2012 Sam Hemelryk
4622 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4624 class css_style_backgroundorigin extends css_style_generic {
4627 * Creates a new background origin style
4629 * @param string $value The value of the style
4630 * @return css_style_backgroundposition
4632 public static function init($value) {
4633 return new css_style_backgroundorigin('background-origin', $value);
4637 * Consolidates this style into a single background style
4639 * @return string
4641 public function consolidate_to() {
4642 return 'background';
4647 * A padding style.
4649 * @package core
4650 * @subpackage cssoptimiser
4651 * @copyright 2012 Sam Hemelryk
4652 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4654 class css_style_padding extends css_style_width implements core_css_consolidatable_style {
4657 * Initialises this padding style into several individual padding styles
4659 * @param string $value The value fo the style
4660 * @return array An array of padding styles
4662 public static function init($value) {
4663 $important = '';
4664 if (strpos($value, '!important') !== false) {
4665 $important = ' !important';
4666 $value = str_replace('!important', '', $value);
4669 $value = preg_replace('#\s+#', ' ', trim($value));
4670 $bits = explode(' ', $value, 4);
4672 $top = $right = $bottom = $left = null;
4673 if (count($bits) > 0) {
4674 $top = $right = $bottom = $left = array_shift($bits);
4676 if (count($bits) > 0) {
4677 $right = $left = array_shift($bits);
4679 if (count($bits) > 0) {
4680 $bottom = array_shift($bits);
4682 if (count($bits) > 0) {
4683 $left = array_shift($bits);
4685 return array(
4686 new css_style_paddingtop('padding-top', $top.$important),
4687 new css_style_paddingright('padding-right', $right.$important),
4688 new css_style_paddingbottom('padding-bottom', $bottom.$important),
4689 new css_style_paddingleft('padding-left', $left.$important)
4694 * Consolidates several padding styles into a single style.
4696 * @param css_style_padding[] $styles Array of padding styles
4697 * @return css_style[] Optimised+consolidated array of padding styles
4699 public static function consolidate(array $styles) {
4700 if (count($styles) != 4) {
4701 return $styles;
4704 $someimportant = false;
4705 $allimportant = null;
4706 $notimportantequal = null;
4707 $firstvalue = null;
4708 foreach ($styles as $style) {
4709 if ($style->is_important()) {
4710 $someimportant = true;
4711 if ($allimportant === null) {
4712 $allimportant = true;
4714 } else {
4715 if ($allimportant === true) {
4716 $allimportant = false;
4718 if ($firstvalue == null) {
4719 $firstvalue = $style->get_value(false);
4720 $notimportantequal = true;
4721 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
4722 $notimportantequal = false;
4727 if ($someimportant && !$allimportant && !$notimportantequal) {
4728 return $styles;
4731 if ($someimportant && !$allimportant && $notimportantequal) {
4732 $return = array(
4733 new css_style_padding('padding', $firstvalue)
4735 foreach ($styles as $style) {
4736 if ($style->is_important()) {
4737 $return[] = $style;
4740 return $return;
4741 } else {
4742 $top = null;
4743 $right = null;
4744 $bottom = null;
4745 $left = null;
4746 foreach ($styles as $style) {
4747 switch ($style->get_name()) {
4748 case 'padding-top' :
4749 $top = $style->get_value(false);
4750 break;
4751 case 'padding-right' :
4752 $right = $style->get_value(false);
4753 break;
4754 case 'padding-bottom' :
4755 $bottom = $style->get_value(false);
4756 break;
4757 case 'padding-left' :
4758 $left = $style->get_value(false);
4759 break;
4762 if ($top == $bottom && $left == $right) {
4763 if ($top == $left) {
4764 $returnstyle = new css_style_padding('padding', $top);
4765 } else {
4766 $returnstyle = new css_style_padding('padding', "{$top} {$left}");
4768 } else if ($left == $right) {
4769 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom}");
4770 } else {
4771 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom} {$left}");
4773 if ($allimportant) {
4774 $returnstyle->set_important();
4776 return array($returnstyle);
4782 * A padding top style.
4784 * @package core
4785 * @subpackage cssoptimiser
4786 * @copyright 2012 Sam Hemelryk
4787 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4789 class css_style_paddingtop extends css_style_padding {
4792 * Initialises this style
4794 * @param string $value The value of the style
4795 * @return css_style_paddingtop
4797 public static function init($value) {
4798 return new css_style_paddingtop('padding-top', $value);
4802 * Consolidates this style into a single padding style
4804 * @return string
4806 public function consolidate_to() {
4807 return 'padding';
4812 * A padding right style.
4814 * @package core
4815 * @subpackage cssoptimiser
4816 * @copyright 2012 Sam Hemelryk
4817 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4819 class css_style_paddingright extends css_style_padding {
4822 * Initialises this style
4824 * @param string $value The value of the style
4825 * @return css_style_paddingright
4827 public static function init($value) {
4828 return new css_style_paddingright('padding-right', $value);
4832 * Consolidates this style into a single padding style
4834 * @return string
4836 public function consolidate_to() {
4837 return 'padding';
4842 * A padding bottom style.
4844 * @package core
4845 * @subpackage cssoptimiser
4846 * @copyright 2012 Sam Hemelryk
4847 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4849 class css_style_paddingbottom extends css_style_padding {
4852 * Initialises this style
4854 * @param string $value The value of the style
4855 * @return css_style_paddingbottom
4857 public static function init($value) {
4858 return new css_style_paddingbottom('padding-bottom', $value);
4862 * Consolidates this style into a single padding style
4864 * @return string
4866 public function consolidate_to() {
4867 return 'padding';
4872 * A padding left style.
4874 * @package core
4875 * @subpackage cssoptimiser
4876 * @copyright 2012 Sam Hemelryk
4877 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4879 class css_style_paddingleft extends css_style_padding {
4882 * Initialises this style
4884 * @param string $value The value of the style
4885 * @return css_style_paddingleft
4887 public static function init($value) {
4888 return new css_style_paddingleft('padding-left', $value);
4892 * Consolidates this style into a single padding style
4894 * @return string
4896 public function consolidate_to() {
4897 return 'padding';
4902 * A cursor style.
4904 * @package core
4905 * @subpackage cssoptimiser
4906 * @copyright 2012 Sam Hemelryk
4907 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4909 class css_style_cursor extends css_style_generic {
4911 * Initialises a new cursor style
4912 * @param string $value
4913 * @return css_style_cursor
4915 public static function init($value) {
4916 return new css_style_cursor('cursor', $value);
4919 * Cleans the given value and returns it.
4921 * @param string $value
4922 * @return string
4924 protected function clean_value($value) {
4925 // Allowed values for the cursor style.
4926 $allowed = array('auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize',
4927 'pointer', 'progress', 's-resize', 'se-resize', 'sw-resize', 'text', 'w-resize', 'wait', 'inherit');
4928 // Has to be one of the allowed values of an image to use. Loosely match the image... doesn't need to be thorough.
4929 if (!in_array($value, $allowed) && !preg_match('#\.[a-zA-Z0-9_\-]{1,5}$#', $value)) {
4930 $this->set_error('Invalid or unexpected cursor value specified: '.$value);
4932 return trim($value);
4937 * A vertical alignment style.
4939 * @package core
4940 * @subpackage cssoptimiser
4941 * @copyright 2012 Sam Hemelryk
4942 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4944 class css_style_verticalalign extends css_style_generic {
4946 * Initialises a new vertical alignment style
4947 * @param string $value
4948 * @return css_style_verticalalign
4950 public static function init($value) {
4951 return new css_style_verticalalign('vertical-align', $value);
4954 * Cleans the given value and returns it.
4956 * @param string $value
4957 * @return string
4959 protected function clean_value($value) {
4960 $allowed = array('baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'inherit');
4961 if (!css_is_width($value) && !in_array($value, $allowed)) {
4962 $this->set_error('Invalid vertical-align value specified: '.$value);
4964 return trim($value);
4969 * A float style.
4971 * @package core
4972 * @subpackage cssoptimiser
4973 * @copyright 2012 Sam Hemelryk
4974 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4976 class css_style_float extends css_style_generic {
4978 * Initialises a new float style
4979 * @param string $value
4980 * @return css_style_float
4982 public static function init($value) {
4983 return new css_style_float('float', $value);
4986 * Cleans the given value and returns it.
4988 * @param string $value
4989 * @return string
4991 protected function clean_value($value) {
4992 $allowed = array('left', 'right', 'none', 'inherit');
4993 if (!css_is_width($value) && !in_array($value, $allowed)) {
4994 $this->set_error('Invalid float value specified: '.$value);
4996 return trim($value);