Moodle release 2.6.4
[moodle.git] / lib / csslib.php
blobdb8083e53715e4fd3ae4a567688bcc953476311e
1 <?php
2 // This file is part of Moodle - http://moodle.org/
3 //
4 // Moodle is free software: you can redistribute it and/or modify
5 // it under the terms of the GNU General Public License as published by
6 // the Free Software Foundation, either version 3 of the License, or
7 // (at your option) any later version.
8 //
9 // Moodle is distributed in the hope that it will be useful,
10 // but WITHOUT ANY WARRANTY; without even the implied warranty of
11 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 // GNU General Public License for more details.
14 // You should have received a copy of the GNU General Public License
15 // along with Moodle. If not, see <http://www.gnu.org/licenses/>.
17 /**
18 * This file contains CSS related class, and function for the CSS optimiser
20 * Please see the {@link css_optimiser} class for greater detail.
22 * @package core
23 * @subpackage cssoptimiser
24 * @copyright 2012 Sam Hemelryk
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 defined('MOODLE_INTERNAL') || die();
30 /**
31 * Stores CSS in a file at the given path.
33 * This function either succeeds or throws an exception.
35 * @param theme_config $theme The theme that the CSS belongs to.
36 * @param string $csspath The path to store the CSS at.
37 * @param array $cssfiles The CSS files to store.
38 * @param bool $chunk If set to true these files will be chunked to ensure
39 * that no one file contains more than 4095 selectors.
40 * @param string $chunkurl If the CSS is be chunked then we need to know the URL
41 * to use for the chunked files.
43 function css_store_css(theme_config $theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) {
44 global $CFG;
46 $css = '';
47 foreach ($cssfiles as $file) {
48 $css .= file_get_contents($file)."\n";
51 // Check if both the CSS optimiser is enabled and the theme supports it.
52 if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) {
53 // This is an experimental feature introduced in Moodle 2.3
54 // The CSS optimiser organises the CSS in order to reduce the overall number
55 // of rules and styles being sent to the client. It does this by collating
56 // the CSS before it is cached removing excess styles and rules and stripping
57 // out any extraneous content such as comments and empty rules.
58 $optimiser = new css_optimiser;
59 $css = $theme->post_process($css);
60 $css = $optimiser->process($css);
62 // If cssoptimisestats is set then stats from the optimisation are collected
63 // and output at the beginning of the CSS.
64 if (!empty($CFG->cssoptimiserstats)) {
65 $css = $optimiser->output_stats_css().$css;
67 } else {
68 // This is the default behaviour.
69 // The cssoptimise setting was introduced in Moodle 2.3 and will hopefully
70 // in the future be changed from an experimental setting to the default.
71 // The css_minify_css will method will use the Minify library remove
72 // comments, additional whitespace and other minor measures to reduce the
73 // the overall CSS being sent.
74 // However it has the distinct disadvantage of having to minify the CSS
75 // before running the post process functions. Potentially things may break
76 // here if theme designers try to push things with CSS post processing.
77 $css = $theme->post_process($css);
78 $css = core_minify::css($css);
81 clearstatcache();
82 if (!file_exists(dirname($csspath))) {
83 @mkdir(dirname($csspath), $CFG->directorypermissions, true);
86 // Prevent serving of incomplete file from concurrent request,
87 // the rename() should be more atomic than fwrite().
88 ignore_user_abort(true);
90 // First up write out the single file for all those using decent browsers.
91 css_write_file($csspath, $css);
93 if ($chunk) {
94 // If we need to chunk the CSS for browsers that are sub-par.
95 $css = css_chunk_by_selector_count($css, $chunkurl);
96 $files = count($css);
97 $count = 1;
98 foreach ($css as $content) {
99 if ($count === $files) {
100 // If there is more than one file and this IS the last file.
101 $filename = preg_replace('#\.css$#', '.0.css', $csspath);
102 } else {
103 // If there is more than one file and this is not the last file.
104 $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
106 $count++;
107 css_write_file($filename, $content);
111 ignore_user_abort(false);
112 if (connection_aborted()) {
113 die;
118 * Writes a CSS file.
120 * @param string $filename
121 * @param string $content
123 function css_write_file($filename, $content) {
124 global $CFG;
125 if ($fp = fopen($filename.'.tmp', 'xb')) {
126 fwrite($fp, $content);
127 fclose($fp);
128 rename($filename.'.tmp', $filename);
129 @chmod($filename, $CFG->filepermissions);
130 @unlink($filename.'.tmp'); // Just in case anything fails.
135 * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
137 * The chunking will not split a group of selectors, or a media query. That means that
138 * if n > $maxselectors and there are n selectors grouped together,
139 * they will not be chunked and you could end up with more selectors than desired.
140 * The same applies for a media query that has more than n selectors.
142 * Also, as we do not split group of selectors or media queries, the chunking might
143 * not be as optimal as it could be, having files with less selectors than it could
144 * potentially contain.
146 * String functions used here are not compliant with unicode characters. But that is
147 * not an issue as the syntax of CSS is using ASCII codes. Even if we have unicode
148 * characters in comments, or in the property 'content: ""', it will behave correcly.
150 * Please note that this strips out the comments if chunking happens.
152 * @param string $css The CSS to chunk.
153 * @param string $importurl The URL to use for import statements.
154 * @param int $maxselectors The number of selectors to limit a chunk to.
155 * @param int $buffer Not used any more.
156 * @return array An array of CSS chunks.
158 function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
160 // Check if we need to chunk this CSS file.
161 $count = substr_count($css, ',') + substr_count($css, '{');
162 if ($count < $maxselectors) {
163 // The number of selectors is less then the max - we're fine.
164 return array($css);
167 $chunks = array(); // The final chunks.
168 $offsets = array(); // The indexes to chunk at.
169 $offset = 0; // The current offset.
170 $selectorcount = 0; // The number of selectors since the last split.
171 $lastvalidoffset = 0; // The last valid index to split at.
172 $lastvalidoffsetselectorcount = 0; // The number of selectors used at the time were could split.
173 $inrule = 0; // The number of rules we are in, should not be greater than 1.
174 $inmedia = false; // Whether or not we are in a media query.
175 $mediacoming = false; // Whether or not we are expeting a media query.
176 $currentoffseterror = null; // Not null when we have recorded an error for the current split.
177 $offseterrors = array(); // The offsets where we found errors.
179 // Remove the comments. Because it's easier, safer and probably a lot of other good reasons.
180 $css = preg_replace('#/\*(.*?)\*/#s', '', $css);
181 $strlen = strlen($css);
183 // Walk through the CSS content character by character.
184 for ($i = 1; $i <= $strlen; $i++) {
185 $char = $css[$i - 1];
186 $offset = $i;
188 // Is that a media query that I see coming towards us?
189 if ($char === '@') {
190 if (!$inmedia && substr($css, $offset, 5) === 'media') {
191 $mediacoming = true;
195 // So we are entering a rule or a media query...
196 if ($char === '{') {
197 if ($mediacoming) {
198 $inmedia = true;
199 $mediacoming = false;
200 } else {
201 $inrule++;
202 $selectorcount++;
206 // Let's count the number of selectors, but only if we are not in a rule, or in
207 // the definition of a media query, as they can contain commas too.
208 if (!$mediacoming && !$inrule && $char === ',') {
209 $selectorcount++;
212 // We reached the end of something.
213 if ($char === '}') {
214 // Oh, we are in a media query.
215 if ($inmedia) {
216 if (!$inrule) {
217 // This is the end of the media query.
218 $inmedia = false;
219 } else {
220 // We were in a rule, in the media query.
221 $inrule--;
223 } else {
224 $inrule--;
225 // Handle stupid broken CSS where there are too many } brackets,
226 // as this can cause it to break (with chunking) where it would
227 // coincidentally have worked otherwise.
228 if ($inrule < 0) {
229 $inrule = 0;
233 // We are not in a media query, and there is no pending rule, it is safe to split here.
234 if (!$inmedia && !$inrule) {
235 $lastvalidoffset = $offset;
236 $lastvalidoffsetselectorcount = $selectorcount;
240 // Alright, this is splitting time...
241 if ($selectorcount > $maxselectors) {
242 if (!$lastvalidoffset) {
243 // We must have reached more selectors into one set than we were allowed. That means that either
244 // the chunk size value is too small, or that we have a gigantic group of selectors, or that a media
245 // query contains more selectors than the chunk size. We have to ignore this because we do not
246 // support split inside a group of selectors or media query.
247 if ($currentoffseterror === null) {
248 $currentoffseterror = $offset;
249 $offseterrors[] = $currentoffseterror;
251 } else {
252 // We identify the offset to split at and reset the number of selectors found from there.
253 $offsets[] = $lastvalidoffset;
254 $selectorcount = $selectorcount - $lastvalidoffsetselectorcount;
255 $lastvalidoffset = 0;
256 $currentoffseterror = null;
261 // Report offset errors.
262 if (!empty($offseterrors)) {
263 debugging('Could not find a safe place to split at offset(s): ' . implode(', ', $offseterrors) . '. Those were ignored.',
264 DEBUG_DEVELOPER);
267 // Now that we have got the offets, we can chunk the CSS.
268 $offsetcount = count($offsets);
269 foreach ($offsets as $key => $index) {
270 $start = 0;
271 if ($key > 0) {
272 $start = $offsets[$key - 1];
274 // From somewhere up to the offset.
275 $chunks[] = substr($css, $start, $index - $start);
277 // Add the last chunk (if there is one), from the last offset to the end of the string.
278 if (end($offsets) != $strlen) {
279 $chunks[] = substr($css, end($offsets));
282 // The array $chunks now contains CSS split into perfect sized chunks.
283 // Import statements can only appear at the very top of a CSS file.
284 // Imported sheets are applied in the the order they are imported and
285 // are followed by the contents of the CSS.
286 // This is terrible for performance.
287 // It means we must put the import statements at the top of the last chunk
288 // to ensure that things are always applied in the correct order.
289 // This way the chunked files are included in the order they were chunked
290 // followed by the contents of the final chunk in the actual sheet.
291 $importcss = '';
292 $slashargs = strpos($importurl, '.php?') === false;
293 $parts = count($chunks);
294 for ($i = 1; $i < $parts; $i++) {
295 if ($slashargs) {
296 $importcss .= "@import url({$importurl}/chunk{$i});\n";
297 } else {
298 $importcss .= "@import url({$importurl}&chunk={$i});\n";
301 $importcss .= end($chunks);
302 $chunks[key($chunks)] = $importcss;
304 return $chunks;
308 * Sends a cached CSS file
310 * This function sends the cached CSS file. Remember it is generated on the first
311 * request, then optimised/minified, and finally cached for serving.
313 * @param string $csspath The path to the CSS file we want to serve.
314 * @param string $etag The revision to make sure we utilise any caches.
316 function css_send_cached_css($csspath, $etag) {
317 // 60 days only - the revision may get incremented quite often.
318 $lifetime = 60*60*24*60;
320 header('Etag: "'.$etag.'"');
321 header('Content-Disposition: inline; filename="styles.php"');
322 header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
323 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
324 header('Pragma: ');
325 header('Cache-Control: public, max-age='.$lifetime);
326 header('Accept-Ranges: none');
327 header('Content-Type: text/css; charset=utf-8');
328 if (!min_enable_zlib_compression()) {
329 header('Content-Length: '.filesize($csspath));
332 readfile($csspath);
333 die;
337 * Sends CSS directly without caching it.
339 * This function takes a raw CSS string, optimises it if required, and then
340 * serves it.
341 * Turning both themedesignermode and CSS optimiser on at the same time is awful
342 * for performance because of the optimiser running here. However it was done so
343 * that theme designers could utilise the optimised output during development to
344 * help them optimise their CSS... not that they should write lazy CSS.
346 * @param string $css
348 function css_send_uncached_css($css) {
349 header('Content-Disposition: inline; filename="styles_debug.php"');
350 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
351 header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT');
352 header('Pragma: ');
353 header('Accept-Ranges: none');
354 header('Content-Type: text/css; charset=utf-8');
356 if (is_array($css)) {
357 $css = implode("\n\n", $css);
359 echo $css;
360 die;
364 * Send file not modified headers
366 * @param int $lastmodified
367 * @param string $etag
369 function css_send_unmodified($lastmodified, $etag) {
370 // 60 days only - the revision may get incremented quite often.
371 $lifetime = 60*60*24*60;
372 header('HTTP/1.1 304 Not Modified');
373 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
374 header('Cache-Control: public, max-age='.$lifetime);
375 header('Content-Type: text/css; charset=utf-8');
376 header('Etag: "'.$etag.'"');
377 if ($lastmodified) {
378 header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
380 die;
384 * Sends a 404 message about CSS not being found.
386 function css_send_css_not_found() {
387 header('HTTP/1.0 404 not found');
388 die('CSS was not found, sorry.');
392 * Determines if the given value is a valid CSS colour.
394 * A CSS colour can be one of the following:
395 * - Hex colour: #AA66BB
396 * - RGB colour: rgb(0-255, 0-255, 0-255)
397 * - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1)
398 * - HSL colour: hsl(0-360, 0-100%, 0-100%)
399 * - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1)
401 * Or a recognised browser colour mapping {@link css_optimiser::$htmlcolours}
403 * @param string $value The colour value to check
404 * @return bool
406 function css_is_colour($value) {
407 $value = trim($value);
409 $hex = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/';
410 $rgb = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i';
411 $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
412 $hsl = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i';
413 $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
415 if (in_array(strtolower($value), array('inherit'))) {
416 return true;
417 } else if (preg_match($hex, $value)) {
418 return true;
419 } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
420 return true;
421 } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
422 // It is an RGB colour.
423 return true;
424 } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
425 // It is an RGBA colour.
426 return true;
427 } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
428 // It is an HSL colour.
429 return true;
430 } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
431 // It is an HSLA colour.
432 return true;
434 // Doesn't look like a colour.
435 return false;
439 * Returns true is the passed value looks like a CSS width.
440 * In order to pass this test the value must be purely numerical or end with a
441 * valid CSS unit term.
443 * @param string|int $value
444 * @return boolean
446 function css_is_width($value) {
447 $value = trim($value);
448 if (in_array(strtolower($value), array('auto', 'inherit'))) {
449 return true;
451 if ((string)$value === '0' || preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) {
452 return true;
454 return false;
458 * A simple sorting function to sort two array values on the number of items they contain
460 * @param array $a
461 * @param array $b
462 * @return int
464 function css_sort_by_count(array $a, array $b) {
465 $a = count($a);
466 $b = count($b);
467 if ($a == $b) {
468 return 0;
470 return ($a > $b) ? -1 : 1;
474 * A basic CSS optimiser that strips out unwanted things and then processes CSS organising and cleaning styles.
476 * This CSS optimiser works by reading through a CSS string one character at a
477 * time and building an object structure of the CSS.
478 * As part of that processing styles are expanded out as much as they can be to
479 * ensure we collect all mappings, at the end of the processing those styles are
480 * then combined into an optimised form to keep them as short as possible.
482 * @package core
483 * @subpackage cssoptimiser
484 * @copyright 2012 Sam Hemelryk
485 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
487 class css_optimiser {
490 * Used when the processor is about to start processing.
491 * Processing states. Used internally.
493 const PROCESSING_START = 0;
496 * Used when the processor is currently processing a selector.
497 * Processing states. Used internally.
499 const PROCESSING_SELECTORS = 0;
502 * Used when the processor is currently processing a style.
503 * Processing states. Used internally.
505 const PROCESSING_STYLES = 1;
508 * Used when the processor is currently processing a comment.
509 * Processing states. Used internally.
511 const PROCESSING_COMMENT = 2;
514 * Used when the processor is currently processing an @ rule.
515 * Processing states. Used internally.
517 const PROCESSING_ATRULE = 3;
520 * The raw string length before optimisation.
521 * Stats variables set during and after processing
522 * @var int
524 protected $rawstrlen = 0;
527 * The number of comments that were removed during optimisation.
528 * Stats variables set during and after processing
529 * @var int
531 protected $commentsincss = 0;
534 * The number of rules in the CSS before optimisation.
535 * Stats variables set during and after processing
536 * @var int
538 protected $rawrules = 0;
541 * The number of selectors using in CSS rules before optimisation.
542 * Stats variables set during and after processing
543 * @var int
545 protected $rawselectors = 0;
548 * The string length after optimisation.
549 * Stats variables set during and after processing
550 * @var int
552 protected $optimisedstrlen = 0;
555 * The number of rules after optimisation.
556 * Stats variables set during and after processing
557 * @var int
559 protected $optimisedrules = 0;
562 * The number of selectors used in rules after optimisation.
563 * Stats variables set during and after processing
564 * @var int
566 protected $optimisedselectors = 0;
569 * The start time of the optimisation.
570 * Stats variables set during and after processing
571 * @var int
573 protected $timestart = 0;
576 * The end time of the optimisation.
577 * Stats variables set during and after processing
578 * @var int
580 protected $timecomplete = 0;
583 * Will be set to any errors that may have occured during processing.
584 * This is updated only at the end of processing NOT during.
586 * @var array
588 protected $errors = array();
591 * Processes incoming CSS optimising it and then returning it.
593 * @param string $css The raw CSS to optimise
594 * @return string The optimised CSS
596 public function process($css) {
597 // Easiest win there is.
598 $css = trim($css);
600 $this->reset_stats();
601 $this->timestart = microtime(true);
602 $this->rawstrlen = strlen($css);
604 // Don't try to process files with no content... it just doesn't make sense.
605 // But we should produce an error for them, an empty CSS file will lead to a
606 // useless request for those running theme designer mode.
607 if ($this->rawstrlen === 0) {
608 $this->errors[] = 'Skipping file as it has no content.';
609 return '';
612 // First up we need to remove all line breaks - this allows us to instantly
613 // reduce our processing requirements and as we will process everything
614 // into a new structure there's really nothing lost.
615 $css = preg_replace('#\r?\n#', ' ', $css);
617 // Next remove the comments... no need to them in an optimised world and
618 // knowing they're all gone allows us to REALLY make our processing simpler.
619 $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
621 $medias = array(
622 'all' => new css_media()
624 $imports = array();
625 $charset = false;
626 // Keyframes are used for CSS animation they will be processed right at the very end.
627 $keyframes = array();
629 $currentprocess = self::PROCESSING_START;
630 $currentrule = css_rule::init();
631 $currentselector = css_selector::init();
632 $inquotes = false; // ' or "
633 $inbraces = false; // {
634 $inbrackets = false; // [
635 $inparenthesis = false; // (
636 /* @var css_media $currentmedia */
637 $currentmedia = $medias['all'];
638 $currentatrule = null;
639 $suspectatrule = false;
641 $buffer = '';
642 $char = null;
644 // Next we are going to iterate over every single character in $css.
645 // This is why we removed line breaks and comments!
646 for ($i = 0; $i < $this->rawstrlen; $i++) {
647 $lastchar = $char;
648 $char = substr($css, $i, 1);
649 if ($char == '@' && $buffer == '') {
650 $suspectatrule = true;
652 switch ($currentprocess) {
653 // Start processing an @ rule e.g. @media, @page, @keyframes.
654 case self::PROCESSING_ATRULE:
655 switch ($char) {
656 case ';':
657 if (!$inbraces) {
658 $buffer .= $char;
659 if ($currentatrule == 'import') {
660 $imports[] = $buffer;
661 $currentprocess = self::PROCESSING_SELECTORS;
662 } else if ($currentatrule == 'charset') {
663 $charset = $buffer;
664 $currentprocess = self::PROCESSING_SELECTORS;
667 if ($currentatrule !== 'media') {
668 $buffer = '';
669 $currentatrule = false;
671 // Continue 1: The switch processing chars
672 // Continue 2: The switch processing the state
673 // Continue 3: The for loop.
674 continue 3;
675 case '{':
676 $regexmediabasic = '#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#';
677 $regexadvmedia = '#\s*@media\s*([^{]+)#';
678 $regexkeyframes = '#@((\-moz\-|\-webkit\-|\-ms\-|\-o\-)?keyframes)\s*([^\s]+)#';
680 if ($currentatrule == 'media' && preg_match($regexmediabasic, $buffer, $matches)) {
681 // Basic media declaration.
682 $mediatypes = str_replace(' ', '', $matches[1]);
683 if (!array_key_exists($mediatypes, $medias)) {
684 $medias[$mediatypes] = new css_media($mediatypes);
686 $currentmedia = $medias[$mediatypes];
687 $currentprocess = self::PROCESSING_SELECTORS;
688 $buffer = '';
689 } else if ($currentatrule == 'media' && preg_match($regexadvmedia, $buffer, $matches)) {
690 // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/.
691 $mediatypes = $matches[1];
692 $hash = md5($mediatypes);
693 $medias[$hash] = new css_media($mediatypes);
694 $currentmedia = $medias[$hash];
695 $currentprocess = self::PROCESSING_SELECTORS;
696 $buffer = '';
697 } else if ($currentatrule == 'keyframes' && preg_match($regexkeyframes, $buffer, $matches)) {
698 // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow
699 // them to be overridden to ensure we don't mess anything up. (means we keep everything in order).
700 $keyframefor = $matches[1];
701 $keyframename = $matches[3];
702 $keyframe = new css_keyframe($keyframefor, $keyframename);
703 $keyframes[] = $keyframe;
704 $currentmedia = $keyframe;
705 $currentprocess = self::PROCESSING_SELECTORS;
706 $buffer = '';
708 // Continue 1: The switch processing chars
709 // Continue 2: The switch processing the state
710 // Continue 3: The for loop.
711 continue 3;
713 break;
714 // Start processing selectors.
715 case self::PROCESSING_START:
716 case self::PROCESSING_SELECTORS:
717 $regexatrule = '#@(media|import|charset|(\-moz\-|\-webkit\-|\-ms\-|\-o\-)?(keyframes))\s*#';
718 switch ($char) {
719 case '[':
720 $inbrackets ++;
721 $buffer .= $char;
722 // Continue 1: The switch processing chars
723 // Continue 2: The switch processing the state
724 // Continue 3: The for loop.
725 continue 3;
726 case ']':
727 $inbrackets --;
728 $buffer .= $char;
729 // Continue 1: The switch processing chars
730 // Continue 2: The switch processing the state
731 // Continue 3: The for loop.
732 continue 3;
733 case ' ':
734 if ($inbrackets) {
735 // Continue 1: The switch processing chars
736 // Continue 2: The switch processing the state
737 // Continue 3: The for loop.
738 continue 3;
740 if (!empty($buffer)) {
741 // Check for known @ rules.
742 if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
743 $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
744 $currentprocess = self::PROCESSING_ATRULE;
745 $buffer .= $char;
746 } else {
747 $currentselector->add($buffer);
748 $buffer = '';
751 $suspectatrule = false;
752 // Continue 1: The switch processing chars
753 // Continue 2: The switch processing the state
754 // Continue 3: The for loop.
755 continue 3;
756 case '{':
757 if ($inbrackets) {
758 // Continue 1: The switch processing chars
759 // Continue 2: The switch processing the state
760 // Continue 3: The for loop.
761 continue 3;
763 // Check for known @ rules.
764 if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
765 // Ahh we've been in an @rule, lets rewind one and have the @rule case process this.
766 $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
767 $currentprocess = self::PROCESSING_ATRULE;
768 $i--;
769 $suspectatrule = false;
770 // Continue 1: The switch processing chars
771 // Continue 2: The switch processing the state
772 // Continue 3: The for loop.
773 continue 3;
775 if ($buffer !== '') {
776 $currentselector->add($buffer);
778 $currentrule->add_selector($currentselector);
779 $currentselector = css_selector::init();
780 $currentprocess = self::PROCESSING_STYLES;
782 $buffer = '';
783 // Continue 1: The switch processing chars
784 // Continue 2: The switch processing the state
785 // Continue 3: The for loop.
786 continue 3;
787 case '}':
788 if ($inbrackets) {
789 // Continue 1: The switch processing chars
790 // Continue 2: The switch processing the state
791 // Continue 3: The for loop.
792 continue 3;
794 if ($currentatrule == 'media') {
795 $currentmedia = $medias['all'];
796 $currentatrule = false;
797 $buffer = '';
798 } else if (strpos($currentatrule, 'keyframes') !== false) {
799 $currentmedia = $medias['all'];
800 $currentatrule = false;
801 $buffer = '';
803 // Continue 1: The switch processing chars
804 // Continue 2: The switch processing the state
805 // Continue 3: The for loop.
806 continue 3;
807 case ',':
808 if ($inbrackets) {
809 // Continue 1: The switch processing chars
810 // Continue 2: The switch processing the state
811 // Continue 3: The for loop.
812 continue 3;
814 $currentselector->add($buffer);
815 $currentrule->add_selector($currentselector);
816 $currentselector = css_selector::init();
817 $buffer = '';
818 // Continue 1: The switch processing chars
819 // Continue 2: The switch processing the state
820 // Continue 3: The for loop.
821 continue 3;
823 break;
824 // Start processing styles.
825 case self::PROCESSING_STYLES:
826 if ($char == '"' || $char == "'") {
827 if ($inquotes === false) {
828 $inquotes = $char;
830 if ($inquotes === $char && $lastchar !== '\\') {
831 $inquotes = false;
834 if ($inquotes) {
835 $buffer .= $char;
836 continue 2;
838 switch ($char) {
839 case ';':
840 if ($inparenthesis) {
841 $buffer .= $char;
842 // Continue 1: The switch processing chars
843 // Continue 2: The switch processing the state
844 // Continue 3: The for loop.
845 continue 3;
847 $currentrule->add_style($buffer);
848 $buffer = '';
849 $inquotes = false;
850 // Continue 1: The switch processing chars
851 // Continue 2: The switch processing the state
852 // Continue 3: The for loop.
853 continue 3;
854 case '}':
855 $currentrule->add_style($buffer);
856 $this->rawselectors += $currentrule->get_selector_count();
858 $currentmedia->add_rule($currentrule);
860 $currentrule = css_rule::init();
861 $currentprocess = self::PROCESSING_SELECTORS;
862 $this->rawrules++;
863 $buffer = '';
864 $inquotes = false;
865 $inparenthesis = false;
866 // Continue 1: The switch processing chars
867 // Continue 2: The switch processing the state
868 // Continue 3: The for loop.
869 continue 3;
870 case '(':
871 $inparenthesis = true;
872 $buffer .= $char;
873 // Continue 1: The switch processing chars
874 // Continue 2: The switch processing the state
875 // Continue 3: The for loop.
876 continue 3;
877 case ')':
878 $inparenthesis = false;
879 $buffer .= $char;
880 // Continue 1: The switch processing chars
881 // Continue 2: The switch processing the state
882 // Continue 3: The for loop.
883 continue 3;
885 break;
887 $buffer .= $char;
890 foreach ($medias as $media) {
891 $this->optimise($media);
893 $css = $this->produce_css($charset, $imports, $medias, $keyframes);
895 $this->timecomplete = microtime(true);
896 return trim($css);
900 * Produces CSS for the given charset, imports, media, and keyframes
901 * @param string $charset
902 * @param array $imports
903 * @param css_media[] $medias
904 * @param css_keyframe[] $keyframes
905 * @return string
907 protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
908 $css = '';
909 if (!empty($charset)) {
910 $imports[] = $charset;
912 if (!empty($imports)) {
913 $css .= implode("\n", $imports);
914 $css .= "\n\n";
917 $cssreset = array();
918 $cssstandard = array();
919 $csskeyframes = array();
921 // Process each media declaration individually.
922 foreach ($medias as $media) {
923 // If this declaration applies to all media types.
924 if (in_array('all', $media->get_types())) {
925 // Collect all rules that represet reset rules and remove them from the media object at the same time.
926 // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they
927 // can't end up out of order because of optimisation.
928 $resetrules = $media->get_reset_rules(true);
929 if (!empty($resetrules)) {
930 $cssreset[] = css_writer::media('all', $resetrules);
933 // Get the standard cSS.
934 $cssstandard[] = $media->out();
937 // Finally if there are any keyframe declarations process them now.
938 if (count($keyframes) > 0) {
939 foreach ($keyframes as $keyframe) {
940 $this->optimisedrules += $keyframe->count_rules();
941 $this->optimisedselectors += $keyframe->count_selectors();
942 if ($keyframe->has_errors()) {
943 $this->errors += $keyframe->get_errors();
945 $csskeyframes[] = $keyframe->out();
949 // Join it all together.
950 $css .= join('', $cssreset);
951 $css .= join('', $cssstandard);
952 $css .= join('', $csskeyframes);
954 // Record the strlenght of the now optimised CSS.
955 $this->optimisedstrlen = strlen($css);
957 // Return the now produced CSS.
958 return $css;
962 * Optimises the CSS rules within a rule collection of one form or another
964 * @param css_rule_collection $media
965 * @return void This function acts in reference
967 protected function optimise(css_rule_collection $media) {
968 $media->organise_rules_by_selectors();
969 $this->optimisedrules += $media->count_rules();
970 $this->optimisedselectors += $media->count_selectors();
971 if ($media->has_errors()) {
972 $this->errors += $media->get_errors();
977 * Returns an array of stats from the last processing run
978 * @return string
980 public function get_stats() {
981 $stats = array(
982 'timestart' => $this->timestart,
983 'timecomplete' => $this->timecomplete,
984 'timetaken' => round($this->timecomplete - $this->timestart, 4),
985 'commentsincss' => $this->commentsincss,
986 'rawstrlen' => $this->rawstrlen,
987 'rawselectors' => $this->rawselectors,
988 'rawrules' => $this->rawrules,
989 'optimisedstrlen' => $this->optimisedstrlen,
990 'optimisedrules' => $this->optimisedrules,
991 'optimisedselectors' => $this->optimisedselectors,
992 'improvementstrlen' => '-',
993 'improvementrules' => '-',
994 'improvementselectors' => '-',
996 // Avoid division by 0 errors by checking we have valid raw values.
997 if ($this->rawstrlen > 0) {
998 $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%';
1000 if ($this->rawrules > 0) {
1001 $stats['improvementrules'] = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%';
1003 if ($this->rawselectors > 0) {
1004 $stats['improvementselectors'] = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%';
1006 return $stats;
1010 * Returns true if any errors have occured during processing
1012 * @return bool
1014 public function has_errors() {
1015 return !empty($this->errors);
1019 * Returns an array of errors that have occured
1021 * @param bool $clear If set to true the errors will be cleared after being returned.
1022 * @return array
1024 public function get_errors($clear = false) {
1025 $errors = $this->errors;
1026 if ($clear) {
1027 // Reset the error array.
1028 $this->errors = array();
1030 return $errors;
1034 * Returns any errors as a string that can be included in CSS.
1036 * @return string
1038 public function output_errors_css() {
1039 $computedcss = "/****************************************\n";
1040 $computedcss .= " *--- Errors found during processing ----\n";
1041 foreach ($this->errors as $error) {
1042 $computedcss .= preg_replace('#^#m', '* ', $error);
1044 $computedcss .= " ****************************************/\n\n";
1045 return $computedcss;
1049 * Returns a string to display stats about the last generation within CSS output
1051 * @return string
1053 public function output_stats_css() {
1055 $computedcss = "/****************************************\n";
1056 $computedcss .= " *------- CSS Optimisation stats --------\n";
1058 if ($this->rawstrlen === 0) {
1059 $computedcss .= " File not processed as it has no content /\n\n";
1060 $computedcss .= " ****************************************/\n\n";
1061 return $computedcss;
1062 } else if ($this->rawrules === 0) {
1063 $computedcss .= " File contained no rules to be processed /\n\n";
1064 $computedcss .= " ****************************************/\n\n";
1065 return $computedcss;
1068 $stats = $this->get_stats();
1070 $computedcss .= " * ".date('r')."\n";
1071 $computedcss .= " * {$stats['commentsincss']} \t comments removed\n";
1072 $computedcss .= " * Optimisation took {$stats['timetaken']} seconds\n";
1073 $computedcss .= " *--------------- before ----------------\n";
1074 $computedcss .= " * {$stats['rawstrlen']} \t chars read in\n";
1075 $computedcss .= " * {$stats['rawrules']} \t rules read in\n";
1076 $computedcss .= " * {$stats['rawselectors']} \t total selectors\n";
1077 $computedcss .= " *---------------- after ----------------\n";
1078 $computedcss .= " * {$stats['optimisedstrlen']} \t chars once optimized\n";
1079 $computedcss .= " * {$stats['optimisedrules']} \t optimized rules\n";
1080 $computedcss .= " * {$stats['optimisedselectors']} \t total selectors once optimized\n";
1081 $computedcss .= " *---------------- stats ----------------\n";
1082 $computedcss .= " * {$stats['improvementstrlen']} \t reduction in chars\n";
1083 $computedcss .= " * {$stats['improvementrules']} \t reduction in rules\n";
1084 $computedcss .= " * {$stats['improvementselectors']} \t reduction in selectors\n";
1085 $computedcss .= " ****************************************/\n\n";
1087 return $computedcss;
1091 * Resets the stats ready for another fresh processing
1093 public function reset_stats() {
1094 $this->commentsincss = 0;
1095 $this->optimisedrules = 0;
1096 $this->optimisedselectors = 0;
1097 $this->optimisedstrlen = 0;
1098 $this->rawrules = 0;
1099 $this->rawselectors = 0;
1100 $this->rawstrlen = 0;
1101 $this->timecomplete = 0;
1102 $this->timestart = 0;
1106 * An array of the common HTML colours that are supported by most browsers.
1108 * This reference table is used to allow us to unify colours, and will aid
1109 * us in identifying buggy CSS using unsupported colours.
1111 * @var string[]
1113 public static $htmlcolours = array(
1114 'aliceblue' => '#F0F8FF',
1115 'antiquewhite' => '#FAEBD7',
1116 'aqua' => '#00FFFF',
1117 'aquamarine' => '#7FFFD4',
1118 'azure' => '#F0FFFF',
1119 'beige' => '#F5F5DC',
1120 'bisque' => '#FFE4C4',
1121 'black' => '#000000',
1122 'blanchedalmond' => '#FFEBCD',
1123 'blue' => '#0000FF',
1124 'blueviolet' => '#8A2BE2',
1125 'brown' => '#A52A2A',
1126 'burlywood' => '#DEB887',
1127 'cadetblue' => '#5F9EA0',
1128 'chartreuse' => '#7FFF00',
1129 'chocolate' => '#D2691E',
1130 'coral' => '#FF7F50',
1131 'cornflowerblue' => '#6495ED',
1132 'cornsilk' => '#FFF8DC',
1133 'crimson' => '#DC143C',
1134 'cyan' => '#00FFFF',
1135 'darkblue' => '#00008B',
1136 'darkcyan' => '#008B8B',
1137 'darkgoldenrod' => '#B8860B',
1138 'darkgray' => '#A9A9A9',
1139 'darkgrey' => '#A9A9A9',
1140 'darkgreen' => '#006400',
1141 'darkKhaki' => '#BDB76B',
1142 'darkmagenta' => '#8B008B',
1143 'darkolivegreen' => '#556B2F',
1144 'arkorange' => '#FF8C00',
1145 'darkorchid' => '#9932CC',
1146 'darkred' => '#8B0000',
1147 'darksalmon' => '#E9967A',
1148 'darkseagreen' => '#8FBC8F',
1149 'darkslateblue' => '#483D8B',
1150 'darkslategray' => '#2F4F4F',
1151 'darkslategrey' => '#2F4F4F',
1152 'darkturquoise' => '#00CED1',
1153 'darkviolet' => '#9400D3',
1154 'deeppink' => '#FF1493',
1155 'deepskyblue' => '#00BFFF',
1156 'dimgray' => '#696969',
1157 'dimgrey' => '#696969',
1158 'dodgerblue' => '#1E90FF',
1159 'firebrick' => '#B22222',
1160 'floralwhite' => '#FFFAF0',
1161 'forestgreen' => '#228B22',
1162 'fuchsia' => '#FF00FF',
1163 'gainsboro' => '#DCDCDC',
1164 'ghostwhite' => '#F8F8FF',
1165 'gold' => '#FFD700',
1166 'goldenrod' => '#DAA520',
1167 'gray' => '#808080',
1168 'grey' => '#808080',
1169 'green' => '#008000',
1170 'greenyellow' => '#ADFF2F',
1171 'honeydew' => '#F0FFF0',
1172 'hotpink' => '#FF69B4',
1173 'indianred ' => '#CD5C5C',
1174 'indigo ' => '#4B0082',
1175 'ivory' => '#FFFFF0',
1176 'khaki' => '#F0E68C',
1177 'lavender' => '#E6E6FA',
1178 'lavenderblush' => '#FFF0F5',
1179 'lawngreen' => '#7CFC00',
1180 'lemonchiffon' => '#FFFACD',
1181 'lightblue' => '#ADD8E6',
1182 'lightcoral' => '#F08080',
1183 'lightcyan' => '#E0FFFF',
1184 'lightgoldenrodyellow' => '#FAFAD2',
1185 'lightgray' => '#D3D3D3',
1186 'lightgrey' => '#D3D3D3',
1187 'lightgreen' => '#90EE90',
1188 'lightpink' => '#FFB6C1',
1189 'lightsalmon' => '#FFA07A',
1190 'lightseagreen' => '#20B2AA',
1191 'lightskyblue' => '#87CEFA',
1192 'lightslategray' => '#778899',
1193 'lightslategrey' => '#778899',
1194 'lightsteelblue' => '#B0C4DE',
1195 'lightyellow' => '#FFFFE0',
1196 'lime' => '#00FF00',
1197 'limegreen' => '#32CD32',
1198 'linen' => '#FAF0E6',
1199 'magenta' => '#FF00FF',
1200 'maroon' => '#800000',
1201 'mediumaquamarine' => '#66CDAA',
1202 'mediumblue' => '#0000CD',
1203 'mediumorchid' => '#BA55D3',
1204 'mediumpurple' => '#9370D8',
1205 'mediumseagreen' => '#3CB371',
1206 'mediumslateblue' => '#7B68EE',
1207 'mediumspringgreen' => '#00FA9A',
1208 'mediumturquoise' => '#48D1CC',
1209 'mediumvioletred' => '#C71585',
1210 'midnightblue' => '#191970',
1211 'mintcream' => '#F5FFFA',
1212 'mistyrose' => '#FFE4E1',
1213 'moccasin' => '#FFE4B5',
1214 'navajowhite' => '#FFDEAD',
1215 'navy' => '#000080',
1216 'oldlace' => '#FDF5E6',
1217 'olive' => '#808000',
1218 'olivedrab' => '#6B8E23',
1219 'orange' => '#FFA500',
1220 'orangered' => '#FF4500',
1221 'orchid' => '#DA70D6',
1222 'palegoldenrod' => '#EEE8AA',
1223 'palegreen' => '#98FB98',
1224 'paleturquoise' => '#AFEEEE',
1225 'palevioletred' => '#D87093',
1226 'papayawhip' => '#FFEFD5',
1227 'peachpuff' => '#FFDAB9',
1228 'peru' => '#CD853F',
1229 'pink' => '#FFC0CB',
1230 'plum' => '#DDA0DD',
1231 'powderblue' => '#B0E0E6',
1232 'purple' => '#800080',
1233 'red' => '#FF0000',
1234 'rosybrown' => '#BC8F8F',
1235 'royalblue' => '#4169E1',
1236 'saddlebrown' => '#8B4513',
1237 'salmon' => '#FA8072',
1238 'sandybrown' => '#F4A460',
1239 'seagreen' => '#2E8B57',
1240 'seashell' => '#FFF5EE',
1241 'sienna' => '#A0522D',
1242 'silver' => '#C0C0C0',
1243 'skyblue' => '#87CEEB',
1244 'slateblue' => '#6A5ACD',
1245 'slategray' => '#708090',
1246 'slategrey' => '#708090',
1247 'snow' => '#FFFAFA',
1248 'springgreen' => '#00FF7F',
1249 'steelblue' => '#4682B4',
1250 'tan' => '#D2B48C',
1251 'teal' => '#008080',
1252 'thistle' => '#D8BFD8',
1253 'tomato' => '#FF6347',
1254 'transparent' => 'transparent',
1255 'turquoise' => '#40E0D0',
1256 'violet' => '#EE82EE',
1257 'wheat' => '#F5DEB3',
1258 'white' => '#FFFFFF',
1259 'whitesmoke' => '#F5F5F5',
1260 'yellow' => '#FFFF00',
1261 'yellowgreen' => '#9ACD32'
1266 * Used to prepare CSS strings
1268 * @package core
1269 * @subpackage cssoptimiser
1270 * @copyright 2012 Sam Hemelryk
1271 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1273 abstract class css_writer {
1276 * The current indent level
1277 * @var int
1279 protected static $indent = 0;
1282 * Returns true if the output should still maintain minimum formatting.
1283 * @return bool
1285 protected static function is_pretty() {
1286 global $CFG;
1287 return (!empty($CFG->cssoptimiserpretty));
1291 * Returns the indenting char to use for indenting things nicely.
1292 * @return string
1294 protected static function get_indent() {
1295 if (self::is_pretty()) {
1296 return str_repeat(" ", self::$indent);
1298 return '';
1302 * Increases the current indent
1304 protected static function increase_indent() {
1305 self::$indent++;
1309 * Decreases the current indent
1311 protected static function decrease_indent() {
1312 self::$indent--;
1316 * Returns the string to use as a separator
1317 * @return string
1319 protected static function get_separator() {
1320 return (self::is_pretty())?"\n":' ';
1324 * Returns CSS for media
1326 * @param string $typestring
1327 * @param css_rule[] $rules An array of css_rule objects
1328 * @return string
1330 public static function media($typestring, array &$rules) {
1331 $nl = self::get_separator();
1333 $output = '';
1334 if ($typestring !== 'all') {
1335 $output .= "\n@media {$typestring} {".$nl;
1336 self::increase_indent();
1338 foreach ($rules as $rule) {
1339 $output .= $rule->out().$nl;
1341 if ($typestring !== 'all') {
1342 self::decrease_indent();
1343 $output .= '}';
1345 return $output;
1349 * Returns CSS for a keyframe
1351 * @param string $for The desired declaration. e.g. keyframes, -moz-keyframes, -webkit-keyframes
1352 * @param string $name The name for the keyframe
1353 * @param css_rule[] $rules An array of rules belonging to the keyframe
1354 * @return string
1356 public static function keyframe($for, $name, array &$rules) {
1357 $output = "\n@{$for} {$name} {";
1358 foreach ($rules as $rule) {
1359 $output .= $rule->out();
1361 $output .= '}';
1362 return $output;
1366 * Returns CSS for a rule
1368 * @param string $selector
1369 * @param string $styles
1370 * @return string
1372 public static function rule($selector, $styles) {
1373 $css = self::get_indent()."{$selector}{{$styles}}";
1374 return $css;
1378 * Returns CSS for the selectors of a rule
1380 * @param css_selector[] $selectors Array of css_selector objects
1381 * @return string
1383 public static function selectors(array $selectors) {
1384 $nl = self::get_separator();
1385 $selectorstrings = array();
1386 foreach ($selectors as $selector) {
1387 $selectorstrings[] = $selector->out();
1389 return join(','.$nl, $selectorstrings);
1393 * Returns a selector given the components that make it up.
1395 * @param array $components
1396 * @return string
1398 public static function selector(array $components) {
1399 return trim(join(' ', $components));
1403 * Returns a CSS string for the provided styles
1405 * @param css_style[] $styles Array of css_style objects
1406 * @return string
1408 public static function styles(array $styles) {
1409 $bits = array();
1410 foreach ($styles as $style) {
1411 // Check if the style is an array. If it is then we are outputing an advanced style.
1412 // An advanced style is a style with one or more values, and can occur in situations like background-image
1413 // where browse specific values are being used.
1414 if (is_array($style)) {
1415 /* @var css_style[] $style */
1416 foreach ($style as $advstyle) {
1417 $bits[] = $advstyle->out();
1419 continue;
1421 $bits[] = $style->out();
1423 return join('', $bits);
1427 * Returns a style CSS
1429 * @param string $name
1430 * @param string $value
1431 * @param bool $important
1432 * @return string
1434 public static function style($name, $value, $important = false) {
1435 $value = trim($value);
1436 if ($important && strpos($value, '!important') === false) {
1437 $value .= ' !important';
1439 return "{$name}:{$value};";
1444 * A consolidatable style interface.
1446 * Class that implement this have a short-hand notation for specifying multiple styles.
1448 * @package core
1449 * @subpackage cssoptimiser
1450 * @copyright 2012 Sam Hemelryk
1451 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1453 interface core_css_consolidatable_style {
1455 * Used to consolidate several styles into a single "short-hand" style.
1456 * @param array $styles
1457 * @return mixed
1459 public static function consolidate(array $styles);
1463 * A structure to represent a CSS selector.
1465 * The selector is the classes, id, elements, and psuedo bits that make up a CSS
1466 * rule.
1468 * @package core
1469 * @subpackage cssoptimiser
1470 * @copyright 2012 Sam Hemelryk
1471 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1473 class css_selector {
1476 * An array of selector bits
1477 * @var array
1479 protected $selectors = array();
1482 * The number of selectors.
1483 * @var int
1485 protected $count = 0;
1488 * Is null if there are no selectors, true if all selectors are basic and false otherwise.
1489 * A basic selector is one that consists of just the element type. e.g. div, span, td, a
1490 * @var bool|null
1492 protected $isbasic = null;
1495 * Initialises a new CSS selector
1496 * @return css_selector
1498 public static function init() {
1499 return new css_selector();
1503 * CSS selectors can only be created through the init method above.
1505 protected function __construct() {
1506 // Nothing to do here by default.
1510 * Adds a selector to the end of the current selector
1511 * @param string $selector
1513 public function add($selector) {
1514 $selector = trim($selector);
1515 $count = 0;
1516 $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
1517 if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
1518 $count ++;
1520 // If its already false then no need to continue, its not basic.
1521 if ($this->isbasic !== false) {
1522 // If theres more than one part making up this selector its not basic.
1523 if ($count > 1) {
1524 $this->isbasic = false;
1525 } else {
1526 // Check whether it is a basic element (a-z+) with possible psuedo selector.
1527 $this->isbasic = (bool)preg_match('#^[a-z]+(:[a-zA-Z]+)?$#', $selector);
1530 $this->count = $count;
1531 $this->selectors[] = $selector;
1534 * Returns the number of individual components that make up this selector
1535 * @return int
1537 public function get_selector_count() {
1538 return $this->count;
1542 * Returns the selector for use in a CSS rule
1543 * @return string
1545 public function out() {
1546 return css_writer::selector($this->selectors);
1550 * Returns true is all of the selectors act only upon basic elements (no classes/ids)
1551 * @return bool
1553 public function is_basic() {
1554 return ($this->isbasic === true);
1559 * A structure to represent a CSS rule.
1561 * @package core
1562 * @subpackage cssoptimiser
1563 * @copyright 2012 Sam Hemelryk
1564 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1566 class css_rule {
1569 * An array of CSS selectors {@link css_selector}
1570 * @var css_selector[]
1572 protected $selectors = array();
1575 * An array of CSS styles {@link css_style}
1576 * @var css_style[]
1578 protected $styles = array();
1581 * Created a new CSS rule. This is the only way to create a new CSS rule externally.
1582 * @return css_rule
1584 public static function init() {
1585 return new css_rule();
1589 * Constructs a new css rule.
1591 * @param string $selector The selector or array of selectors that make up this rule.
1592 * @param css_style[] $styles An array of styles that belong to this rule.
1594 protected function __construct($selector = null, array $styles = array()) {
1595 if ($selector != null) {
1596 if (is_array($selector)) {
1597 $this->selectors = $selector;
1598 } else {
1599 $this->selectors = array($selector);
1601 $this->add_styles($styles);
1606 * Adds a new CSS selector to this rule
1608 * e.g. $rule->add_selector('.one #two.two');
1610 * @param css_selector $selector Adds a CSS selector to this rule.
1612 public function add_selector(css_selector $selector) {
1613 $this->selectors[] = $selector;
1617 * Adds a new CSS style to this rule.
1619 * @param css_style|string $style Adds a new style to this rule
1621 public function add_style($style) {
1622 if (is_string($style)) {
1623 $style = trim($style);
1624 if (empty($style)) {
1625 return;
1627 $bits = explode(':', $style, 2);
1628 if (count($bits) == 2) {
1629 list($name, $value) = array_map('trim', $bits);
1631 if (isset($name) && isset($value) && $name !== '' && $value !== '') {
1632 $style = css_style::init_automatic($name, $value);
1634 } else if ($style instanceof css_style) {
1635 // Clone the style as it may be coming from another rule and we don't
1636 // want references as it will likely be overwritten by proceeding
1637 // rules.
1638 $style = clone($style);
1640 if ($style instanceof css_style) {
1641 $name = $style->get_name();
1642 $exists = array_key_exists($name, $this->styles);
1643 // We need to find out if the current style support multiple values, or whether the style
1644 // is already set up to record multiple values. This can happen with background images which can have single
1645 // and multiple values.
1646 if ($style->allows_multiple_values() || ($exists && is_array($this->styles[$name]))) {
1647 if (!$exists) {
1648 $this->styles[$name] = array();
1649 } else if ($this->styles[$name] instanceof css_style) {
1650 $this->styles[$name] = array($this->styles[$name]);
1652 $this->styles[$name][] = $style;
1653 } else if ($exists) {
1654 $this->styles[$name]->set_value($style->get_value());
1655 } else {
1656 $this->styles[$name] = $style;
1658 } else if (is_array($style)) {
1659 // We probably shouldn't worry about processing styles here but to
1660 // be truthful it doesn't hurt.
1661 foreach ($style as $astyle) {
1662 $this->add_style($astyle);
1668 * An easy method of adding several styles at once. Just calls add_style.
1670 * This method simply iterates over the array and calls {@link css_rule::add_style()}
1671 * with each.
1673 * @param css_style[] $styles Adds an array of styles
1675 public function add_styles(array $styles) {
1676 foreach ($styles as $style) {
1677 $this->add_style($style);
1682 * Returns the array of selectors
1684 * @return css_selector[]
1686 public function get_selectors() {
1687 return $this->selectors;
1691 * Returns the array of styles
1693 * @return css_style[]
1695 public function get_styles() {
1696 return $this->styles;
1700 * Outputs this rule as a fragment of CSS
1702 * @return string
1704 public function out() {
1705 $selectors = css_writer::selectors($this->selectors);
1706 $styles = css_writer::styles($this->get_consolidated_styles());
1707 return css_writer::rule($selectors, $styles);
1711 * Consolidates all styles associated with this rule
1713 * @return css_style[] An array of consolidated styles
1715 public function get_consolidated_styles() {
1716 /* @var css_style[] $organisedstyles */
1717 $organisedstyles = array();
1718 /* @var css_style[] $finalstyles */
1719 $finalstyles = array();
1720 /* @var core_css_consolidatable_style[] $consolidate */
1721 $consolidate = array();
1722 /* @var css_style[] $advancedstyles */
1723 $advancedstyles = array();
1724 foreach ($this->styles as $style) {
1725 // If the style is an array then we are processing an advanced style. An advanced style is a style that can have
1726 // one or more values. Background-image is one such example as it can have browser specific styles.
1727 if (is_array($style)) {
1728 $single = null;
1729 $count = 0;
1730 foreach ($style as $advstyle) {
1731 /* @var css_style $advstyle */
1732 $key = $count++;
1733 $advancedstyles[$key] = $advstyle;
1734 if (!$advstyle->allows_multiple_values()) {
1735 if (!is_null($single)) {
1736 unset($advancedstyles[$single]);
1738 $single = $key;
1741 if (!is_null($single)) {
1742 $style = $advancedstyles[$single];
1744 $consolidatetoclass = $style->consolidate_to();
1745 if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1746 class_exists('css_style_'.$consolidatetoclass)) {
1747 $class = 'css_style_'.$consolidatetoclass;
1748 if (!array_key_exists($class, $consolidate)) {
1749 $consolidate[$class] = array();
1750 $organisedstyles[$class] = true;
1752 $consolidate[$class][] = $style;
1753 unset($advancedstyles[$single]);
1757 continue;
1759 $consolidatetoclass = $style->consolidate_to();
1760 if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1761 class_exists('css_style_'.$consolidatetoclass)) {
1762 $class = 'css_style_'.$consolidatetoclass;
1763 if (!array_key_exists($class, $consolidate)) {
1764 $consolidate[$class] = array();
1765 $organisedstyles[$class] = true;
1767 $consolidate[$class][] = $style;
1768 } else {
1769 $organisedstyles[$style->get_name()] = $style;
1773 foreach ($consolidate as $class => $styles) {
1774 $organisedstyles[$class] = call_user_func(array($class, 'consolidate'), $styles);
1777 foreach ($organisedstyles as $style) {
1778 if (is_array($style)) {
1779 foreach ($style as $s) {
1780 $finalstyles[] = $s;
1782 } else {
1783 $finalstyles[] = $style;
1786 $finalstyles = array_merge($finalstyles, $advancedstyles);
1787 return $finalstyles;
1791 * Splits this rules into an array of CSS rules. One for each of the selectors
1792 * that make up this rule.
1794 * @return css_rule[]
1796 public function split_by_selector() {
1797 $return = array();
1798 foreach ($this->selectors as $selector) {
1799 $return[] = new css_rule($selector, $this->styles);
1801 return $return;
1805 * Splits this rule into an array of rules. One for each of the styles that
1806 * make up this rule
1808 * @return css_rule[] Array of css_rule objects
1810 public function split_by_style() {
1811 $return = array();
1812 foreach ($this->styles as $style) {
1813 if (is_array($style)) {
1814 $return[] = new css_rule($this->selectors, $style);
1815 continue;
1817 $return[] = new css_rule($this->selectors, array($style));
1819 return $return;
1823 * Gets a hash for the styles of this rule
1825 * @return string
1827 public function get_style_hash() {
1828 return md5(css_writer::styles($this->styles));
1832 * Gets a hash for the selectors of this rule
1834 * @return string
1836 public function get_selector_hash() {
1837 return md5(css_writer::selectors($this->selectors));
1841 * Gets the number of selectors that make up this rule.
1843 * @return int
1845 public function get_selector_count() {
1846 $count = 0;
1847 foreach ($this->selectors as $selector) {
1848 $count += $selector->get_selector_count();
1850 return $count;
1854 * Returns true if there are any errors with this rule.
1856 * @return bool
1858 public function has_errors() {
1859 foreach ($this->styles as $style) {
1860 if (is_array($style)) {
1861 /* @var css_style[] $style */
1862 foreach ($style as $advstyle) {
1863 if ($advstyle->has_error()) {
1864 return true;
1867 continue;
1869 if ($style->has_error()) {
1870 return true;
1873 return false;
1877 * Returns the error strings that were recorded when processing this rule.
1879 * Before calling this function you should first call {@link css_rule::has_errors()}
1880 * to make sure there are errors (hopefully there arn't).
1882 * @return string
1884 public function get_error_string() {
1885 $css = $this->out();
1886 $errors = array();
1887 foreach ($this->styles as $style) {
1888 if (is_array($style)) {
1889 /* @var css_style[] $style */
1890 foreach ($style as $advstyle) {
1891 if ($advstyle instanceof css_style && $advstyle->has_error()) {
1892 $errors[] = " * ".$advstyle->get_last_error();
1895 } else if ($style instanceof css_style && $style->has_error()) {
1896 $errors[] = " * ".$style->get_last_error();
1899 return $css." has the following errors:\n".join("\n", $errors);
1903 * Returns true if this rule could be considered a reset rule.
1905 * A reset rule is a rule that acts upon an HTML element and does not include any other parts to its selector.
1907 * @return bool
1909 public function is_reset_rule() {
1910 foreach ($this->selectors as $selector) {
1911 if (!$selector->is_basic()) {
1912 return false;
1915 return true;
1920 * An abstract CSS rule collection class.
1922 * This class is extended by things such as media and keyframe declaration. They are declarations that
1923 * group rules together for a purpose.
1924 * When no declaration is specified rules accumulate into @media all.
1926 * @package core
1927 * @subpackage cssoptimiser
1928 * @copyright 2012 Sam Hemelryk
1929 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1931 abstract class css_rule_collection {
1933 * An array of rules within this collection instance
1934 * @var css_rule[]
1936 protected $rules = array();
1939 * The collection must be able to print itself.
1941 abstract public function out();
1944 * Adds a new CSS rule to this collection instance
1946 * @param css_rule $newrule
1948 public function add_rule(css_rule $newrule) {
1949 foreach ($newrule->split_by_selector() as $rule) {
1950 $hash = $rule->get_selector_hash();
1951 if (!array_key_exists($hash, $this->rules)) {
1952 $this->rules[$hash] = $rule;
1953 } else {
1954 $this->rules[$hash]->add_styles($rule->get_styles());
1960 * Returns the rules used by this collection
1962 * @return css_rule[]
1964 public function get_rules() {
1965 return $this->rules;
1969 * Organises rules by gropuing selectors based upon the styles and consolidating
1970 * those selectors into single rules.
1972 * @return bool True if the CSS was optimised by this method
1974 public function organise_rules_by_selectors() {
1975 /* @var css_rule[] $optimisedrules */
1976 $optimisedrules = array();
1977 $beforecount = count($this->rules);
1978 $lasthash = null;
1979 /* @var css_rule $lastrule */
1980 $lastrule = null;
1981 foreach ($this->rules as $rule) {
1982 $hash = $rule->get_style_hash();
1983 if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) {
1984 foreach ($rule->get_selectors() as $selector) {
1985 $lastrule->add_selector($selector);
1987 continue;
1989 $lastrule = clone($rule);
1990 $lasthash = $hash;
1991 $optimisedrules[] = $lastrule;
1993 $this->rules = array();
1994 foreach ($optimisedrules as $optimised) {
1995 $this->rules[$optimised->get_selector_hash()] = $optimised;
1997 $aftercount = count($this->rules);
1998 return ($beforecount < $aftercount);
2002 * Returns the total number of rules that exist within this collection
2004 * @return int
2006 public function count_rules() {
2007 return count($this->rules);
2011 * Returns the total number of selectors that exist within this collection
2013 * @return int
2015 public function count_selectors() {
2016 $count = 0;
2017 foreach ($this->rules as $rule) {
2018 $count += $rule->get_selector_count();
2020 return $count;
2024 * Returns true if the collection has any rules that have errors
2026 * @return boolean
2028 public function has_errors() {
2029 foreach ($this->rules as $rule) {
2030 if ($rule->has_errors()) {
2031 return true;
2034 return false;
2038 * Returns any errors that have happened within rules in this collection.
2040 * @return string[]
2042 public function get_errors() {
2043 $errors = array();
2044 foreach ($this->rules as $rule) {
2045 if ($rule->has_errors()) {
2046 $errors[] = $rule->get_error_string();
2049 return $errors;
2054 * A media class to organise rules by the media they apply to.
2056 * @package core
2057 * @subpackage cssoptimiser
2058 * @copyright 2012 Sam Hemelryk
2059 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2061 class css_media extends css_rule_collection {
2064 * An array of the different media types this instance applies to.
2065 * @var array
2067 protected $types = array();
2070 * Initalises a new media instance
2072 * @param string $for The media that the contained rules are destined for.
2074 public function __construct($for = 'all') {
2075 $types = explode(',', $for);
2076 $this->types = array_map('trim', $types);
2080 * Returns the CSS for this media and all of its rules.
2082 * @return string
2084 public function out() {
2085 return css_writer::media(join(',', $this->types), $this->rules);
2089 * Returns an array of media that this media instance applies to
2091 * @return array
2093 public function get_types() {
2094 return $this->types;
2098 * Returns all of the reset rules known by this media set.
2099 * @param bool $remove If set to true reset rules will be removed before being returned.
2100 * @return array
2102 public function get_reset_rules($remove = false) {
2103 $resetrules = array();
2104 foreach ($this->rules as $key => $rule) {
2105 if ($rule->is_reset_rule()) {
2106 $resetrules[] = clone $rule;
2107 if ($remove) {
2108 unset($this->rules[$key]);
2112 return $resetrules;
2117 * A media class to organise rules by the media they apply to.
2119 * @package core
2120 * @subpackage cssoptimiser
2121 * @copyright 2012 Sam Hemelryk
2122 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2124 class css_keyframe extends css_rule_collection {
2127 * The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2128 * @var string
2130 protected $for;
2133 * The name for the keyframes
2134 * @var string
2136 protected $name;
2138 * Constructs a new keyframe
2140 * @param string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2141 * @param string $name The name for the keyframes
2143 public function __construct($for, $name) {
2144 $this->for = $for;
2145 $this->name = $name;
2148 * Returns the directive of this keyframe
2150 * e.g. keyframes, -moz-keyframes, -webkit-keyframes
2151 * @return string
2153 public function get_for() {
2154 return $this->for;
2157 * Returns the name of this keyframe
2158 * @return string
2160 public function get_name() {
2161 return $this->name;
2164 * Returns the CSS for this collection of keyframes and all of its rules.
2166 * @return string
2168 public function out() {
2169 return css_writer::keyframe($this->for, $this->name, $this->rules);
2174 * An absract class to represent CSS styles
2176 * @package core
2177 * @subpackage cssoptimiser
2178 * @copyright 2012 Sam Hemelryk
2179 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2181 abstract class css_style {
2183 /** Constant used for recongise a special empty value in a CSS style */
2184 const NULL_VALUE = '@@$NULL$@@';
2187 * The name of the style
2188 * @var string
2190 protected $name;
2193 * The value for the style
2194 * @var mixed
2196 protected $value;
2199 * If set to true this style was defined with the !important rule.
2200 * Only trolls use !important.
2201 * Don't hide under bridges.. its not good for your skin. Do the proper thing
2202 * and fix the issue don't just force a fix that will undoubtedly one day
2203 * lead to further frustration.
2204 * @var bool
2206 protected $important = false;
2209 * Gets set to true if this style has an error
2210 * @var bool
2212 protected $error = false;
2215 * The last error message that occured
2216 * @var string
2218 protected $errormessage = null;
2221 * Initialises a new style.
2223 * This is the only public way to create a style to ensure they that appropriate
2224 * style class is used if it exists.
2226 * @param string $name The name of the style.
2227 * @param string $value The value of the style.
2228 * @return css_style_generic
2230 public static function init_automatic($name, $value) {
2231 $cleanedname = preg_replace('#[^a-zA-Z0-9]+#', '', $name);
2232 $specificclass = 'css_style_'.$cleanedname;
2233 if (class_exists($specificclass)) {
2234 $style = call_user_func(array($specificclass, 'init'), $value);
2235 if ($cleanedname !== $name && !is_array($style)) {
2236 $style->set_actual_name($name);
2238 return $style;
2240 return new css_style_generic($name, $value);
2244 * Creates a new style when given its name and value
2246 * @param string $name The name of the style.
2247 * @param string $value The value of the style.
2249 protected function __construct($name, $value) {
2250 $this->name = $name;
2251 $this->set_value($value);
2255 * Sets the value for the style
2257 * @param string $value
2259 final public function set_value($value) {
2260 $value = trim($value);
2261 $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
2262 if ($important) {
2263 $value = substr($value, 0, -(strlen($matches[1])));
2264 $value = rtrim($value);
2266 if (!$this->important || $important) {
2267 $this->value = $this->clean_value($value);
2268 $this->important = $important;
2270 if (!$this->is_valid()) {
2271 $this->set_error('Invalid value for '.$this->name);
2276 * Returns true if the value associated with this style is valid
2278 * @return bool
2280 public function is_valid() {
2281 return true;
2285 * Returns the name for the style
2287 * @return string
2289 public function get_name() {
2290 return $this->name;
2294 * Returns the value for the style
2296 * @param bool $includeimportant If set to true and the rule is important !important postfix will be used.
2297 * @return string
2299 public function get_value($includeimportant = true) {
2300 $value = $this->value;
2301 if ($includeimportant && $this->important) {
2302 $value .= ' !important';
2304 return $value;
2308 * Returns the style ready for use in CSS
2310 * @param string|null $value A value to use to override the value for this style.
2311 * @return string
2313 public function out($value = null) {
2314 if (is_null($value)) {
2315 $value = $this->get_value();
2317 return css_writer::style($this->name, $value, $this->important);
2321 * This can be overridden by a specific style allowing it to clean its values
2322 * consistently.
2324 * @param mixed $value
2325 * @return mixed
2327 protected function clean_value($value) {
2328 return $value;
2332 * If this particular style can be consolidated into another style this function
2333 * should return the style that it can be consolidated into.
2335 * @return string|null
2337 public function consolidate_to() {
2338 return null;
2342 * Sets the last error message.
2344 * @param string $message
2346 protected function set_error($message) {
2347 $this->error = true;
2348 $this->errormessage = $message;
2352 * Returns true if an error has occured
2354 * @return bool
2356 public function has_error() {
2357 return $this->error;
2361 * Returns the last error that occured or null if no errors have happened.
2363 * @return string
2365 public function get_last_error() {
2366 return $this->errormessage;
2370 * Returns true if the value for this style is the special null value.
2372 * This should only be overriden in circumstances where a shorthand style can lead
2373 * to move explicit styles being overwritten. Not a common place occurenace.
2375 * Example:
2376 * This occurs if the shorthand background property was used but no proper value
2377 * was specified for this style.
2378 * This leads to a null value being used unless otherwise overridden.
2380 * @return bool
2382 public function is_special_empty_value() {
2383 return false;
2387 * Returns true if this style permits multiple values.
2389 * This occurs for styles such as background image that can have browser specific values that need to be maintained because
2390 * of course we don't know what browser the user is using, and optimisation occurs before caching.
2391 * Thus we must always server all values we encounter in the order we encounter them for when this is set to true.
2393 * @return boolean False by default, true if the style supports muliple values.
2395 public function allows_multiple_values() {
2396 return false;
2400 * Returns true if this style was marked important.
2401 * @return bool
2403 public function is_important() {
2404 return !empty($this->important);
2408 * Sets the important flag for this style and its current value.
2409 * @param bool $important
2411 public function set_important($important = true) {
2412 $this->important = (bool) $important;
2416 * Sets the actual name used within the style.
2418 * This method allows us to support browser hacks like *width:0;
2420 * @param string $name
2422 public function set_actual_name($name) {
2423 $this->name = $name;
2428 * A generic CSS style class to use when a more specific class does not exist.
2430 * @package core
2431 * @subpackage cssoptimiser
2432 * @copyright 2012 Sam Hemelryk
2433 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2435 class css_style_generic extends css_style {
2438 * Cleans incoming values for typical things that can be optimised.
2440 * @param mixed $value Cleans the provided value optimising it if possible
2441 * @return string
2443 protected function clean_value($value) {
2444 if (trim($value) == '0px') {
2445 $value = 0;
2446 } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
2447 $value = '#'.strtoupper($matches[1]);
2449 return $value;
2454 * A colour CSS style
2456 * @package core
2457 * @subpackage cssoptimiser
2458 * @copyright 2012 Sam Hemelryk
2459 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2461 class css_style_color extends css_style {
2464 * Creates a new colour style
2466 * @param mixed $value Initialises a new colour style
2467 * @return css_style_color
2469 public static function init($value) {
2470 return new css_style_color('color', $value);
2474 * Cleans the colour unifing it to a 6 char hash colour if possible
2475 * Doing this allows us to associate identical colours being specified in
2476 * different ways. e.g. Red, red, #F00, and #F00000
2478 * @param mixed $value Cleans the provided value optimising it if possible
2479 * @return string
2481 protected function clean_value($value) {
2482 $value = trim($value);
2483 if (css_is_colour($value)) {
2484 if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
2485 $value = '#'.strtoupper($matches[1]);
2486 } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
2487 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
2488 $value = '#'.strtoupper($value);
2489 } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
2490 $value = css_optimiser::$htmlcolours[strtolower($value)];
2493 return $value;
2497 * Returns the colour style for use within CSS.
2498 * Will return an optimised hash colour.
2500 * e.g #123456
2501 * #123 instead of #112233
2502 * #F00 instead of red
2504 * @param string $overridevalue If provided then this value will be used instead
2505 * of the styles current value.
2506 * @return string
2508 public function out($overridevalue = null) {
2509 if ($overridevalue === null) {
2510 $overridevalue = $this->value;
2512 return parent::out(self::shrink_value($overridevalue));
2516 * Shrinks the colour value is possible.
2518 * @param string $value Shrinks the current value to an optimial form if possible
2519 * @return string
2521 public static function shrink_value($value) {
2522 if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) {
2523 return '#'.$matches[1].$matches[2].$matches[3];
2525 return $value;
2529 * Returns true if the value is a valid colour.
2531 * @return bool
2533 public function is_valid() {
2534 return css_is_colour($this->value);
2539 * A width style
2541 * @package core
2542 * @subpackage cssoptimiser
2543 * @copyright 2012 Sam Hemelryk
2544 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2546 class css_style_width extends css_style {
2549 * Checks if the width is valid
2550 * @return bool
2552 public function is_valid() {
2553 return css_is_width($this->value);
2557 * Cleans the provided value
2559 * @param mixed $value Cleans the provided value optimising it if possible
2560 * @return string
2562 protected function clean_value($value) {
2563 if (!css_is_width($value)) {
2564 // Note we don't actually change the value to something valid. That
2565 // would be bad for futureproofing.
2566 $this->set_error('Invalid width specified for '.$this->name);
2567 } else if (preg_match('#^0\D+$#', $value)) {
2568 $value = 0;
2570 return trim($value);
2574 * Initialises a new width style
2576 * @param mixed $value The value this style has
2577 * @return css_style_width
2579 public static function init($value) {
2580 return new css_style_width('width', $value);
2585 * A margin style
2587 * @package core
2588 * @subpackage cssoptimiser
2589 * @copyright 2012 Sam Hemelryk
2590 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2592 class css_style_margin extends css_style_width implements core_css_consolidatable_style {
2595 * Initialises a margin style.
2597 * In this case we split the margin into several other margin styles so that
2598 * we can properly condense overrides and then reconsolidate them later into
2599 * an optimal form.
2601 * @param string $value The value the style has
2602 * @return array An array of margin values that can later be consolidated
2604 public static function init($value) {
2605 $important = '';
2606 if (strpos($value, '!important') !== false) {
2607 $important = ' !important';
2608 $value = str_replace('!important', '', $value);
2611 $value = preg_replace('#\s+#', ' ', trim($value));
2612 $bits = explode(' ', $value, 4);
2614 $top = $right = $bottom = $left = null;
2615 if (count($bits) > 0) {
2616 $top = $right = $bottom = $left = array_shift($bits);
2618 if (count($bits) > 0) {
2619 $right = $left = array_shift($bits);
2621 if (count($bits) > 0) {
2622 $bottom = array_shift($bits);
2624 if (count($bits) > 0) {
2625 $left = array_shift($bits);
2627 return array(
2628 new css_style_margintop('margin-top', $top.$important),
2629 new css_style_marginright('margin-right', $right.$important),
2630 new css_style_marginbottom('margin-bottom', $bottom.$important),
2631 new css_style_marginleft('margin-left', $left.$important)
2636 * Consolidates individual margin styles into a single margin style
2638 * @param css_style[] $styles
2639 * @return css_style[] An array of consolidated styles
2641 public static function consolidate(array $styles) {
2642 if (count($styles) != 4) {
2643 return $styles;
2646 $someimportant = false;
2647 $allimportant = null;
2648 $notimportantequal = null;
2649 $firstvalue = null;
2650 foreach ($styles as $style) {
2651 if ($style->is_important()) {
2652 $someimportant = true;
2653 if ($allimportant === null) {
2654 $allimportant = true;
2656 } else {
2657 if ($allimportant === true) {
2658 $allimportant = false;
2660 if ($firstvalue == null) {
2661 $firstvalue = $style->get_value(false);
2662 $notimportantequal = true;
2663 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
2664 $notimportantequal = false;
2669 if ($someimportant && !$allimportant && !$notimportantequal) {
2670 return $styles;
2673 if ($someimportant && !$allimportant && $notimportantequal) {
2674 $return = array(
2675 new css_style_margin('margin', $firstvalue)
2677 foreach ($styles as $style) {
2678 if ($style->is_important()) {
2679 $return[] = $style;
2682 return $return;
2683 } else {
2684 $top = null;
2685 $right = null;
2686 $bottom = null;
2687 $left = null;
2688 foreach ($styles as $style) {
2689 switch ($style->get_name()) {
2690 case 'margin-top' :
2691 $top = $style->get_value(false);
2692 break;
2693 case 'margin-right' :
2694 $right = $style->get_value(false);
2695 break;
2696 case 'margin-bottom' :
2697 $bottom = $style->get_value(false);
2698 break;
2699 case 'margin-left' :
2700 $left = $style->get_value(false);
2701 break;
2704 if ($top == $bottom && $left == $right) {
2705 if ($top == $left) {
2706 $returnstyle = new css_style_margin('margin', $top);
2707 } else {
2708 $returnstyle = new css_style_margin('margin', "{$top} {$left}");
2710 } else if ($left == $right) {
2711 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom}");
2712 } else {
2713 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}");
2715 if ($allimportant) {
2716 $returnstyle->set_important();
2718 return array($returnstyle);
2724 * A margin top style
2726 * @package core
2727 * @subpackage cssoptimiser
2728 * @copyright 2012 Sam Hemelryk
2729 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2731 class css_style_margintop extends css_style_margin {
2734 * A simple init, just a single style
2736 * @param string $value The value the style has
2737 * @return css_style_margintop
2739 public static function init($value) {
2740 return new css_style_margintop('margin-top', $value);
2744 * This style can be consolidated into a single margin style
2746 * @return string
2748 public function consolidate_to() {
2749 return 'margin';
2754 * A margin right style
2756 * @package core
2757 * @subpackage cssoptimiser
2758 * @copyright 2012 Sam Hemelryk
2759 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2761 class css_style_marginright extends css_style_margin {
2764 * A simple init, just a single style
2766 * @param string $value The value the style has
2767 * @return css_style_margintop
2769 public static function init($value) {
2770 return new css_style_marginright('margin-right', $value);
2774 * This style can be consolidated into a single margin style
2776 * @return string
2778 public function consolidate_to() {
2779 return 'margin';
2784 * A margin bottom style
2786 * @package core
2787 * @subpackage cssoptimiser
2788 * @copyright 2012 Sam Hemelryk
2789 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2791 class css_style_marginbottom extends css_style_margin {
2794 * A simple init, just a single style
2796 * @param string $value The value the style has
2797 * @return css_style_margintop
2799 public static function init($value) {
2800 return new css_style_marginbottom('margin-bottom', $value);
2804 * This style can be consolidated into a single margin style
2806 * @return string
2808 public function consolidate_to() {
2809 return 'margin';
2814 * A margin left style
2816 * @package core
2817 * @subpackage cssoptimiser
2818 * @copyright 2012 Sam Hemelryk
2819 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2821 class css_style_marginleft extends css_style_margin {
2824 * A simple init, just a single style
2826 * @param string $value The value the style has
2827 * @return css_style_margintop
2829 public static function init($value) {
2830 return new css_style_marginleft('margin-left', $value);
2834 * This style can be consolidated into a single margin style
2836 * @return string
2838 public function consolidate_to() {
2839 return 'margin';
2844 * A border style
2846 * @package core
2847 * @subpackage cssoptimiser
2848 * @copyright 2012 Sam Hemelryk
2849 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2851 class css_style_border extends css_style implements core_css_consolidatable_style {
2854 * Initalises the border style into an array of individual style compontents
2856 * @param string $value The value the style has
2857 * @return css_style_bordercolor
2859 public static function init($value) {
2860 $value = preg_replace('#\s+#', ' ', $value);
2861 $bits = explode(' ', $value, 3);
2863 $return = array();
2864 if (count($bits) > 0) {
2865 $width = array_shift($bits);
2866 if (!css_style_borderwidth::is_border_width($width)) {
2867 $width = '0';
2869 $return[] = css_style_bordertopwidth::init($width);
2870 $return[] = css_style_borderrightwidth::init($width);
2871 $return[] = css_style_borderbottomwidth::init($width);
2872 $return[] = css_style_borderleftwidth::init($width);
2874 if (count($bits) > 0) {
2875 $style = array_shift($bits);
2876 $return[] = css_style_bordertopstyle::init($style);
2877 $return[] = css_style_borderrightstyle::init($style);
2878 $return[] = css_style_borderbottomstyle::init($style);
2879 $return[] = css_style_borderleftstyle::init($style);
2881 if (count($bits) > 0) {
2882 $colour = array_shift($bits);
2883 $return[] = css_style_bordertopcolor::init($colour);
2884 $return[] = css_style_borderrightcolor::init($colour);
2885 $return[] = css_style_borderbottomcolor::init($colour);
2886 $return[] = css_style_borderleftcolor::init($colour);
2888 return $return;
2892 * Consolidates all border styles into a single style
2894 * @param css_style[] $styles An array of border styles
2895 * @return css_style[] An optimised array of border styles
2897 public static function consolidate(array $styles) {
2899 $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2900 $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2901 $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2903 foreach ($styles as $style) {
2904 switch ($style->get_name()) {
2905 case 'border-top-width':
2906 $borderwidths['top'] = $style->get_value();
2907 break;
2908 case 'border-right-width':
2909 $borderwidths['right'] = $style->get_value();
2910 break;
2911 case 'border-bottom-width':
2912 $borderwidths['bottom'] = $style->get_value();
2913 break;
2914 case 'border-left-width':
2915 $borderwidths['left'] = $style->get_value();
2916 break;
2918 case 'border-top-style':
2919 $borderstyles['top'] = $style->get_value();
2920 break;
2921 case 'border-right-style':
2922 $borderstyles['right'] = $style->get_value();
2923 break;
2924 case 'border-bottom-style':
2925 $borderstyles['bottom'] = $style->get_value();
2926 break;
2927 case 'border-left-style':
2928 $borderstyles['left'] = $style->get_value();
2929 break;
2931 case 'border-top-color':
2932 $bordercolors['top'] = css_style_color::shrink_value($style->get_value());
2933 break;
2934 case 'border-right-color':
2935 $bordercolors['right'] = css_style_color::shrink_value($style->get_value());
2936 break;
2937 case 'border-bottom-color':
2938 $bordercolors['bottom'] = css_style_color::shrink_value($style->get_value());
2939 break;
2940 case 'border-left-color':
2941 $bordercolors['left'] = css_style_color::shrink_value($style->get_value());
2942 break;
2946 $uniquewidths = count(array_unique($borderwidths));
2947 $uniquestyles = count(array_unique($borderstyles));
2948 $uniquecolors = count(array_unique($bordercolors));
2950 $nullwidths = in_array(null, $borderwidths, true);
2951 $nullstyles = in_array(null, $borderstyles, true);
2952 $nullcolors = in_array(null, $bordercolors, true);
2954 $allwidthsthesame = ($uniquewidths === 1)?1:0;
2955 $allstylesthesame = ($uniquestyles === 1)?1:0;
2956 $allcolorsthesame = ($uniquecolors === 1)?1:0;
2958 $allwidthsnull = $allwidthsthesame && $nullwidths;
2959 $allstylesnull = $allstylesthesame && $nullstyles;
2960 $allcolorsnull = $allcolorsthesame && $nullcolors;
2962 /* @var css_style[] $return */
2963 $return = array();
2964 if ($allwidthsnull && $allstylesnull && $allcolorsnull) {
2965 // Everything is null still... boo.
2966 return array(new css_style_border('border', ''));
2968 } else if ($allwidthsnull && $allstylesnull) {
2970 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2971 return $return;
2973 } else if ($allwidthsnull && $allcolorsnull) {
2975 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2976 return $return;
2978 } else if ($allcolorsnull && $allstylesnull) {
2980 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2981 return $return;
2985 if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 3) {
2987 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']);
2989 } else if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 2) {
2991 if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) {
2993 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']);
2994 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2996 } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) {
2998 $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']);
2999 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
3001 } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) {
3003 $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']);
3004 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
3006 } else {
3007 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
3008 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
3009 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
3012 } else if (!$nullwidths && !$nullcolors && !$nullstyles &&
3013 max(array_count_values($borderwidths)) == 3 &&
3014 max(array_count_values($borderstyles)) == 3 &&
3015 max(array_count_values($bordercolors)) == 3) {
3017 $widthkeys = array();
3018 $stylekeys = array();
3019 $colorkeys = array();
3021 foreach ($borderwidths as $key => $value) {
3022 if (!array_key_exists($value, $widthkeys)) {
3023 $widthkeys[$value] = array();
3025 $widthkeys[$value][] = $key;
3027 usort($widthkeys, 'css_sort_by_count');
3028 $widthkeys = array_values($widthkeys);
3030 foreach ($borderstyles as $key => $value) {
3031 if (!array_key_exists($value, $stylekeys)) {
3032 $stylekeys[$value] = array();
3034 $stylekeys[$value][] = $key;
3036 usort($stylekeys, 'css_sort_by_count');
3037 $stylekeys = array_values($stylekeys);
3039 foreach ($bordercolors as $key => $value) {
3040 if (!array_key_exists($value, $colorkeys)) {
3041 $colorkeys[$value] = array();
3043 $colorkeys[$value][] = $key;
3045 usort($colorkeys, 'css_sort_by_count');
3046 $colorkeys = array_values($colorkeys);
3048 if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) {
3049 $key = $widthkeys[0][0];
3050 self::build_style_string($return, 'css_style_border', 'border',
3051 $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
3052 $key = $widthkeys[1][0];
3053 self::build_style_string($return, 'css_style_border'.$key, 'border-'.$key,
3054 $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
3055 } else {
3056 self::build_style_string($return, 'css_style_bordertop', 'border-top',
3057 $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
3058 self::build_style_string($return, 'css_style_borderright', 'border-right',
3059 $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
3060 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
3061 $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
3062 self::build_style_string($return, 'css_style_borderleft', 'border-left',
3063 $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
3065 } else {
3066 self::build_style_string($return, 'css_style_bordertop', 'border-top',
3067 $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
3068 self::build_style_string($return, 'css_style_borderright', 'border-right',
3069 $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
3070 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
3071 $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
3072 self::build_style_string($return, 'css_style_borderleft', 'border-left',
3073 $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
3075 foreach ($return as $key => $style) {
3076 if ($style->get_value() == '') {
3077 unset($return[$key]);
3080 return $return;
3084 * Border styles get consolidated to a single border style.
3086 * @return string
3088 public function consolidate_to() {
3089 return 'border';
3093 * Consolidates a series of border styles into an optimised array of border
3094 * styles by looking at the direction of the border and prioritising that
3095 * during the optimisation.
3097 * @param array $array An array to add styles into during consolidation. Passed by reference.
3098 * @param string $class The class type to initalise
3099 * @param string $style The style to create
3100 * @param string|array $top The top value
3101 * @param string $right The right value
3102 * @param string $bottom The bottom value
3103 * @param string $left The left value
3104 * @return bool
3106 public static function consolidate_styles_by_direction(&$array, $class, $style,
3107 $top, $right = null, $bottom = null, $left = null) {
3108 if (is_array($top)) {
3109 $right = $top['right'];
3110 $bottom = $top['bottom'];
3111 $left = $top['left'];
3112 $top = $top['top'];
3115 if ($top == $bottom && $left == $right && $top == $left) {
3116 if (is_null($top)) {
3117 $array[] = new $class($style, '');
3118 } else {
3119 $array[] = new $class($style, $top);
3121 } else if ($top == null || $right == null || $bottom == null || $left == null) {
3122 if ($top !== null) {
3123 $array[] = new $class(str_replace('border-', 'border-top-', $style), $top);
3125 if ($right !== null) {
3126 $array[] = new $class(str_replace('border-', 'border-right-', $style), $right);
3128 if ($bottom !== null) {
3129 $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom);
3131 if ($left !== null) {
3132 $array[] = new $class(str_replace('border-', 'border-left-', $style), $left);
3134 } else if ($top == $bottom && $left == $right) {
3135 $array[] = new $class($style, $top.' '.$right);
3136 } else if ($left == $right) {
3137 $array[] = new $class($style, $top.' '.$right.' '.$bottom);
3138 } else {
3139 $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left);
3141 return true;
3145 * Builds a border style for a set of width, style, and colour values
3147 * @param array $array An array into which the generated style is added
3148 * @param string $class The class type to initialise
3149 * @param string $cssstyle The style to use
3150 * @param string $width The width of the border
3151 * @param string $style The style of the border
3152 * @param string $color The colour of the border
3153 * @return bool
3155 public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) {
3156 if (!is_null($width) && !is_null($style) && !is_null($color)) {
3157 $array[] = new $class($cssstyle, $width.' '.$style.' '.$color);
3158 } else if (!is_null($width) && !is_null($style) && is_null($color)) {
3159 $array[] = new $class($cssstyle, $width.' '.$style);
3160 } else if (!is_null($width) && is_null($style) && is_null($color)) {
3161 $array[] = new $class($cssstyle, $width);
3162 } else {
3163 if (!is_null($width)) {
3164 $array[] = new $class($cssstyle, $width);
3166 if (!is_null($style)) {
3167 $array[] = new $class($cssstyle, $style);
3169 if (!is_null($color)) {
3170 $array[] = new $class($cssstyle, $color);
3173 return true;
3178 * A border colour style
3180 * @package core
3181 * @subpackage cssoptimiser
3182 * @copyright 2012 Sam Hemelryk
3183 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3185 class css_style_bordercolor extends css_style_color {
3188 * Creates a new border colour style
3190 * Based upon the colour style
3192 * @param mixed $value
3193 * @return Array of css_style_bordercolor
3195 public static function init($value) {
3196 $value = preg_replace('#\s+#', ' ', $value);
3197 $bits = explode(' ', $value, 4);
3199 $top = $right = $bottom = $left = null;
3200 if (count($bits) > 0) {
3201 $top = $right = $bottom = $left = array_shift($bits);
3203 if (count($bits) > 0) {
3204 $right = $left = array_shift($bits);
3206 if (count($bits) > 0) {
3207 $bottom = array_shift($bits);
3209 if (count($bits) > 0) {
3210 $left = array_shift($bits);
3212 return array(
3213 css_style_bordertopcolor::init($top),
3214 css_style_borderrightcolor::init($right),
3215 css_style_borderbottomcolor::init($bottom),
3216 css_style_borderleftcolor::init($left)
3221 * Consolidate this to a single border style
3223 * @return string
3225 public function consolidate_to() {
3226 return 'border';
3230 * Cleans the value
3232 * @param string $value Cleans the provided value optimising it if possible
3233 * @return string
3235 protected function clean_value($value) {
3236 $values = explode(' ', $value);
3237 $values = array_map('parent::clean_value', $values);
3238 return join (' ', $values);
3242 * Outputs this style
3244 * @param string $overridevalue
3245 * @return string
3247 public function out($overridevalue = null) {
3248 if ($overridevalue === null) {
3249 $overridevalue = $this->value;
3251 $values = explode(' ', $overridevalue);
3252 $values = array_map('css_style_color::shrink_value', $values);
3253 return parent::out(join (' ', $values));
3258 * A border left style
3260 * @package core
3261 * @subpackage cssoptimiser
3262 * @copyright 2012 Sam Hemelryk
3263 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3265 class css_style_borderleft extends css_style_generic {
3268 * Initialises the border left style into individual components
3270 * @param string $value
3271 * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor
3273 public static function init($value) {
3274 $value = preg_replace('#\s+#', ' ', $value);
3275 $bits = explode(' ', $value, 3);
3277 $return = array();
3278 if (count($bits) > 0) {
3279 $return[] = css_style_borderleftwidth::init(array_shift($bits));
3281 if (count($bits) > 0) {
3282 $return[] = css_style_borderleftstyle::init(array_shift($bits));
3284 if (count($bits) > 0) {
3285 $return[] = css_style_borderleftcolor::init(array_shift($bits));
3287 return $return;
3291 * Consolidate this to a single border style
3293 * @return string
3295 public function consolidate_to() {
3296 return 'border';
3301 * A border right style
3303 * @package core
3304 * @subpackage cssoptimiser
3305 * @copyright 2012 Sam Hemelryk
3306 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3308 class css_style_borderright extends css_style_generic {
3311 * Initialises the border right style into individual components
3313 * @param string $value The value of the style
3314 * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor
3316 public static function init($value) {
3317 $value = preg_replace('#\s+#', ' ', $value);
3318 $bits = explode(' ', $value, 3);
3320 $return = array();
3321 if (count($bits) > 0) {
3322 $return[] = css_style_borderrightwidth::init(array_shift($bits));
3324 if (count($bits) > 0) {
3325 $return[] = css_style_borderrightstyle::init(array_shift($bits));
3327 if (count($bits) > 0) {
3328 $return[] = css_style_borderrightcolor::init(array_shift($bits));
3330 return $return;
3334 * Consolidate this to a single border style
3336 * @return string
3338 public function consolidate_to() {
3339 return 'border';
3344 * A border top style
3346 * @package core
3347 * @subpackage cssoptimiser
3348 * @copyright 2012 Sam Hemelryk
3349 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3351 class css_style_bordertop extends css_style_generic {
3354 * Initialises the border top style into individual components
3356 * @param string $value The value of the style
3357 * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor
3359 public static function init($value) {
3360 $value = preg_replace('#\s+#', ' ', $value);
3361 $bits = explode(' ', $value, 3);
3363 $return = array();
3364 if (count($bits) > 0) {
3365 $return[] = css_style_bordertopwidth::init(array_shift($bits));
3367 if (count($bits) > 0) {
3368 $return[] = css_style_bordertopstyle::init(array_shift($bits));
3370 if (count($bits) > 0) {
3371 $return[] = css_style_bordertopcolor::init(array_shift($bits));
3373 return $return;
3377 * Consolidate this to a single border style
3379 * @return string
3381 public function consolidate_to() {
3382 return 'border';
3387 * A border bottom style
3389 * @package core
3390 * @subpackage cssoptimiser
3391 * @copyright 2012 Sam Hemelryk
3392 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3394 class css_style_borderbottom extends css_style_generic {
3397 * Initialises the border bottom style into individual components
3399 * @param string $value The value of the style
3400 * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor
3402 public static function init($value) {
3403 $value = preg_replace('#\s+#', ' ', $value);
3404 $bits = explode(' ', $value, 3);
3406 $return = array();
3407 if (count($bits) > 0) {
3408 $return[] = css_style_borderbottomwidth::init(array_shift($bits));
3410 if (count($bits) > 0) {
3411 $return[] = css_style_borderbottomstyle::init(array_shift($bits));
3413 if (count($bits) > 0) {
3414 $return[] = css_style_borderbottomcolor::init(array_shift($bits));
3416 return $return;
3420 * Consolidate this to a single border style
3422 * @return string
3424 public function consolidate_to() {
3425 return 'border';
3430 * A border width style
3432 * @package core
3433 * @subpackage cssoptimiser
3434 * @copyright 2012 Sam Hemelryk
3435 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3437 class css_style_borderwidth extends css_style_width {
3440 * Creates a new border colour style
3442 * Based upon the colour style
3444 * @param string $value The value of the style
3445 * @return array Array of css_style_border*width
3447 public static function init($value) {
3448 $value = preg_replace('#\s+#', ' ', $value);
3449 $bits = explode(' ', $value, 4);
3451 $top = $right = $bottom = $left = null;
3452 if (count($bits) > 0) {
3453 $top = $right = $bottom = $left = array_shift($bits);
3455 if (count($bits) > 0) {
3456 $right = $left = array_shift($bits);
3458 if (count($bits) > 0) {
3459 $bottom = array_shift($bits);
3461 if (count($bits) > 0) {
3462 $left = array_shift($bits);
3464 return array(
3465 css_style_bordertopwidth::init($top),
3466 css_style_borderrightwidth::init($right),
3467 css_style_borderbottomwidth::init($bottom),
3468 css_style_borderleftwidth::init($left)
3473 * Consolidate this to a single border style
3475 * @return string
3477 public function consolidate_to() {
3478 return 'border';
3482 * Checks if the width is valid
3483 * @return bool
3485 public function is_valid() {
3486 return self::is_border_width($this->value);
3490 * Cleans the provided value
3492 * @param mixed $value Cleans the provided value optimising it if possible
3493 * @return string
3495 protected function clean_value($value) {
3496 $isvalid = self::is_border_width($value);
3497 if (!$isvalid) {
3498 $this->set_error('Invalid width specified for '.$this->name);
3499 } else if (preg_match('#^0\D+$#', $value)) {
3500 return '0';
3502 return trim($value);
3506 * Returns true if the provided value is a permitted border width
3507 * @param string $value The value to check
3508 * @return bool
3510 public static function is_border_width($value) {
3511 $altwidthvalues = array('thin', 'medium', 'thick');
3512 return css_is_width($value) || in_array($value, $altwidthvalues);
3517 * A border style style
3519 * @package core
3520 * @subpackage cssoptimiser
3521 * @copyright 2012 Sam Hemelryk
3522 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3524 class css_style_borderstyle extends css_style_generic {
3527 * Creates a new border colour style
3529 * Based upon the colour style
3531 * @param string $value The value of the style
3532 * @return array Array of css_style_border*style
3534 public static function init($value) {
3535 $value = preg_replace('#\s+#', ' ', $value);
3536 $bits = explode(' ', $value, 4);
3538 $top = $right = $bottom = $left = null;
3539 if (count($bits) > 0) {
3540 $top = $right = $bottom = $left = array_shift($bits);
3542 if (count($bits) > 0) {
3543 $right = $left = array_shift($bits);
3545 if (count($bits) > 0) {
3546 $bottom = array_shift($bits);
3548 if (count($bits) > 0) {
3549 $left = array_shift($bits);
3551 return array(
3552 css_style_bordertopstyle::init($top),
3553 css_style_borderrightstyle::init($right),
3554 css_style_borderbottomstyle::init($bottom),
3555 css_style_borderleftstyle::init($left)
3560 * Consolidate this to a single border style
3562 * @return string
3564 public function consolidate_to() {
3565 return 'border';
3570 * A border top colour style
3572 * @package core
3573 * @subpackage cssoptimiser
3574 * @copyright 2012 Sam Hemelryk
3575 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3577 class css_style_bordertopcolor extends css_style_bordercolor {
3580 * Initialises this style object
3582 * @param string $value The value of the style
3583 * @return css_style_bordertopcolor
3585 public static function init($value) {
3586 return new css_style_bordertopcolor('border-top-color', $value);
3590 * Consolidate this to a single border style
3592 * @return string
3594 public function consolidate_to() {
3595 return 'border';
3600 * A border left colour style
3602 * @package core
3603 * @subpackage cssoptimiser
3604 * @copyright 2012 Sam Hemelryk
3605 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3607 class css_style_borderleftcolor extends css_style_bordercolor {
3610 * Initialises this style object
3612 * @param string $value The value of the style
3613 * @return css_style_borderleftcolor
3615 public static function init($value) {
3616 return new css_style_borderleftcolor('border-left-color', $value);
3620 * Consolidate this to a single border style
3622 * @return string
3624 public function consolidate_to() {
3625 return 'border';
3630 * A border right colour style
3632 * @package core
3633 * @subpackage cssoptimiser
3634 * @copyright 2012 Sam Hemelryk
3635 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3637 class css_style_borderrightcolor extends css_style_bordercolor {
3640 * Initialises this style object
3642 * @param string $value The value of the style
3643 * @return css_style_borderrightcolor
3645 public static function init($value) {
3646 return new css_style_borderrightcolor('border-right-color', $value);
3650 * Consolidate this to a single border style
3652 * @return string
3654 public function consolidate_to() {
3655 return 'border';
3660 * A border bottom colour style
3662 * @package core
3663 * @subpackage cssoptimiser
3664 * @copyright 2012 Sam Hemelryk
3665 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3667 class css_style_borderbottomcolor extends css_style_bordercolor {
3670 * Initialises this style object
3672 * @param string $value The value of the style
3673 * @return css_style_borderbottomcolor
3675 public static function init($value) {
3676 return new css_style_borderbottomcolor('border-bottom-color', $value);
3680 * Consolidate this to a single border style
3682 * @return string
3684 public function consolidate_to() {
3685 return 'border';
3690 * A border width top style
3692 * @package core
3693 * @subpackage cssoptimiser
3694 * @copyright 2012 Sam Hemelryk
3695 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3697 class css_style_bordertopwidth extends css_style_borderwidth {
3700 * Initialises this style object
3702 * @param string $value The value of the style
3703 * @return css_style_bordertopwidth
3705 public static function init($value) {
3706 return new css_style_bordertopwidth('border-top-width', $value);
3710 * Consolidate this to a single border style
3712 * @return string
3714 public function consolidate_to() {
3715 return 'border';
3720 * A border width left style
3722 * @package core
3723 * @subpackage cssoptimiser
3724 * @copyright 2012 Sam Hemelryk
3725 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3727 class css_style_borderleftwidth extends css_style_borderwidth {
3730 * Initialises this style object
3732 * @param string $value The value of the style
3733 * @return css_style_borderleftwidth
3735 public static function init($value) {
3736 return new css_style_borderleftwidth('border-left-width', $value);
3740 * Consolidate this to a single border style
3742 * @return string
3744 public function consolidate_to() {
3745 return 'border';
3750 * A border width right style
3752 * @package core
3753 * @subpackage cssoptimiser
3754 * @copyright 2012 Sam Hemelryk
3755 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3757 class css_style_borderrightwidth extends css_style_borderwidth {
3760 * Initialises this style object
3762 * @param string $value The value of the style
3763 * @return css_style_borderrightwidth
3765 public static function init($value) {
3766 return new css_style_borderrightwidth('border-right-width', $value);
3770 * Consolidate this to a single border style
3772 * @return string
3774 public function consolidate_to() {
3775 return 'border';
3780 * A border width bottom style
3782 * @package core
3783 * @subpackage cssoptimiser
3784 * @copyright 2012 Sam Hemelryk
3785 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3787 class css_style_borderbottomwidth extends css_style_borderwidth {
3790 * Initialises this style object
3792 * @param string $value The value of the style
3793 * @return css_style_borderbottomwidth
3795 public static function init($value) {
3796 return new css_style_borderbottomwidth('border-bottom-width', $value);
3800 * Consolidate this to a single border style
3802 * @return string
3804 public function consolidate_to() {
3805 return 'border';
3810 * A border top style
3812 * @package core
3813 * @subpackage cssoptimiser
3814 * @copyright 2012 Sam Hemelryk
3815 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3817 class css_style_bordertopstyle extends css_style_borderstyle {
3820 * Initialises this style object
3822 * @param string $value The value of the style
3823 * @return css_style_bordertopstyle
3825 public static function init($value) {
3826 return new css_style_bordertopstyle('border-top-style', $value);
3830 * Consolidate this to a single border style
3832 * @return string
3834 public function consolidate_to() {
3835 return 'border';
3840 * A border left style
3842 * @package core
3843 * @subpackage cssoptimiser
3844 * @copyright 2012 Sam Hemelryk
3845 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3847 class css_style_borderleftstyle extends css_style_borderstyle {
3850 * Initialises this style object
3852 * @param string $value The value of the style
3853 * @return css_style_borderleftstyle
3855 public static function init($value) {
3856 return new css_style_borderleftstyle('border-left-style', $value);
3860 * Consolidate this to a single border style
3862 * @return string
3864 public function consolidate_to() {
3865 return 'border';
3870 * A border right style
3872 * @package core
3873 * @subpackage cssoptimiser
3874 * @copyright 2012 Sam Hemelryk
3875 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3877 class css_style_borderrightstyle extends css_style_borderstyle {
3880 * Initialises this style object
3882 * @param string $value The value of the style
3883 * @return css_style_borderrightstyle
3885 public static function init($value) {
3886 return new css_style_borderrightstyle('border-right-style', $value);
3890 * Consolidate this to a single border style
3892 * @return string
3894 public function consolidate_to() {
3895 return 'border';
3900 * A border bottom style
3902 * @package core
3903 * @subpackage cssoptimiser
3904 * @copyright 2012 Sam Hemelryk
3905 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3907 class css_style_borderbottomstyle extends css_style_borderstyle {
3910 * Initialises this style object
3912 * @param string $value The value for the style
3913 * @return css_style_borderbottomstyle
3915 public static function init($value) {
3916 return new css_style_borderbottomstyle('border-bottom-style', $value);
3920 * Consolidate this to a single border style
3922 * @return string
3924 public function consolidate_to() {
3925 return 'border';
3930 * A background style
3932 * @package core
3933 * @subpackage cssoptimiser
3934 * @copyright 2012 Sam Hemelryk
3935 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3937 class css_style_background extends css_style implements core_css_consolidatable_style {
3940 * Initialises a background style
3942 * @param string $value The value of the style
3943 * @return array An array of background component.
3945 public static function init($value) {
3946 // Colour - image - repeat - attachment - position.
3947 $imageurl = null;
3948 if (preg_match('#url\(([^\)]+)\)#', $value, $matches)) {
3949 $imageurl = trim($matches[1]);
3950 $value = str_replace($matches[1], '', $value);
3953 // Switch out the brackets so that they don't get messed up when we explode.
3954 $brackets = array();
3955 $bracketcount = 0;
3956 while (preg_match('#\([^\)\(]+\)#', $value, $matches)) {
3957 $key = "##BRACKET-{$bracketcount}##";
3958 $bracketcount++;
3959 $brackets[$key] = $matches[0];
3960 $value = str_replace($matches[0], $key, $value);
3963 $important = (stripos($value, '!important') !== false);
3964 if ($important) {
3965 // Great some genius put !important in the background shorthand property.
3966 $value = str_replace('!important', '', $value);
3969 $value = preg_replace('#\s+#', ' ', $value);
3970 $bits = explode(' ', $value);
3972 foreach ($bits as $key => $bit) {
3973 $bits[$key] = self::replace_bracket_placeholders($bit, $brackets);
3975 unset($bracketcount);
3976 unset($brackets);
3978 $repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit');
3979 $attachments = array('scroll' , 'fixed', 'inherit');
3980 $positions = array('top', 'left', 'bottom', 'right', 'center');
3982 /* @var css_style_background[] $return */
3983 $return = array();
3984 $unknownbits = array();
3986 $color = self::NULL_VALUE;
3987 if (count($bits) > 0 && css_is_colour(reset($bits))) {
3988 $color = array_shift($bits);
3991 $image = self::NULL_VALUE;
3992 if (count($bits) > 0 && preg_match('#^\s*(none|inherit|url\(\))\s*$#', reset($bits))) {
3993 $image = array_shift($bits);
3994 if ($image == 'url()') {
3995 $image = "url({$imageurl})";
3999 $repeat = self::NULL_VALUE;
4000 if (count($bits) > 0 && in_array(reset($bits), $repeats)) {
4001 $repeat = array_shift($bits);
4004 $attachment = self::NULL_VALUE;
4005 if (count($bits) > 0 && in_array(reset($bits), $attachments)) {
4006 // Scroll , fixed, inherit.
4007 $attachment = array_shift($bits);
4010 $position = self::NULL_VALUE;
4011 if (count($bits) > 0) {
4012 $widthbits = array();
4013 foreach ($bits as $bit) {
4014 if (in_array($bit, $positions) || css_is_width($bit)) {
4015 $widthbits[] = $bit;
4016 } else {
4017 $unknownbits[] = $bit;
4020 if (count($widthbits)) {
4021 $position = join(' ', $widthbits);
4025 if (count($unknownbits)) {
4026 foreach ($unknownbits as $bit) {
4027 $bit = trim($bit);
4028 if ($color === self::NULL_VALUE && css_is_colour($bit)) {
4029 $color = $bit;
4030 } else if ($repeat === self::NULL_VALUE && in_array($bit, $repeats)) {
4031 $repeat = $bit;
4032 } else if ($attachment === self::NULL_VALUE && in_array($bit, $attachments)) {
4033 $attachment = $bit;
4034 } else if ($bit !== '') {
4035 $advanced = css_style_background_advanced::init($bit);
4036 if ($important) {
4037 $advanced->set_important();
4039 $return[] = $advanced;
4044 if ($color === self::NULL_VALUE &&
4045 $image === self::NULL_VALUE &&
4046 $repeat === self::NULL_VALUE && $attachment === self::NULL_VALUE &&
4047 $position === self::NULL_VALUE) {
4048 // All primaries are null, return without doing anything else. There may be advanced madness there.
4049 return $return;
4052 $return[] = css_style_backgroundcolor::init($color);
4053 $return[] = css_style_backgroundimage::init($image);
4054 $return[] = css_style_backgroundrepeat::init($repeat);
4055 $return[] = css_style_backgroundattachment::init($attachment);
4056 $return[] = css_style_backgroundposition::init($position);
4058 if ($important) {
4059 foreach ($return as $style) {
4060 $style->set_important();
4064 return $return;
4068 * Static helper method to switch in bracket replacements
4070 * @param string $value
4071 * @param array $placeholders
4072 * @return string
4074 protected static function replace_bracket_placeholders($value, array $placeholders) {
4075 while (preg_match('/##BRACKET-\d+##/', $value, $matches)) {
4076 $value = str_replace($matches[0], $placeholders[$matches[0]], $value);
4078 return $value;
4082 * Consolidates background styles into a single background style
4084 * @param css_style_background[] $styles Consolidates the provided array of background styles
4085 * @return css_style[] Consolidated optimised background styles
4087 public static function consolidate(array $styles) {
4089 if (empty($styles)) {
4090 return $styles;
4093 $color = null;
4094 $image = null;
4095 $repeat = null;
4096 $attachment = null;
4097 $position = null;
4098 $size = null;
4099 $origin = null;
4100 $clip = null;
4102 $someimportant = false;
4103 $allimportant = null;
4104 foreach ($styles as $style) {
4105 if ($style instanceof css_style_backgroundimage_advanced) {
4106 continue;
4108 if ($style->is_important()) {
4109 $someimportant = true;
4110 if ($allimportant === null) {
4111 $allimportant = true;
4113 } else if ($allimportant === true) {
4114 $allimportant = false;
4118 /* @var css_style[] $organisedstyles */
4119 $organisedstyles = array();
4120 /* @var css_style[] $advancedstyles */
4121 $advancedstyles = array();
4122 /* @var css_style[] $importantstyles */
4123 $importantstyles = array();
4124 foreach ($styles as $style) {
4125 if ($style instanceof css_style_backgroundimage_advanced) {
4126 $advancedstyles[] = $style;
4127 continue;
4129 if ($someimportant && !$allimportant && $style->is_important()) {
4130 $importantstyles[] = $style;
4131 continue;
4133 $organisedstyles[$style->get_name()] = $style;
4134 switch ($style->get_name()) {
4135 case 'background-color' :
4136 $color = css_style_color::shrink_value($style->get_value(false));
4137 break;
4138 case 'background-image' :
4139 $image = $style->get_value(false);
4140 break;
4141 case 'background-repeat' :
4142 $repeat = $style->get_value(false);
4143 break;
4144 case 'background-attachment' :
4145 $attachment = $style->get_value(false);
4146 break;
4147 case 'background-position' :
4148 $position = $style->get_value(false);
4149 break;
4150 case 'background-clip' :
4151 $clip = $style->get_value();
4152 break;
4153 case 'background-origin' :
4154 $origin = $style->get_value();
4155 break;
4156 case 'background-size' :
4157 $size = $style->get_value();
4158 break;
4162 /* @var css_style[] $consolidatetosingle */
4163 $consolidatetosingle = array();
4164 if (!is_null($color) && !is_null($image) && !is_null($repeat) && !is_null($attachment) && !is_null($position)) {
4165 // We can use the shorthand background-style!
4166 if (!$organisedstyles['background-color']->is_special_empty_value()) {
4167 $consolidatetosingle[] = $color;
4169 if (!$organisedstyles['background-image']->is_special_empty_value()) {
4170 $consolidatetosingle[] = $image;
4172 if (!$organisedstyles['background-repeat']->is_special_empty_value()) {
4173 $consolidatetosingle[] = $repeat;
4175 if (!$organisedstyles['background-attachment']->is_special_empty_value()) {
4176 $consolidatetosingle[] = $attachment;
4178 if (!$organisedstyles['background-position']->is_special_empty_value()) {
4179 $consolidatetosingle[] = $position;
4181 // Reset them all to null so we don't use them again.
4182 $color = null;
4183 $image = null;
4184 $repeat = null;
4185 $attachment = null;
4186 $position = null;
4189 $return = array();
4190 // Single background style needs to come first.
4191 if (count($consolidatetosingle) > 0) {
4192 $returnstyle = new css_style_background('background', join(' ', $consolidatetosingle));
4193 if ($allimportant) {
4194 $returnstyle->set_important();
4196 $return[] = $returnstyle;
4198 foreach ($styles as $style) {
4199 $value = null;
4200 switch ($style->get_name()) {
4201 case 'background-color' :
4202 $value = $color;
4203 break;
4204 case 'background-image' :
4205 $value = $image;
4206 break;
4207 case 'background-repeat' :
4208 $value = $repeat;
4209 break;
4210 case 'background-attachment' :
4211 $value = $attachment;
4212 break;
4213 case 'background-position' :
4214 $value = $position;
4215 break;
4216 case 'background-clip' :
4217 $value = $clip;
4218 break;
4219 case 'background-origin':
4220 $value = $origin;
4221 break;
4222 case 'background-size':
4223 $value = $size;
4224 break;
4226 if (!is_null($value)) {
4227 $return[] = $style;
4230 $return = array_merge($return, $importantstyles, $advancedstyles);
4231 return $return;
4236 * A advanced background style that allows multiple values to preserve unknown entities
4238 * @package core
4239 * @subpackage cssoptimiser
4240 * @copyright 2012 Sam Hemelryk
4241 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4243 class css_style_background_advanced extends css_style_generic {
4245 * Creates a new background colour style
4247 * @param string $value The value of the style
4248 * @return css_style_backgroundimage
4250 public static function init($value) {
4251 $value = preg_replace('#\s+#', ' ', $value);
4252 return new css_style_background_advanced('background', $value);
4256 * Returns true because the advanced background image supports multiple values.
4257 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4259 * @return boolean
4261 public function allows_multiple_values() {
4262 return true;
4267 * A background colour style.
4269 * Based upon the colour style.
4271 * @package core
4272 * @subpackage cssoptimiser
4273 * @copyright 2012 Sam Hemelryk
4274 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4276 class css_style_backgroundcolor extends css_style_color {
4279 * Creates a new background colour style
4281 * @param string $value The value of the style
4282 * @return css_style_backgroundcolor
4284 public static function init($value) {
4285 return new css_style_backgroundcolor('background-color', $value);
4289 * css_style_backgroundcolor consolidates to css_style_background
4291 * @return string
4293 public function consolidate_to() {
4294 return 'background';
4298 * Returns true if the value for this style is the special null value.
4300 * This occurs if the shorthand background property was used but no proper value
4301 * was specified for this style.
4302 * This leads to a null value being used unless otherwise overridden.
4304 * @return bool
4306 public function is_special_empty_value() {
4307 return ($this->value === self::NULL_VALUE);
4311 * Returns true if the value for this style is valid
4312 * @return bool
4314 public function is_valid() {
4315 return $this->is_special_empty_value() || parent::is_valid();
4320 * A background image style.
4322 * @package core
4323 * @subpackage cssoptimiser
4324 * @copyright 2012 Sam Hemelryk
4325 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4327 class css_style_backgroundimage extends css_style_generic {
4330 * Creates a new background image style
4332 * @param string $value The value of the style
4333 * @return css_style_backgroundimage
4335 public static function init($value) {
4336 if ($value !== self::NULL_VALUE && !preg_match('#^\s*(none|inherit|url\()#i', $value)) {
4337 return css_style_backgroundimage_advanced::init($value);
4339 return new css_style_backgroundimage('background-image', $value);
4343 * Consolidates this style into a single background style
4345 * @return string
4347 public function consolidate_to() {
4348 return 'background';
4352 * Returns true if the value for this style is the special null value.
4354 * This occurs if the shorthand background property was used but no proper value
4355 * was specified for this style.
4356 * This leads to a null value being used unless otherwise overridden.
4358 * @return bool
4360 public function is_special_empty_value() {
4361 return ($this->value === self::NULL_VALUE);
4365 * Returns true if the value for this style is valid
4366 * @return bool
4368 public function is_valid() {
4369 return $this->is_special_empty_value() || parent::is_valid();
4374 * A background image style that supports multiple values and masquerades as a background-image
4376 * @package core
4377 * @subpackage cssoptimiser
4378 * @copyright 2012 Sam Hemelryk
4379 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4381 class css_style_backgroundimage_advanced extends css_style_generic {
4383 * Creates a new background colour style
4385 * @param string $value The value of the style
4386 * @return css_style_backgroundimage
4388 public static function init($value) {
4389 $value = preg_replace('#\s+#', ' ', $value);
4390 return new css_style_backgroundimage_advanced('background-image', $value);
4394 * Returns true because the advanced background image supports multiple values.
4395 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4397 * @return boolean
4399 public function allows_multiple_values() {
4400 return true;
4405 * A background repeat style.
4407 * @package core
4408 * @subpackage cssoptimiser
4409 * @copyright 2012 Sam Hemelryk
4410 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4412 class css_style_backgroundrepeat extends css_style_generic {
4415 * Creates a new background colour style
4417 * @param string $value The value of the style
4418 * @return css_style_backgroundrepeat
4420 public static function init($value) {
4421 return new css_style_backgroundrepeat('background-repeat', $value);
4425 * Consolidates this style into a single background style
4427 * @return string
4429 public function consolidate_to() {
4430 return 'background';
4434 * Returns true if the value for this style is the special null value.
4436 * This occurs if the shorthand background property was used but no proper value
4437 * was specified for this style.
4438 * This leads to a null value being used unless otherwise overridden.
4440 * @return bool
4442 public function is_special_empty_value() {
4443 return ($this->value === self::NULL_VALUE);
4447 * Returns true if the value for this style is valid
4448 * @return bool
4450 public function is_valid() {
4451 return $this->is_special_empty_value() || parent::is_valid();
4456 * A background attachment style.
4458 * @package core
4459 * @subpackage cssoptimiser
4460 * @copyright 2012 Sam Hemelryk
4461 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4463 class css_style_backgroundattachment extends css_style_generic {
4466 * Creates a new background colour style
4468 * @param string $value The value of the style
4469 * @return css_style_backgroundattachment
4471 public static function init($value) {
4472 return new css_style_backgroundattachment('background-attachment', $value);
4476 * Consolidates this style into a single background style
4478 * @return string
4480 public function consolidate_to() {
4481 return 'background';
4485 * Returns true if the value for this style is the special null value.
4487 * This occurs if the shorthand background property was used but no proper value
4488 * was specified for this style.
4489 * This leads to a null value being used unless otherwise overridden.
4491 * @return bool
4493 public function is_special_empty_value() {
4494 return ($this->value === self::NULL_VALUE);
4498 * Returns true if the value for this style is valid
4499 * @return bool
4501 public function is_valid() {
4502 return $this->is_special_empty_value() || parent::is_valid();
4507 * A background position style.
4509 * @package core
4510 * @subpackage cssoptimiser
4511 * @copyright 2012 Sam Hemelryk
4512 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4514 class css_style_backgroundposition extends css_style_generic {
4517 * Creates a new background colour style
4519 * @param string $value The value of the style
4520 * @return css_style_backgroundposition
4522 public static function init($value) {
4523 return new css_style_backgroundposition('background-position', $value);
4527 * Consolidates this style into a single background style
4529 * @return string
4531 public function consolidate_to() {
4532 return 'background';
4536 * Returns true if the value for this style is the special null value.
4538 * This occurs if the shorthand background property was used but no proper value
4539 * was specified for this style.
4540 * This leads to a null value being used unless otherwise overridden.
4542 * @return bool
4544 public function is_special_empty_value() {
4545 return ($this->value === self::NULL_VALUE);
4549 * Returns true if the value for this style is valid
4550 * @return bool
4552 public function is_valid() {
4553 return $this->is_special_empty_value() || parent::is_valid();
4558 * A background size style.
4560 * @package core
4561 * @subpackage cssoptimiser
4562 * @copyright 2012 Sam Hemelryk
4563 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4565 class css_style_backgroundsize extends css_style_generic {
4568 * Creates a new background size style
4570 * @param string $value The value of the style
4571 * @return css_style_backgroundposition
4573 public static function init($value) {
4574 return new css_style_backgroundsize('background-size', $value);
4578 * Consolidates this style into a single background style
4580 * @return string
4582 public function consolidate_to() {
4583 return 'background';
4588 * A background clip style.
4590 * @package core
4591 * @subpackage cssoptimiser
4592 * @copyright 2012 Sam Hemelryk
4593 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4595 class css_style_backgroundclip extends css_style_generic {
4598 * Creates a new background clip style
4600 * @param string $value The value of the style
4601 * @return css_style_backgroundposition
4603 public static function init($value) {
4604 return new css_style_backgroundclip('background-clip', $value);
4608 * Consolidates this style into a single background style
4610 * @return string
4612 public function consolidate_to() {
4613 return 'background';
4618 * A background origin style.
4620 * @package core
4621 * @subpackage cssoptimiser
4622 * @copyright 2012 Sam Hemelryk
4623 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4625 class css_style_backgroundorigin extends css_style_generic {
4628 * Creates a new background origin style
4630 * @param string $value The value of the style
4631 * @return css_style_backgroundposition
4633 public static function init($value) {
4634 return new css_style_backgroundorigin('background-origin', $value);
4638 * Consolidates this style into a single background style
4640 * @return string
4642 public function consolidate_to() {
4643 return 'background';
4648 * A padding style.
4650 * @package core
4651 * @subpackage cssoptimiser
4652 * @copyright 2012 Sam Hemelryk
4653 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4655 class css_style_padding extends css_style_width implements core_css_consolidatable_style {
4658 * Initialises this padding style into several individual padding styles
4660 * @param string $value The value fo the style
4661 * @return array An array of padding styles
4663 public static function init($value) {
4664 $important = '';
4665 if (strpos($value, '!important') !== false) {
4666 $important = ' !important';
4667 $value = str_replace('!important', '', $value);
4670 $value = preg_replace('#\s+#', ' ', trim($value));
4671 $bits = explode(' ', $value, 4);
4673 $top = $right = $bottom = $left = null;
4674 if (count($bits) > 0) {
4675 $top = $right = $bottom = $left = array_shift($bits);
4677 if (count($bits) > 0) {
4678 $right = $left = array_shift($bits);
4680 if (count($bits) > 0) {
4681 $bottom = array_shift($bits);
4683 if (count($bits) > 0) {
4684 $left = array_shift($bits);
4686 return array(
4687 new css_style_paddingtop('padding-top', $top.$important),
4688 new css_style_paddingright('padding-right', $right.$important),
4689 new css_style_paddingbottom('padding-bottom', $bottom.$important),
4690 new css_style_paddingleft('padding-left', $left.$important)
4695 * Consolidates several padding styles into a single style.
4697 * @param css_style_padding[] $styles Array of padding styles
4698 * @return css_style[] Optimised+consolidated array of padding styles
4700 public static function consolidate(array $styles) {
4701 if (count($styles) != 4) {
4702 return $styles;
4705 $someimportant = false;
4706 $allimportant = null;
4707 $notimportantequal = null;
4708 $firstvalue = null;
4709 foreach ($styles as $style) {
4710 if ($style->is_important()) {
4711 $someimportant = true;
4712 if ($allimportant === null) {
4713 $allimportant = true;
4715 } else {
4716 if ($allimportant === true) {
4717 $allimportant = false;
4719 if ($firstvalue == null) {
4720 $firstvalue = $style->get_value(false);
4721 $notimportantequal = true;
4722 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
4723 $notimportantequal = false;
4728 if ($someimportant && !$allimportant && !$notimportantequal) {
4729 return $styles;
4732 if ($someimportant && !$allimportant && $notimportantequal) {
4733 $return = array(
4734 new css_style_padding('padding', $firstvalue)
4736 foreach ($styles as $style) {
4737 if ($style->is_important()) {
4738 $return[] = $style;
4741 return $return;
4742 } else {
4743 $top = null;
4744 $right = null;
4745 $bottom = null;
4746 $left = null;
4747 foreach ($styles as $style) {
4748 switch ($style->get_name()) {
4749 case 'padding-top' :
4750 $top = $style->get_value(false);
4751 break;
4752 case 'padding-right' :
4753 $right = $style->get_value(false);
4754 break;
4755 case 'padding-bottom' :
4756 $bottom = $style->get_value(false);
4757 break;
4758 case 'padding-left' :
4759 $left = $style->get_value(false);
4760 break;
4763 if ($top == $bottom && $left == $right) {
4764 if ($top == $left) {
4765 $returnstyle = new css_style_padding('padding', $top);
4766 } else {
4767 $returnstyle = new css_style_padding('padding', "{$top} {$left}");
4769 } else if ($left == $right) {
4770 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom}");
4771 } else {
4772 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom} {$left}");
4774 if ($allimportant) {
4775 $returnstyle->set_important();
4777 return array($returnstyle);
4783 * A padding top style.
4785 * @package core
4786 * @subpackage cssoptimiser
4787 * @copyright 2012 Sam Hemelryk
4788 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4790 class css_style_paddingtop extends css_style_padding {
4793 * Initialises this style
4795 * @param string $value The value of the style
4796 * @return css_style_paddingtop
4798 public static function init($value) {
4799 return new css_style_paddingtop('padding-top', $value);
4803 * Consolidates this style into a single padding style
4805 * @return string
4807 public function consolidate_to() {
4808 return 'padding';
4813 * A padding right style.
4815 * @package core
4816 * @subpackage cssoptimiser
4817 * @copyright 2012 Sam Hemelryk
4818 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4820 class css_style_paddingright extends css_style_padding {
4823 * Initialises this style
4825 * @param string $value The value of the style
4826 * @return css_style_paddingright
4828 public static function init($value) {
4829 return new css_style_paddingright('padding-right', $value);
4833 * Consolidates this style into a single padding style
4835 * @return string
4837 public function consolidate_to() {
4838 return 'padding';
4843 * A padding bottom style.
4845 * @package core
4846 * @subpackage cssoptimiser
4847 * @copyright 2012 Sam Hemelryk
4848 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4850 class css_style_paddingbottom extends css_style_padding {
4853 * Initialises this style
4855 * @param string $value The value of the style
4856 * @return css_style_paddingbottom
4858 public static function init($value) {
4859 return new css_style_paddingbottom('padding-bottom', $value);
4863 * Consolidates this style into a single padding style
4865 * @return string
4867 public function consolidate_to() {
4868 return 'padding';
4873 * A padding left style.
4875 * @package core
4876 * @subpackage cssoptimiser
4877 * @copyright 2012 Sam Hemelryk
4878 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4880 class css_style_paddingleft extends css_style_padding {
4883 * Initialises this style
4885 * @param string $value The value of the style
4886 * @return css_style_paddingleft
4888 public static function init($value) {
4889 return new css_style_paddingleft('padding-left', $value);
4893 * Consolidates this style into a single padding style
4895 * @return string
4897 public function consolidate_to() {
4898 return 'padding';
4903 * A cursor style.
4905 * @package core
4906 * @subpackage cssoptimiser
4907 * @copyright 2012 Sam Hemelryk
4908 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4910 class css_style_cursor extends css_style_generic {
4912 * Initialises a new cursor style
4913 * @param string $value
4914 * @return css_style_cursor
4916 public static function init($value) {
4917 return new css_style_cursor('cursor', $value);
4920 * Cleans the given value and returns it.
4922 * @param string $value
4923 * @return string
4925 protected function clean_value($value) {
4926 // Allowed values for the cursor style.
4927 $allowed = array('auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize',
4928 'pointer', 'progress', 's-resize', 'se-resize', 'sw-resize', 'text', 'w-resize', 'wait', 'inherit');
4929 // Has to be one of the allowed values of an image to use. Loosely match the image... doesn't need to be thorough.
4930 if (!in_array($value, $allowed) && !preg_match('#\.[a-zA-Z0-9_\-]{1,5}$#', $value)) {
4931 $this->set_error('Invalid or unexpected cursor value specified: '.$value);
4933 return trim($value);
4938 * A vertical alignment style.
4940 * @package core
4941 * @subpackage cssoptimiser
4942 * @copyright 2012 Sam Hemelryk
4943 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4945 class css_style_verticalalign extends css_style_generic {
4947 * Initialises a new vertical alignment style
4948 * @param string $value
4949 * @return css_style_verticalalign
4951 public static function init($value) {
4952 return new css_style_verticalalign('vertical-align', $value);
4955 * Cleans the given value and returns it.
4957 * @param string $value
4958 * @return string
4960 protected function clean_value($value) {
4961 $allowed = array('baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'inherit');
4962 if (!css_is_width($value) && !in_array($value, $allowed)) {
4963 $this->set_error('Invalid vertical-align value specified: '.$value);
4965 return trim($value);
4970 * A float style.
4972 * @package core
4973 * @subpackage cssoptimiser
4974 * @copyright 2012 Sam Hemelryk
4975 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4977 class css_style_float extends css_style_generic {
4979 * Initialises a new float style
4980 * @param string $value
4981 * @return css_style_float
4983 public static function init($value) {
4984 return new css_style_float('float', $value);
4987 * Cleans the given value and returns it.
4989 * @param string $value
4990 * @return string
4992 protected function clean_value($value) {
4993 $allowed = array('left', 'right', 'none', 'inherit');
4994 if (!css_is_width($value) && !in_array($value, $allowed)) {
4995 $this->set_error('Invalid float value specified: '.$value);
4997 return trim($value);