MDL-33699: Allow grades of 0 in assignment quickgrading
[moodle.git] / lib / csslib.php
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.
13 //
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
19  *
20  * Please see the {@see css_optimiser} class for greater detail.
21  *
22  * @package core_css
23  * @category css
24  * @copyright 2012 Sam Hemelryk
25  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
26  */
28 //NOTE: do not verify MOODLE_INTERNAL here, this is used from themes too
30 /**
31  * Stores CSS in a file at the given path.
32  *
33  * This function either succeeds or throws an exception.
34  *
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  */
39 function css_store_css(theme_config $theme, $csspath, array $cssfiles) {
40     global $CFG;
42     // Check if both the CSS optimiser is enabled and the theme supports it.
43     if (!empty($CFG->enablecssoptimiser) && $theme->supportscssoptimisation) {
44         // This is an experimental feature introduced in Moodle 2.3
45         // The CSS optimiser organises the CSS in order to reduce the overall number
46         // of rules and styles being sent to the client. It does this by collating
47         // the CSS before it is cached removing excess styles and rules and stripping
48         // out any extraneous content such as comments and empty rules.
49         $optimiser = new css_optimiser;
50         $css = '';
51         foreach ($cssfiles as $file) {
52             $css .= file_get_contents($file)."\n";
53         }
54         $css = $theme->post_process($css);
55         $css = $optimiser->process($css);
57         // If cssoptimisestats is set then stats from the optimisation are collected
58         // and output at the beginning of the CSS
59         if (!empty($CFG->cssoptimiserstats)) {
60             $css = $optimiser->output_stats_css().$css;
61         }
62     } else {
63         // This is the default behaviour.
64         // The cssoptimise setting was introduced in Moodle 2.3 and will hopefully
65         // in the future be changed from an experimental setting to the default.
66         // The css_minify_css will method will use the Minify library remove
67         // comments, additional whitespace and other minor measures to reduce the
68         // the overall CSS being sent.
69         // However it has the distinct disadvantage of having to minify the CSS
70         // before running the post process functions. Potentially things may break
71         // here if theme designers try to push things with CSS post processing.
72         $css = $theme->post_process(css_minify_css($cssfiles));
73     }
75     clearstatcache();
76     if (!file_exists(dirname($csspath))) {
77         @mkdir(dirname($csspath), $CFG->directorypermissions, true);
78     }
80     // Prevent serving of incomplete file from concurrent request,
81     // the rename() should be more atomic than fwrite().
82     ignore_user_abort(true);
83     if ($fp = fopen($csspath.'.tmp', 'xb')) {
84         fwrite($fp, $css);
85         fclose($fp);
86         rename($csspath.'.tmp', $csspath);
87         @chmod($csspath, $CFG->filepermissions);
88         @unlink($csspath.'.tmp'); // just in case anything fails
89     }
90     ignore_user_abort(false);
91     if (connection_aborted()) {
92         die;
93     }
94 }
96 /**
97  * Sends IE specific CSS
98  *
99  * In writing the CSS parser I have a theory that we could optimise the CSS
100  * then split it based upon the number of selectors to ensure we dont' break IE
101  * and that we include only as many sub-stylesheets as we require.
102  * Of course just a theory but may be fun to code.
103  *
104  * @param string $themename The name of the theme we are sending CSS for.
105  * @param string $rev The revision to ensure we utilise the cache.
106  * @param string $etag The revision to ensure we utilise the cache.
107  * @param bool $slasharguments
108  */
109 function css_send_ie_css($themename, $rev, $etag, $slasharguments) {
110     global $CFG;
112     $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often
114     $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot);
116     $css  = "/** Unfortunately IE6/7 does not support more than 4096 selectors in one CSS file, which means we have to use some ugly hacks :-( **/";
117     if ($slasharguments) {
118         $css .= "\n@import url($relroot/styles.php/$themename/$rev/plugins);";
119         $css .= "\n@import url($relroot/styles.php/$themename/$rev/parents);";
120         $css .= "\n@import url($relroot/styles.php/$themename/$rev/theme);";
121     } else {
122         $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=plugins);";
123         $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=parents);";
124         $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=theme);";
125     }
127     header('Etag: '.$etag);
128     header('Content-Disposition: inline; filename="styles.php"');
129     header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
130     header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
131     header('Pragma: ');
132     header('Cache-Control: public, max-age='.$lifetime);
133     header('Accept-Ranges: none');
134     header('Content-Type: text/css; charset=utf-8');
135     header('Content-Length: '.strlen($css));
137     echo $css;
138     die;
141 /**
142  * Sends a cached CSS file
143  *
144  * This function sends the cached CSS file. Remember it is generated on the first
145  * request, then optimised/minified, and finally cached for serving.
146  *
147  * @param string $csspath The path to the CSS file we want to serve.
148  * @param string $etag The revision to make sure we utilise any caches.
149  */
150 function css_send_cached_css($csspath, $etag) {
151     $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often
153     header('Etag: '.$etag);
154     header('Content-Disposition: inline; filename="styles.php"');
155     header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
156     header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
157     header('Pragma: ');
158     header('Cache-Control: public, max-age='.$lifetime);
159     header('Accept-Ranges: none');
160     header('Content-Type: text/css; charset=utf-8');
161     if (!min_enable_zlib_compression()) {
162         header('Content-Length: '.filesize($csspath));
163     }
165     readfile($csspath);
166     die;
169 /**
170  * Sends CSS directly without caching it.
171  *
172  * This function takes a raw CSS string, optimises it if required, and then
173  * serves it.
174  * Turning both themedesignermode and CSS optimiser on at the same time is aweful
175  * for performance because of the optimiser running here. However it was done so
176  * that theme designers could utilise the optimised output during development to
177  * help them optimise their CSS... not that they should write lazy CSS.
178  *
179  * @param string CSS
180  */
181 function css_send_uncached_css($css, $themesupportsoptimisation = true) {
182     global $CFG;
184     header('Content-Disposition: inline; filename="styles_debug.php"');
185     header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
186     header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT');
187     header('Pragma: ');
188     header('Accept-Ranges: none');
189     header('Content-Type: text/css; charset=utf-8');
191     if (is_array($css)) {
192         $css = implode("\n\n", $css);
193     }
195     echo $css;
197     die;
200 /**
201  * Send file not modified headers
202  * @param int $lastmodified
203  * @param string $etag
204  */
205 function css_send_unmodified($lastmodified, $etag) {
206     $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often
207     header('HTTP/1.1 304 Not Modified');
208     header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
209     header('Cache-Control: public, max-age='.$lifetime);
210     header('Content-Type: text/css; charset=utf-8');
211     header('Etag: '.$etag);
212     if ($lastmodified) {
213         header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
214     }
215     die;
218 /**
219  * Sends a 404 message about CSS not being found.
220  */
221 function css_send_css_not_found() {
222     header('HTTP/1.0 404 not found');
223     die('CSS was not found, sorry.');
226 /**
227  * Uses the minify library to compress CSS.
228  *
229  * This is used if $CFG->enablecssoptimiser has been turned off. This was
230  * the original CSS optimisation library.
231  * It removes whitespace and shrinks things but does no apparent optimisation.
232  * Note the minify library is still being used for JavaScript.
233  *
234  * @param array $files An array of files to minify
235  * @return string The minified CSS
236  */
237 function css_minify_css($files) {
238     global $CFG;
240     if (empty($files)) {
241         return '';
242     }
244     set_include_path($CFG->libdir . '/minify/lib' . PATH_SEPARATOR . get_include_path());
245     require_once('Minify.php');
247     if (0 === stripos(PHP_OS, 'win')) {
248         Minify::setDocRoot(); // IIS may need help
249     }
250     // disable all caching, we do it in moodle
251     Minify::setCache(null, false);
253     $options = array(
254         'bubbleCssImports' => false,
255         // Don't gzip content we just want text for storage
256         'encodeOutput' => false,
257         // Maximum age to cache, not used but required
258         'maxAge' => (60*60*24*20),
259         // The files to minify
260         'files' => $files,
261         // Turn orr URI rewriting
262         'rewriteCssUris' => false,
263         // This returns the CSS rather than echoing it for display
264         'quiet' => true
265     );
267     $error = 'unknown';
268     try {
269         $result = Minify::serve('Files', $options);
270         if ($result['success']) {
271             return $result['content'];
272         }
273     } catch (Exception $e) {
274         $error = $e->getMessage();
275         $error = str_replace("\r", ' ', $error);
276         $error = str_replace("\n", ' ', $error);
277     }
279     // minification failed - try to inform the theme developer and include the non-minified version
280     $css = <<<EOD
281 /* Error: $error */
282 /* Problem detected during theme CSS minimisation, please review the following code */
283 /* ================================================================================ */
286 EOD;
287     foreach ($files as $cssfile) {
288         $css .= file_get_contents($cssfile)."\n";
289     }
290     return $css;
293 /**
294  * Determines if the given value is a valid CSS colour.
295  *
296  * A CSS colour can be one of the following:
297  *    - Hex colour:  #AA66BB
298  *    - RGB colour:  rgb(0-255, 0-255, 0-255)
299  *    - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1)
300  *    - HSL colour:  hsl(0-360, 0-100%, 0-100%)
301  *    - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1)
302  *
303  * Or a recognised browser colour mapping {@see css_optimiser::$htmlcolours}
304  *
305  * @param string $value The colour value to check
306  * @return bool
307  */
308 function css_is_colour($value) {
309     $value = trim($value);
310     if (in_array(strtolower($value), array('inherit'))) {
311         return true;
312     } else if (preg_match('/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/', $value)) {
313         return true;
314     } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
315         return true;
316     } else if (preg_match('#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i', $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
317         // It is an RGB colour
318         return true;
319     } else if (preg_match('#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i', $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
320         // It is an RGBA colour
321         return true;
322     } else if (preg_match('#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i', $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
323         // It is an HSL colour
324         return true;
325     } else if (preg_match('#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i', $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
326         // It is an HSLA colour
327         return true;
328     }
329     // Doesn't look like a colour.
330     return false;
333 /**
334  * Returns true is the passed value looks like a CSS width.
335  * In order to pass this test the value must be purely numerical or end with a
336  * valid CSS unit term.
337  *
338  * @param string|int $value
339  * @return boolean
340  */
341 function css_is_width($value) {
342     $value = trim($value);
343     if (in_array(strtolower($value), array('auto', 'inherit'))) {
344         return true;
345     }
346     if (preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|%|in|cm|mm|ex|pc)?$#i', $value)) {
347         return true;
348     }
349     return false;
352 /**
353  * A simple sorting function to sort two array values on the number of items they contain
354  *
355  * @param array $a
356  * @param array $b
357  * @return int
358  */
359 function css_sort_by_count(array $a, array $b) {
360     $a = count($a);
361     $b = count($b);
362     if ($a == $b) {
363         return 0;
364     }
365     return ($a > $b) ? -1 : 1;
368 /**
369  * A basic CSS optimiser that strips out unwanted things and then processing the
370  * CSS organising styles and moving duplicates and useless CSS.
371  *
372  * This CSS optimiser works by reading through a CSS string one character at a
373  * time and building an object structure of the CSS.
374  * As part of that processing styles are expanded out as much as they can be to
375  * ensure we collect all mappings, at the end of the processing those styles are
376  * then combined into an optimised form to keep them as short as possible.
377  *
378  * @package core_css
379  * @category css
380  * @copyright 2012 Sam Hemelryk
381  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
382  */
383 class css_optimiser {
385     /**
386      * Used when the processor is about to start processing.
387      * Processing states. Used internally.
388      */
389     const PROCESSING_START = 0;
391     /**
392      * Used when the processor is currently processing a selector.
393      * Processing states. Used internally.
394      */
395     const PROCESSING_SELECTORS = 0;
397     /**
398      * Used when the processor is currently processing a style.
399      * Processing states. Used internally.
400      */
401     const PROCESSING_STYLES = 1;
403     /**
404      * Used when the processor is currently processing a comment.
405      * Processing states. Used internally.
406      */
407     const PROCESSING_COMMENT = 2;
409     /**
410      * Used when the processor is currently processing an @ rule.
411      * Processing states. Used internally.
412      */
413     const PROCESSING_ATRULE = 3;
415     /**
416      * The raw string length before optimisation.
417      * Stats variables set during and after processing
418      * @var int
419      */
420     protected $rawstrlen = 0;
422     /**
423      * The number of comments that were removed during optimisation.
424      * Stats variables set during and after processing
425      * @var int
426      */
427     protected $commentsincss = 0;
429     /**
430      * The number of rules in the CSS before optimisation.
431      * Stats variables set during and after processing
432      * @var int
433      */
434     protected $rawrules = 0;
436     /**
437      * The number of selectors using in CSS rules before optimisation.
438      * Stats variables set during and after processing
439      * @var int
440      */
441     protected $rawselectors = 0;
443     /**
444      * The string length after optimisation.
445      * Stats variables set during and after processing
446      * @var int
447      */
448     protected $optimisedstrlen = 0;
450     /**
451      * The number of rules after optimisation.
452      * Stats variables set during and after processing
453      * @var int
454      */
455     protected $optimisedrules = 0;
457     /**
458      * The number of selectors used in rules after optimisation.
459      * Stats variables set during and after processing
460      * @var int
461      */
462     protected $optimisedselectors = 0;
464     /**
465      * The start time of the optimisation.
466      * Stats variables set during and after processing
467      * @var int
468      */
469     protected $timestart = 0;
471     /**
472      * The end time of the optimisation.
473      * Stats variables set during and after processing
474      * @var int
475      */
476     protected $timecomplete = 0;
478     /**
479      * Will be set to any errors that may have occured during processing.
480      * This is updated only at the end of processing NOT during.
481      *
482      * @var array
483      */
484     protected $errors = array();
486     /**
487      * Processes incoming CSS optimising it and then returning it.
488      *
489      * @param string $css The raw CSS to optimise
490      * @return string The optimised CSS
491      */
492     public function process($css) {
493         global $CFG;
495         $this->reset_stats();
496         $this->timestart = microtime(true);
497         $this->rawstrlen = strlen($css);
499         // First up we need to remove all line breaks - this allows us to instantly
500         // reduce our processing requirements and as we will process everything
501         // into a new structure there's really nothing lost.
502         $css = preg_replace('#\r?\n#', ' ', $css);
504         // Next remove the comments... no need to them in an optimised world and
505         // knowing they're all gone allows us to REALLY make our processing simpler
506         $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
508         $medias = array(
509             'all' => new css_media()
510         );
511         $imports = array();
512         $charset = false;
514         $currentprocess = self::PROCESSING_START;
515         $currentrule = css_rule::init();
516         $currentselector = css_selector::init();
517         $inquotes = false;      // ' or "
518         $inbraces = false;      // {
519         $inbrackets = false;    // [
520         $inparenthesis = false; // (
521         $currentmedia = $medias['all'];
522         $currentatrule = null;
523         $suspectatrule = false;
525         $buffer = '';
526         $char = null;
528         // Next we are going to iterate over every single character in $css.
529         // This is why we removed line breaks and comments!
530         for ($i = 0; $i < $this->rawstrlen; $i++) {
531             $lastchar = $char;
532             $char = substr($css, $i, 1);
533             if ($char == '@' && $buffer == '') {
534                 $suspectatrule = true;
535             }
536             switch ($currentprocess) {
537                 // Start processing an at rule e.g. @media, @page
538                 case self::PROCESSING_ATRULE:
539                     switch ($char) {
540                         case ';':
541                             if (!$inbraces) {
542                                 $buffer .= $char;
543                                 if ($currentatrule == 'import') {
544                                     $imports[] = $buffer;
545                                     $currentprocess = self::PROCESSING_SELECTORS;
546                                 } else if ($currentatrule == 'charset') {
547                                     $charset = $buffer;
548                                     $currentprocess = self::PROCESSING_SELECTORS;
549                                 }
550                             }
551                             $buffer = '';
552                             $currentatrule = false;
553                             // continue 1: The switch processing chars
554                             // continue 2: The switch processing the state
555                             // continue 3: The for loop
556                             continue 3;
557                         case '{':
558                             if ($currentatrule == 'media' && preg_match('#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)#', $buffer, $matches)) {
559                                 $mediatypes = str_replace(' ', '', $matches[1]);
560                                 if (!array_key_exists($mediatypes, $medias)) {
561                                     $medias[$mediatypes] = new css_media($mediatypes);
562                                 }
563                                 $currentmedia = $medias[$mediatypes];
564                                 $currentprocess = self::PROCESSING_SELECTORS;
565                                 $buffer = '';
566                             }
567                             // continue 1: The switch processing chars
568                             // continue 2: The switch processing the state
569                             // continue 3: The for loop
570                             continue 3;
571                     }
572                     break;
573                 // Start processing selectors
574                 case self::PROCESSING_START:
575                 case self::PROCESSING_SELECTORS:
576                     switch ($char) {
577                         case '[':
578                             $inbrackets ++;
579                             $buffer .= $char;
580                             // continue 1: The switch processing chars
581                             // continue 2: The switch processing the state
582                             // continue 3: The for loop
583                             continue 3;
584                         case ']':
585                             $inbrackets --;
586                             $buffer .= $char;
587                             // continue 1: The switch processing chars
588                             // continue 2: The switch processing the state
589                             // continue 3: The for loop
590                             continue 3;
591                         case ' ':
592                             if ($inbrackets) {
593                                 // continue 1: The switch processing chars
594                                 // continue 2: The switch processing the state
595                                 // continue 3: The for loop
596                                 continue 3;
597                             }
598                             if (!empty($buffer)) {
599                                 if ($suspectatrule && preg_match('#@(media|import|charset)\s*#', $buffer, $matches)) {
600                                     $currentatrule = $matches[1];
601                                     $currentprocess = self::PROCESSING_ATRULE;
602                                     $buffer .= $char;
603                                 } else {
604                                     $currentselector->add($buffer);
605                                     $buffer = '';
606                                 }
607                             }
608                             $suspectatrule = false;
609                             // continue 1: The switch processing chars
610                             // continue 2: The switch processing the state
611                             // continue 3: The for loop
612                             continue 3;
613                         case '{':
614                             if ($inbrackets) {
615                                 // continue 1: The switch processing chars
616                                 // continue 2: The switch processing the state
617                                 // continue 3: The for loop
618                                 continue 3;
619                             }
621                             $currentselector->add($buffer);
622                             $currentrule->add_selector($currentselector);
623                             $currentselector = css_selector::init();
624                             $currentprocess = self::PROCESSING_STYLES;
626                             $buffer = '';
627                             // continue 1: The switch processing chars
628                             // continue 2: The switch processing the state
629                             // continue 3: The for loop
630                             continue 3;
631                         case '}':
632                             if ($inbrackets) {
633                                 // continue 1: The switch processing chars
634                                 // continue 2: The switch processing the state
635                                 // continue 3: The for loop
636                                 continue 3;
637                             }
638                             if ($currentatrule == 'media') {
639                                 $currentmedia = $medias['all'];
640                                 $currentatrule = false;
641                                 $buffer = '';
642                             }
643                             // continue 1: The switch processing chars
644                             // continue 2: The switch processing the state
645                             // continue 3: The for loop
646                             continue 3;
647                         case ',':
648                             if ($inbrackets) {
649                                 // continue 1: The switch processing chars
650                                 // continue 2: The switch processing the state
651                                 // continue 3: The for loop
652                                 continue 3;
653                             }
654                             $currentselector->add($buffer);
655                             $currentrule->add_selector($currentselector);
656                             $currentselector = css_selector::init();
657                             $buffer = '';
658                             // continue 1: The switch processing chars
659                             // continue 2: The switch processing the state
660                             // continue 3: The for loop
661                             continue 3;
662                     }
663                     break;
664                 // Start processing styles
665                 case self::PROCESSING_STYLES:
666                     if ($char == '"' || $char == "'") {
667                         if ($inquotes === false) {
668                             $inquotes = $char;
669                         }
670                         if ($inquotes === $char && $lastchar !== '\\') {
671                             $inquotes = false;
672                         }
673                     }
674                     if ($inquotes) {
675                         $buffer .= $char;
676                         continue 2;
677                     }
678                     switch ($char) {
679                         case ';':
680                             if ($inparenthesis) {
681                                 $buffer .= $char;
682                                 // continue 1: The switch processing chars
683                                 // continue 2: The switch processing the state
684                                 // continue 3: The for loop
685                                 continue 3;
686                             }
687                             $currentrule->add_style($buffer);
688                             $buffer = '';
689                             $inquotes = false;
690                             // continue 1: The switch processing chars
691                             // continue 2: The switch processing the state
692                             // continue 3: The for loop
693                             continue 3;
694                         case '}':
695                             $currentrule->add_style($buffer);
696                             $this->rawselectors += $currentrule->get_selector_count();
698                             $currentmedia->add_rule($currentrule);
700                             $currentrule = css_rule::init();
701                             $currentprocess = self::PROCESSING_SELECTORS;
702                             $this->rawrules++;
703                             $buffer = '';
704                             $inquotes = false;
705                             $inparenthesis = false;
706                             // continue 1: The switch processing chars
707                             // continue 2: The switch processing the state
708                             // continue 3: The for loop
709                             continue 3;
710                         case '(':
711                             $inparenthesis = true;
712                             $buffer .= $char;
713                             // continue 1: The switch processing chars
714                             // continue 2: The switch processing the state
715                             // continue 3: The for loop
716                             continue 3;
717                         case ')':
718                             $inparenthesis = false;
719                             $buffer .= $char;
720                             // continue 1: The switch processing chars
721                             // continue 2: The switch processing the state
722                             // continue 3: The for loop
723                             continue 3;
724                     }
725                     break;
726             }
727             $buffer .= $char;
728         }
730         $css = '';
731         if (!empty($charset)) {
732             $imports[] = $charset;
733         }
734         if (!empty($imports)) {
735             $css .= implode("\n", $imports);
736             $css .= "\n\n";
737         }
738         foreach ($medias as $media) {
739             $media->organise_rules_by_selectors();
740             $this->optimisedrules += $media->count_rules();
741             $this->optimisedselectors +=  $media->count_selectors();
742             if ($media->has_errors()) {
743                 $this->errors[] = $media->get_errors();
744             }
745             $css .= $media->out();
746         }
747         $this->optimisedstrlen = strlen($css);
749         $this->timecomplete = microtime(true);
750         return trim($css);
751     }
753     /**
754      * Returns an array of stats from the last processing run
755      * @return string
756      */
757     public function get_stats() {
758         $stats = array(
759             'timestart'             => $this->timestart,
760             'timecomplete'          => $this->timecomplete,
761             'timetaken'             => round($this->timecomplete - $this->timestart, 4),
762             'commentsincss'         => $this->commentsincss,
763             'rawstrlen'             => $this->rawstrlen,
764             'rawselectors'          => $this->rawselectors,
765             'rawrules'              => $this->rawrules,
766             'optimisedstrlen'       => $this->optimisedstrlen,
767             'optimisedrules'        => $this->optimisedrules,
768             'optimisedselectors'     => $this->optimisedselectors,
769             'improvementstrlen'     => round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%',
770             'improvementrules'      => round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%',
771             'improvementselectors'  => round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%',
772         );
773         return $stats;
774     }
776     /**
777      * Returns true if any errors have occured during processing
778      *
779      * @return bool
780      */
781     public function has_errors() {
782         return !empty($this->errors);
783     }
785     /**
786      * Returns an array of errors that have occured
787      *
788      * @return array
789      */
790     public function get_errors() {
791         return $this->errors;
792     }
794     /**
795      * Returns any errors as a string that can be included in CSS.
796      *
797      * @return string
798      */
799     public function output_errors_css() {
800         $computedcss  = "/****************************************\n";
801         $computedcss .= " *--- Errors found during processing ----\n";
802         foreach ($this->errors as $error) {
803             $computedcss .= preg_replace('#^#m', '* ', $error);
804         }
805         $computedcss .= " ****************************************/\n\n";
806         return $computedcss;
807     }
809     /**
810      * Returns a string to display stats about the last generation within CSS output
811      *
812      * @return string
813      */
814     public function output_stats_css() {
815         $stats = $this->get_stats();
817         $strlenimprovement = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1);
818         $ruleimprovement = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1);
819         $selectorimprovement = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1);
820         $timetaken = round($this->timecomplete - $this->timestart, 4);
822         $computedcss  = "/****************************************\n";
823         $computedcss .= " *------- CSS Optimisation stats --------\n";
824         $computedcss .= " *  ".date('r')."\n";
825         $computedcss .= " *  {$stats['commentsincss']}  \t comments removed\n";
826         $computedcss .= " *  Optimisation took {$stats['timetaken']} seconds\n";
827         $computedcss .= " *--------------- before ----------------\n";
828         $computedcss .= " *  {$stats['rawstrlen']}  \t chars read in\n";
829         $computedcss .= " *  {$stats['rawrules']}  \t rules read in\n";
830         $computedcss .= " *  {$stats['rawselectors']}  \t total selectors\n";
831         $computedcss .= " *---------------- after ----------------\n";
832         $computedcss .= " *  {$stats['optimisedstrlen']}  \t chars once optimized\n";
833         $computedcss .= " *  {$stats['optimisedrules']}  \t optimized rules\n";
834         $computedcss .= " *  {$stats['optimisedselectors']}  \t total selectors once optimized\n";
835         $computedcss .= " *---------------- stats ----------------\n";
836         $computedcss .= " *  {$stats['improvementstrlen']}  \t reduction in chars\n";
837         $computedcss .= " *  {$stats['improvementrules']}  \t reduction in rules\n";
838         $computedcss .= " *  {$stats['improvementselectors']}  \t reduction in selectors\n";
839         $computedcss .= " ****************************************/\n\n";
841         return $computedcss;
842     }
844     /**
845      * Resets the stats ready for another fresh processing
846      */
847     public function reset_stats() {
848         $this->commentsincss = 0;
849         $this->optimisedrules = 0;
850         $this->optimisedselectors = 0;
851         $this->optimisedstrlen = 0;
852         $this->rawrules = 0;
853         $this->rawselectors = 0;
854         $this->rawstrlen = 0;
855         $this->timecomplete = 0;
856         $this->timestart = 0;
857     }
859     /**
860      * An array of the common HTML colours that are supported by most browsers.
861      *
862      * This reference table is used to allow us to unify colours, and will aid
863      * us in identifying buggy CSS using unsupported colours.
864      *
865      * @staticvar array
866      * @var array
867      */
868     public static $htmlcolours = array(
869         'aliceblue' => '#F0F8FF',
870         'antiquewhite' => '#FAEBD7',
871         'aqua' => '#00FFFF',
872         'aquamarine' => '#7FFFD4',
873         'azure' => '#F0FFFF',
874         'beige' => '#F5F5DC',
875         'bisque' => '#FFE4C4',
876         'black' => '#000000',
877         'blanchedalmond' => '#FFEBCD',
878         'blue' => '#0000FF',
879         'blueviolet' => '#8A2BE2',
880         'brown' => '#A52A2A',
881         'burlywood' => '#DEB887',
882         'cadetblue' => '#5F9EA0',
883         'chartreuse' => '#7FFF00',
884         'chocolate' => '#D2691E',
885         'coral' => '#FF7F50',
886         'cornflowerblue' => '#6495ED',
887         'cornsilk' => '#FFF8DC',
888         'crimson' => '#DC143C',
889         'cyan' => '#00FFFF',
890         'darkblue' => '#00008B',
891         'darkcyan' => '#008B8B',
892         'darkgoldenrod' => '#B8860B',
893         'darkgray' => '#A9A9A9',
894         'darkgrey' => '#A9A9A9',
895         'darkgreen' => '#006400',
896         'darkKhaki' => '#BDB76B',
897         'darkmagenta' => '#8B008B',
898         'darkolivegreen' => '#556B2F',
899         'arkorange' => '#FF8C00',
900         'darkorchid' => '#9932CC',
901         'darkred' => '#8B0000',
902         'darksalmon' => '#E9967A',
903         'darkseagreen' => '#8FBC8F',
904         'darkslateblue' => '#483D8B',
905         'darkslategray' => '#2F4F4F',
906         'darkslategrey' => '#2F4F4F',
907         'darkturquoise' => '#00CED1',
908         'darkviolet' => '#9400D3',
909         'deeppink' => '#FF1493',
910         'deepskyblue' => '#00BFFF',
911         'dimgray' => '#696969',
912         'dimgrey' => '#696969',
913         'dodgerblue' => '#1E90FF',
914         'firebrick' => '#B22222',
915         'floralwhite' => '#FFFAF0',
916         'forestgreen' => '#228B22',
917         'fuchsia' => '#FF00FF',
918         'gainsboro' => '#DCDCDC',
919         'ghostwhite' => '#F8F8FF',
920         'gold' => '#FFD700',
921         'goldenrod' => '#DAA520',
922         'gray' => '#808080',
923         'grey' => '#808080',
924         'green' => '#008000',
925         'greenyellow' => '#ADFF2F',
926         'honeydew' => '#F0FFF0',
927         'hotpink' => '#FF69B4',
928         'indianred ' => '#CD5C5C',
929         'indigo ' => '#4B0082',
930         'ivory' => '#FFFFF0',
931         'khaki' => '#F0E68C',
932         'lavender' => '#E6E6FA',
933         'lavenderblush' => '#FFF0F5',
934         'lawngreen' => '#7CFC00',
935         'lemonchiffon' => '#FFFACD',
936         'lightblue' => '#ADD8E6',
937         'lightcoral' => '#F08080',
938         'lightcyan' => '#E0FFFF',
939         'lightgoldenrodyellow' => '#FAFAD2',
940         'lightgray' => '#D3D3D3',
941         'lightgrey' => '#D3D3D3',
942         'lightgreen' => '#90EE90',
943         'lightpink' => '#FFB6C1',
944         'lightsalmon' => '#FFA07A',
945         'lightseagreen' => '#20B2AA',
946         'lightskyblue' => '#87CEFA',
947         'lightslategray' => '#778899',
948         'lightslategrey' => '#778899',
949         'lightsteelblue' => '#B0C4DE',
950         'lightyellow' => '#FFFFE0',
951         'lime' => '#00FF00',
952         'limegreen' => '#32CD32',
953         'linen' => '#FAF0E6',
954         'magenta' => '#FF00FF',
955         'maroon' => '#800000',
956         'mediumaquamarine' => '#66CDAA',
957         'mediumblue' => '#0000CD',
958         'mediumorchid' => '#BA55D3',
959         'mediumpurple' => '#9370D8',
960         'mediumseagreen' => '#3CB371',
961         'mediumslateblue' => '#7B68EE',
962         'mediumspringgreen' => '#00FA9A',
963         'mediumturquoise' => '#48D1CC',
964         'mediumvioletred' => '#C71585',
965         'midnightblue' => '#191970',
966         'mintcream' => '#F5FFFA',
967         'mistyrose' => '#FFE4E1',
968         'moccasin' => '#FFE4B5',
969         'navajowhite' => '#FFDEAD',
970         'navy' => '#000080',
971         'oldlace' => '#FDF5E6',
972         'olive' => '#808000',
973         'olivedrab' => '#6B8E23',
974         'orange' => '#FFA500',
975         'orangered' => '#FF4500',
976         'orchid' => '#DA70D6',
977         'palegoldenrod' => '#EEE8AA',
978         'palegreen' => '#98FB98',
979         'paleturquoise' => '#AFEEEE',
980         'palevioletred' => '#D87093',
981         'papayawhip' => '#FFEFD5',
982         'peachpuff' => '#FFDAB9',
983         'peru' => '#CD853F',
984         'pink' => '#FFC0CB',
985         'plum' => '#DDA0DD',
986         'powderblue' => '#B0E0E6',
987         'purple' => '#800080',
988         'red' => '#FF0000',
989         'rosybrown' => '#BC8F8F',
990         'royalblue' => '#4169E1',
991         'saddlebrown' => '#8B4513',
992         'salmon' => '#FA8072',
993         'sandybrown' => '#F4A460',
994         'seagreen' => '#2E8B57',
995         'seashell' => '#FFF5EE',
996         'sienna' => '#A0522D',
997         'silver' => '#C0C0C0',
998         'skyblue' => '#87CEEB',
999         'slateblue' => '#6A5ACD',
1000         'slategray' => '#708090',
1001         'slategrey' => '#708090',
1002         'snow' => '#FFFAFA',
1003         'springgreen' => '#00FF7F',
1004         'steelblue' => '#4682B4',
1005         'tan' => '#D2B48C',
1006         'teal' => '#008080',
1007         'thistle' => '#D8BFD8',
1008         'tomato' => '#FF6347',
1009         'transparent' => 'transparent',
1010         'turquoise' => '#40E0D0',
1011         'violet' => '#EE82EE',
1012         'wheat' => '#F5DEB3',
1013         'white' => '#FFFFFF',
1014         'whitesmoke' => '#F5F5F5',
1015         'yellow' => '#FFFF00',
1016         'yellowgreen' => '#9ACD32'
1017     );
1020 /**
1021  * Used to prepare CSS strings
1022  *
1023  * @package core_css
1024  * @category css
1025  * @copyright 2012 Sam Hemelryk
1026  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1027  */
1028 abstract class css_writer {
1030     /**
1031      * The current indent level
1032      * @var int
1033      */
1034     protected static $indent = 0;
1036     /**
1037      * Returns true if the output should still maintain minimum formatting.
1038      * @return bool
1039      */
1040     protected static function is_pretty() {
1041         global $CFG;
1042         return (!empty($CFG->cssoptimiserpretty));
1043     }
1045     /**
1046      * Returns the indenting char to use for indenting things nicely.
1047      * @return string
1048      */
1049     protected static function get_indent() {
1050         if (self::is_pretty()) {
1051             return str_repeat("  ", self::$indent);
1052         }
1053         return '';
1054     }
1056     /**
1057      * Increases the current indent
1058      */
1059     protected static function increase_indent() {
1060         self::$indent++;
1061     }
1063     /**
1064      * Decreases the current indent
1065      */
1066     protected static function decrease_indent() {
1067         self::$indent--;
1068     }
1070     /**
1071      * Returns the string to use as a separator
1072      * @return string
1073      */
1074     protected static function get_separator() {
1075         return (self::is_pretty())?"\n":' ';
1076     }
1078     /**
1079      * Returns CSS for media
1080      *
1081      * @param string $typestring
1082      * @param array $rules An array of css_rule objects
1083      * @return string
1084      */
1085     public static function media($typestring, array &$rules) {
1086         $nl = self::get_separator();
1088         $output = '';
1089         if ($typestring !== 'all') {
1090             $output .= $nl.$nl."@media {$typestring} {".$nl;
1091             self::increase_indent();
1092         }
1093         foreach ($rules as $rule) {
1094             $output .= $rule->out().$nl;
1095         }
1096         if ($typestring !== 'all') {
1097             self::decrease_indent();
1098             $output .= '}';
1099         }
1100         return $output;
1101     }
1103     /**
1104      * Returns CSS for a rule
1105      *
1106      * @param string $selector
1107      * @param string $styles
1108      * @return string
1109      */
1110     public static function rule($selector, $styles) {
1111         $css = self::get_indent()."{$selector}{{$styles}}";
1112         return $css;
1113     }
1115     /**
1116      * Returns CSS for the selectors of a rule
1117      *
1118      * @param array $selectors Array of css_selector objects
1119      * @return string
1120      */
1121     public static function selectors(array $selectors) {
1122         $nl = self::get_separator();
1123         $selectorstrings = array();
1124         foreach ($selectors as $selector) {
1125             $selectorstrings[] = $selector->out();
1126         }
1127         return join(','.$nl, $selectorstrings);
1128     }
1130     /**
1131      * Returns a selector given the components that make it up.
1132      *
1133      * @param array $components
1134      * @return string
1135      */
1136     public static function selector(array $components) {
1137         return trim(join(' ', $components));
1138     }
1140     /**
1141      * Returns a CSS string for the provided styles
1142      *
1143      * @param array $styles Array of css_style objects
1144      * @return string
1145      */
1146     public static function styles(array $styles) {
1147         $bits = array();
1148         foreach ($styles as $style) {
1149             $bits[] = $style->out();
1150         }
1151         return join('', $bits);
1152     }
1154     /**
1155      * Returns a style CSS
1156      *
1157      * @param string $name
1158      * @param string $value
1159      * @param bool $important
1160      * @return string
1161      */
1162     public static function style($name, $value, $important = false) {
1163         if ($important && strpos($value, '!important') === false) {
1164             $value .= ' !important';
1165         }
1166         return "{$name}:{$value};";
1167     }
1170 /**
1171  * A structure to represent a CSS selector.
1172  *
1173  * The selector is the classes, id, elements, and psuedo bits that make up a CSS
1174  * rule.
1175  *
1176  * @package core_css
1177  * @category css
1178  * @copyright 2012 Sam Hemelryk
1179  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1180  */
1181 class css_selector {
1183     /**
1184      * An array of selector bits
1185      * @var array
1186      */
1187     protected $selectors = array();
1189     /**
1190      * The number of selectors.
1191      * @var int
1192      */
1193     protected $count = 0;
1195     /**
1196      * Initialises a new CSS selector
1197      * @return css_selector
1198      */
1199     public static function init() {
1200         return new css_selector();
1201     }
1203     /**
1204      * CSS selectors can only be created through the init method above.
1205      */
1206     protected function __construct() {}
1208     /**
1209      * Adds a selector to the end of the current selector
1210      * @param string $selector
1211      */
1212     public function add($selector) {
1213         $selector = trim($selector);
1214         $count = 0;
1215         $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
1216         if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
1217             $count ++;
1218         }
1219         $this->count = $count;
1220         $this->selectors[] = $selector;
1221     }
1222     /**
1223      * Returns the number of individual components that make up this selector
1224      * @return int
1225      */
1226     public function get_selector_count() {
1227         return $this->count;
1228     }
1230     /**
1231      * Returns the selector for use in a CSS rule
1232      * @return string
1233      */
1234     public function out() {
1235         return css_writer::selector($this->selectors);
1236     }
1239 /**
1240  * A structure to represent a CSS rule.
1241  *
1242  * @package core_css
1243  * @category css
1244  * @copyright 2012 Sam Hemelryk
1245  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1246  */
1247 class css_rule {
1249     /**
1250      * An array of CSS selectors {@see css_selector}
1251      * @var array
1252      */
1253     protected $selectors = array();
1255     /**
1256      * An array of CSS styles {@see css_style}
1257      * @var array
1258      */
1259     protected $styles = array();
1261     /**
1262      * Created a new CSS rule. This is the only way to create a new CSS rule externally.
1263      * @return css_rule
1264      */
1265     public static function init() {
1266         return new css_rule();
1267     }
1269     /**
1270      * Constructs a new css rule.
1271      *
1272      * @param string $selector The selector or array of selectors that make up this rule.
1273      * @param array $styles An array of styles that belong to this rule.
1274      */
1275     protected function __construct($selector = null, array $styles = array()) {
1276         if ($selector != null) {
1277             if (is_array($selector)) {
1278                 $this->selectors = $selector;
1279             } else {
1280                 $this->selectors = array($selector);
1281             }
1282             $this->add_styles($styles);
1283         }
1284     }
1286     /**
1287      * Adds a new CSS selector to this rule
1288      *
1289      * e.g. $rule->add_selector('.one #two.two');
1290      *
1291      * @param css_selector $selector Adds a CSS selector to this rule.
1292      */
1293     public function add_selector(css_selector $selector) {
1294         $this->selectors[] = $selector;
1295     }
1297     /**
1298      * Adds a new CSS style to this rule.
1299      *
1300      * @param css_style|string $style Adds a new style to this rule
1301      */
1302     public function add_style($style) {
1303         if (is_string($style)) {
1304             $style = trim($style);
1305             if (empty($style)) {
1306                 return;
1307             }
1308             $bits = explode(':', $style, 2);
1309             if (count($bits) == 2) {
1310                 list($name, $value) = array_map('trim', $bits);
1311             }
1312             if (isset($name) && isset($value) && $name !== '' && $value !== '') {
1313                 $style = css_style::init_automatic($name, $value);
1314             }
1315         } else if ($style instanceof css_style) {
1316             // Clone the style as it may be coming from another rule and we don't
1317             // want references as it will likely be overwritten by proceeding
1318             // rules
1319             $style = clone($style);
1320         }
1321         if ($style instanceof css_style) {
1322             $name = $style->get_name();
1323             if (array_key_exists($name, $this->styles)) {
1324                 $this->styles[$name]->set_value($style->get_value());
1325             } else {
1326                 $this->styles[$name] = $style;
1327             }
1328         } else if (is_array($style)) {
1329             // We probably shouldn't worry about processing styles here but to
1330             // be truthful it doesn't hurt.
1331             foreach ($style as $astyle) {
1332                 $this->add_style($astyle);
1333             }
1334         }
1335     }
1337     /**
1338      * An easy method of adding several styles at once. Just calls add_style.
1339      *
1340      * This method simply iterates over the array and calls {@see css_rule::add_style()}
1341      * with each.
1342      *
1343      * @param array $styles Adds an array of styles
1344      */
1345     public function add_styles(array $styles) {
1346         foreach ($styles as $style) {
1347             $this->add_style($style);
1348         }
1349     }
1351     /**
1352      * Returns the array of selectors
1353      *
1354      * @return array
1355      */
1356     public function get_selectors() {
1357         return $this->selectors;
1358     }
1360     /**
1361      * Returns the array of styles
1362      *
1363      * @return array
1364      */
1365     public function get_styles() {
1366         return $this->styles;
1367     }
1369     /**
1370      * Outputs this rule as a fragment of CSS
1371      *
1372      * @return string
1373      */
1374     public function out() {
1375         $selectors = css_writer::selectors($this->selectors);
1376         $styles = css_writer::styles($this->get_consolidated_styles());
1377         return css_writer::rule($selectors, $styles);
1378     }
1380     /**
1381      * Consolidates all styles associated with this rule
1382      *
1383      * @return array An array of consolidated styles
1384      */
1385     public function get_consolidated_styles() {
1386         $finalstyles = array();
1387         $consolidate = array();
1388         foreach ($this->styles as $style) {
1389             $consolidatetoclass = $style->consolidate_to();
1390             if ($style->is_valid() && !empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) {
1391                 $class = 'css_style_'.$consolidatetoclass;
1392                 if (!array_key_exists($class, $consolidate)) {
1393                     $consolidate[$class] = array();
1394                 }
1395                 $consolidate[$class][] = $style;
1396             } else {
1397                 $finalstyles[] = $style;
1398             }
1399         }
1401         foreach ($consolidate as $class => $styles) {
1402             $styles = $class::consolidate($styles);
1403             foreach ($styles as $style) {
1404                 $finalstyles[] = $style;
1405             }
1406         }
1407         return $finalstyles;
1408     }
1410     /**
1411      * Splits this rules into an array of CSS rules. One for each of the selectors
1412      * that make up this rule.
1413      *
1414      * @return array(css_rule)
1415      */
1416     public function split_by_selector() {
1417         $return = array();
1418         foreach ($this->selectors as $selector) {
1419             $return[] = new css_rule($selector, $this->styles);
1420         }
1421         return $return;
1422     }
1424     /**
1425      * Splits this rule into an array of rules. One for each of the styles that
1426      * make up this rule
1427      *
1428      * @return array Array of css_rule objects
1429      */
1430     public function split_by_style() {
1431         $return = array();
1432         foreach ($this->styles as $style) {
1433             $return[] = new css_rule($this->selectors, array($style));
1434         }
1435         return $return;
1436     }
1438     /**
1439      * Gets a hash for the styles of this rule
1440      *
1441      * @return string
1442      */
1443     public function get_style_hash() {
1444         return md5(css_writer::styles($this->styles));
1445     }
1447     /**
1448      * Gets a hash for the selectors of this rule
1449      *
1450      * @return string
1451      */
1452     public function get_selector_hash() {
1453         return md5(css_writer::selectors($this->selectors));
1454     }
1456     /**
1457      * Gets the number of selectors that make up this rule.
1458      *
1459      * @return int
1460      */
1461     public function get_selector_count() {
1462         $count = 0;
1463         foreach ($this->selectors as $selector) {
1464             $count += $selector->get_selector_count();
1465         }
1466         return $count;
1467     }
1469     /**
1470      * Returns true if there are any errors with this rule.
1471      *
1472      * @return bool
1473      */
1474     public function has_errors() {
1475         foreach ($this->styles as $style) {
1476             if ($style->has_error()) {
1477                 return true;
1478             }
1479         }
1480         return false;
1481     }
1483     /**
1484      * Returns the error strings that were recorded when processing this rule.
1485      *
1486      * Before calling this function you should first call {@see css_rule::has_errors()}
1487      * to make sure there are errors (hopefully there arn't).
1488      *
1489      * @return string
1490      */
1491     public function get_error_string() {
1492         $css = $this->out();
1493         $errors = array();
1494         foreach ($this->styles as $style) {
1495             if ($style->has_error()) {
1496                 $errors[] = "  * ".$style->get_last_error();
1497             }
1498         }
1499         return $css." has the following errors:\n".join("\n", $errors);
1501     }
1504 /**
1505  * A media class to organise rules by the media they apply to.
1506  *
1507  * @package core_css
1508  * @category css
1509  * @copyright 2012 Sam Hemelryk
1510  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1511  */
1512 class css_media {
1514     /**
1515      * An array of the different media types this instance applies to.
1516      * @var array
1517      */
1518     protected $types = array();
1520     /**
1521      * An array of rules within this media instance
1522      * @var array
1523      */
1524     protected $rules = array();
1526     /**
1527      * Initalises a new media instance
1528      *
1529      * @param string $for The media that the contained rules are destined for.
1530      */
1531     public function __construct($for = 'all') {
1532         $types = explode(',', $for);
1533         $this->types = array_map('trim', $types);
1534     }
1536     /**
1537      * Adds a new CSS rule to this media instance
1538      *
1539      * @param css_rule $newrule
1540      */
1541     public function add_rule(css_rule $newrule) {
1542         foreach ($newrule->split_by_selector() as $rule) {
1543             $hash = $rule->get_selector_hash();
1544             if (!array_key_exists($hash, $this->rules)) {
1545                 $this->rules[$hash] = $rule;
1546             } else {
1547                 $this->rules[$hash]->add_styles($rule->get_styles());
1548             }
1549         }
1550     }
1552     /**
1553      * Returns the rules used by this
1554      *
1555      * @return array
1556      */
1557     public function get_rules() {
1558         return $this->rules;
1559     }
1561     /**
1562      * Organises rules by gropuing selectors based upon the styles and consolidating
1563      * those selectors into single rules.
1564      *
1565      * @return bool True if the CSS was optimised by this method
1566      */
1567     public function organise_rules_by_selectors() {
1568         $optimised = array();
1569         $beforecount = count($this->rules);
1570         foreach ($this->rules as $rule) {
1571             $hash = $rule->get_style_hash();
1572             if (!array_key_exists($hash, $optimised)) {
1573                 $optimised[$hash] = clone($rule);
1574             } else {
1575                 foreach ($rule->get_selectors() as $selector) {
1576                     $optimised[$hash]->add_selector($selector);
1577                 }
1578             }
1579         }
1580         $this->rules = $optimised;
1581         $aftercount = count($this->rules);
1582         return ($beforecount < $aftercount);
1583     }
1585     /**
1586      * Returns the total number of rules that exist within this media set
1587      *
1588      * @return int
1589      */
1590     public function count_rules() {
1591         return count($this->rules);
1592     }
1594     /**
1595      * Returns the total number of selectors that exist within this media set
1596      *
1597      * @return int
1598      */
1599     public function count_selectors() {
1600         $count = 0;
1601         foreach ($this->rules as $rule) {
1602             $count += $rule->get_selector_count();
1603         }
1604         return $count;
1605     }
1607     /**
1608      * Returns the CSS for this media and all of its rules.
1609      *
1610      * @return string
1611      */
1612     public function out() {
1613         return css_writer::media(join(',', $this->types), $this->rules);
1614     }
1616     /**
1617      * Returns an array of media that this media instance applies to
1618      *
1619      * @return array
1620      */
1621     public function get_types() {
1622         return $this->types;
1623     }
1625     /**
1626      * Returns true if the media has any rules that have errors
1627      *
1628      * @return boolean
1629      */
1630     public function has_errors() {
1631         foreach ($this->rules as $rule) {
1632             if ($rule->has_errors()) {
1633                 return true;
1634             }
1635         }
1636         return false;
1637     }
1639     /**
1640      * Returns any errors that have happened within rules in this media set.
1641      *
1642      * @return string
1643      */
1644     public function get_errors() {
1645         $errors = array();
1646         foreach ($this->rules as $rule) {
1647             if ($rule->has_errors()) {
1648                 $errors[] = $rule->get_error_string();
1649             }
1650         }
1651         return join("\n", $errors);
1652     }
1655 /**
1656  * An absract class to represent CSS styles
1657  *
1658  * @package core_css
1659  * @category css
1660  * @copyright 2012 Sam Hemelryk
1661  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1662  */
1663 abstract class css_style {
1665     /**
1666      * The name of the style
1667      * @var string
1668      */
1669     protected $name;
1671     /**
1672      * The value for the style
1673      * @var mixed
1674      */
1675     protected $value;
1677     /**
1678      * If set to true this style was defined with the !important rule.
1679      * Only trolls use !important.
1680      * Don't hide under bridges.. its not good for your skin. Do the proper thing
1681      * and fix the issue don't just force a fix that will undoubtedly one day
1682      * lead to further frustration.
1683      * @var bool
1684      */
1685     protected $important = false;
1687     /**
1688      * Gets set to true if this style has an error
1689      * @var bool
1690      */
1691     protected $error = false;
1693     /**
1694      * The last error message that occured
1695      * @var string
1696      */
1697     protected $errormessage = null;
1700     /**
1701      * Initialises a new style.
1702      *
1703      * This is the only public way to create a style to ensure they that appropriate
1704      * style class is used if it exists.
1705      *
1706      * @param string $name The name of the style.
1707      * @param string $value The value of the style.
1708      * @return css_style_generic
1709      */
1710     public static function init_automatic($name, $value) {
1711         $specificclass = 'css_style_'.preg_replace('#[^a-zA-Z0-9]+#', '', $name);
1712         if (class_exists($specificclass)) {
1713             return $specificclass::init($value);
1714         }
1715         return new css_style_generic($name, $value);
1716     }
1718     /**
1719      * Creates a new style when given its name and value
1720      *
1721      * @param string $name The name of the style.
1722      * @param string $value The value of the style.
1723      */
1724     protected function __construct($name, $value) {
1725         $this->name = $name;
1726         $this->set_value($value);
1727     }
1729     /**
1730      * Sets the value for the style
1731      *
1732      * @param string $value
1733      */
1734     final public function set_value($value) {
1735         $value = trim($value);
1736         $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
1737         if ($important) {
1738             $value = substr($value, 0, -(strlen($matches[1])));
1739         }
1740         if (!$this->important || $important) {
1741             $this->value = $this->clean_value($value);
1742             $this->important = $important;
1743         }
1744         if (!$this->is_valid()) {
1745             $this->set_error('Invalid value for '.$this->name);
1746         }
1747     }
1749     /**
1750      * Returns true if the value associated with this style is valid
1751      *
1752      * @return bool
1753      */
1754     public function is_valid() {
1755         return true;
1756     }
1758     /**
1759      * Returns the name for the style
1760      *
1761      * @return string
1762      */
1763     public function get_name() {
1764         return $this->name;
1765     }
1767     /**
1768      * Returns the value for the style
1769      *
1770      * @return string
1771      */
1772     public function get_value() {
1773         $value = $this->value;
1774         if ($this->important) {
1775             $value .= ' !important';
1776         }
1777         return $value;
1778     }
1780     /**
1781      * Returns the style ready for use in CSS
1782      *
1783      * @param string|null $value A value to use to override the value for this style.
1784      * @return string
1785      */
1786     public function out($value = null) {
1787         if (is_null($value)) {
1788             $value = $this->get_value();
1789         }
1790         return css_writer::style($this->name, $value, $this->important);
1791     }
1793     /**
1794      * This can be overridden by a specific style allowing it to clean its values
1795      * consistently.
1796      *
1797      * @param mixed $value
1798      * @return mixed
1799      */
1800     protected function clean_value($value) {
1801         return $value;
1802     }
1804     /**
1805      * If this particular style can be consolidated into another style this function
1806      * should return the style that it can be consolidated into.
1807      *
1808      * @return string|null
1809      */
1810     public function consolidate_to() {
1811         return null;
1812     }
1814     /**
1815      * Sets the last error message.
1816      *
1817      * @param string $message
1818      */
1819     protected function set_error($message) {
1820         $this->error = true;
1821         $this->errormessage = $message;
1822     }
1824     /**
1825      * Returns true if an error has occured
1826      *
1827      * @return bool
1828      */
1829     public function has_error() {
1830         return $this->error;
1831     }
1833     /**
1834      * Returns the last error that occured or null if no errors have happened.
1835      *
1836      * @return string
1837      */
1838     public function get_last_error() {
1839         return $this->errormessage;
1840     }
1843 /**
1844  * A generic CSS style class to use when a more specific class does not exist.
1845  *
1846  * @package core_css
1847  * @category css
1848  * @copyright 2012 Sam Hemelryk
1849  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1850  */
1851 class css_style_generic extends css_style {
1853     /**
1854      * Cleans incoming values for typical things that can be optimised.
1855      *
1856      * @param mixed $value Cleans the provided value optimising it if possible
1857      * @return string
1858      */
1859     protected function clean_value($value) {
1860         if (trim($value) == '0px') {
1861             $value = 0;
1862         } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
1863             $value = '#'.strtoupper($matches[1]);
1864         }
1865         return $value;
1866     }
1869 /**
1870  * A colour CSS style
1871  *
1872  * @package core_css
1873  * @category css
1874  * @copyright 2012 Sam Hemelryk
1875  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1876  */
1877 class css_style_color extends css_style {
1879     /**
1880      * Creates a new colour style
1881      *
1882      * @param mixed $value Initialises a new colour style
1883      * @return css_style_color
1884      */
1885     public static function init($value) {
1886         return new css_style_color('color', $value);
1887     }
1889     /**
1890      * Cleans the colour unifing it to a 6 char hash colour if possible
1891      * Doing this allows us to associate identical colours being specified in
1892      * different ways. e.g. Red, red, #F00, and #F00000
1893      *
1894      * @param mixed $value Cleans the provided value optimising it if possible
1895      * @return string
1896      */
1897     protected function clean_value($value) {
1898         $value = trim($value);
1899         if (css_is_colour($value)) {
1900             if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
1901                 $value = '#'.strtoupper($matches[1]);
1902             } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
1903                 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
1904                 $value = '#'.strtoupper($value);
1905             } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
1906                 $value = css_optimiser::$htmlcolours[strtolower($value)];
1907             }
1908         }
1909         return $value;
1910     }
1912     /**
1913      * Returns the colour style for use within CSS.
1914      * Will return an optimised hash colour.
1915      *
1916      * e.g #123456
1917      *     #123 instead of #112233
1918      *     #F00 instead of red
1919      *
1920      * @param string $overridevalue If provided then this value will be used instead
1921      *     of the styles current value.
1922      * @return string
1923      */
1924     public function out($overridevalue = null) {
1925         if ($overridevalue === null) {
1926             $overridevalue = $this->value;
1927         }
1928         return parent::out(self::shrink_value($overridevalue));
1929     }
1931     /**
1932      * Shrinks the colour value is possible.
1933      *
1934      * @param string $value Shrinks the current value to an optimial form if possible
1935      * @return string
1936      */
1937     public static function shrink_value($value) {
1938         if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) {
1939             return '#'.$matches[1].$matches[2].$matches[3];
1940         }
1941         return $value;
1942     }
1944     /**
1945      * Returns true if the value is a valid colour.
1946      *
1947      * @return bool
1948      */
1949     public function is_valid() {
1950         return css_is_colour($this->value);
1951     }
1954 /**
1955  * A width style
1956  *
1957  * @package core_css
1958  * @category css
1959  * @copyright 2012 Sam Hemelryk
1960  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1961  */
1962 class css_style_width extends css_style {
1964     /**
1965      * Checks if the width is valid
1966      * @return bool
1967      */
1968     public function is_valid() {
1969         return css_is_width($this->value);
1970     }
1972     /**
1973      * Cleans the provided value
1974      *
1975      * @param mixed $value Cleans the provided value optimising it if possible
1976      * @return string
1977      */
1978     protected function clean_value($value) {
1979         if (!css_is_width($value)) {
1980             // Note we don't actually change the value to something valid. That
1981             // would be bad for futureproofing.
1982             $this->set_error('Invalid width specified for '.$this->name);
1983         } else if (preg_match('#^0\D+$#', $value)) {
1984             $value = 0;
1985         }
1986         return trim($value);
1987     }
1989     /**
1990      * Initialises a new width style
1991      *
1992      * @param mixed $value The value this style has
1993      * @return css_style_width
1994      */
1995     public static function init($value) {
1996         return new css_style_width('width', $value);
1997     }
2000 /**
2001  * A margin style
2002  *
2003  * @package core_css
2004  * @category css
2005  * @copyright 2012 Sam Hemelryk
2006  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2007  */
2008 class css_style_margin extends css_style_width {
2010     /**
2011      * Initialises a margin style.
2012      *
2013      * In this case we split the margin into several other margin styles so that
2014      * we can properly condense overrides and then reconsolidate them later into
2015      * an optimal form.
2016      *
2017      * @param string $value The value the style has
2018      * @return array An array of margin values that can later be consolidated
2019      */
2020     public static function init($value) {
2021         $important = '';
2022         if (strpos($value, '!important') !== false) {
2023             $important = ' !important';
2024             $value = str_replace('!important', '', $value);
2025         }
2027         $value = preg_replace('#\s+#', ' ', trim($value));
2028         $bits = explode(' ', $value, 4);
2030         $top = $right = $bottom = $left = null;
2031         if (count($bits) > 0) {
2032             $top = $right = $bottom = $left = array_shift($bits);
2033         }
2034         if (count($bits) > 0) {
2035             $right = $left = array_shift($bits);
2036         }
2037         if (count($bits) > 0) {
2038             $bottom = array_shift($bits);
2039         }
2040         if (count($bits) > 0) {
2041             $left = array_shift($bits);
2042         }
2043         return array(
2044             new css_style_margintop('margin-top', $top.$important),
2045             new css_style_marginright('margin-right', $right.$important),
2046             new css_style_marginbottom('margin-bottom', $bottom.$important),
2047             new css_style_marginleft('margin-left', $left.$important)
2048         );
2049     }
2051     /**
2052      * Consolidates individual margin styles into a single margin style
2053      *
2054      * @param array $styles
2055      * @return array An array of consolidated styles
2056      */
2057     public static function consolidate(array $styles) {
2058         if (count($styles) != 4) {
2059             return $styles;
2060         }
2061         $top = $right = $bottom = $left = null;
2062         foreach ($styles as $style) {
2063             switch ($style->get_name()) {
2064                 case 'margin-top' : $top = $style->get_value();break;
2065                 case 'margin-right' : $right = $style->get_value();break;
2066                 case 'margin-bottom' : $bottom = $style->get_value();break;
2067                 case 'margin-left' : $left = $style->get_value();break;
2068             }
2069         }
2070         if ($top == $bottom && $left == $right) {
2071             if ($top == $left) {
2072                 return array(new css_style_margin('margin', $top));
2073             } else {
2074                 return array(new css_style_margin('margin', "{$top} {$left}"));
2075             }
2076         } else if ($left == $right) {
2077             return array(new css_style_margin('margin', "{$top} {$right} {$bottom}"));
2078         } else {
2079             return array(new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}"));
2080         }
2081     }
2084 /**
2085  * A margin top style
2086  *
2087  * @package core_css
2088  * @category css
2089  * @copyright 2012 Sam Hemelryk
2090  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2091  */
2092 class css_style_margintop extends css_style_margin {
2094     /**
2095      * A simple init, just a single style
2096      *
2097      * @param string $value The value the style has
2098      * @return css_style_margintop
2099      */
2100     public static function init($value) {
2101         return new css_style_margintop('margin-top', $value);
2102     }
2104     /**
2105      * This style can be consolidated into a single margin style
2106      *
2107      * @return string
2108      */
2109     public function consolidate_to() {
2110         return 'margin';
2111     }
2114 /**
2115  * A margin right style
2116  *
2117  * @package core_css
2118  * @category css
2119  * @copyright 2012 Sam Hemelryk
2120  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2121  */
2122 class css_style_marginright extends css_style_margin {
2124     /**
2125      * A simple init, just a single style
2126      *
2127      * @param string $value The value the style has
2128      * @return css_style_margintop
2129      */
2130     public static function init($value) {
2131         return new css_style_marginright('margin-right', $value);
2132     }
2134     /**
2135      * This style can be consolidated into a single margin style
2136      *
2137      * @return string
2138      */
2139     public function consolidate_to() {
2140         return 'margin';
2141     }
2144 /**
2145  * A margin bottom style
2146  *
2147  * @package core_css
2148  * @category css
2149  * @copyright 2012 Sam Hemelryk
2150  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2151  */
2152 class css_style_marginbottom extends css_style_margin {
2154     /**
2155      * A simple init, just a single style
2156      *
2157      * @param string $value The value the style has
2158      * @return css_style_margintop
2159      */
2160     public static function init($value) {
2161         return new css_style_marginbottom('margin-bottom', $value);
2162     }
2164     /**
2165      * This style can be consolidated into a single margin style
2166      *
2167      * @return string
2168      */
2169     public function consolidate_to() {
2170         return 'margin';
2171     }
2174 /**
2175  * A margin left style
2176  *
2177  * @package core_css
2178  * @category css
2179  * @copyright 2012 Sam Hemelryk
2180  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2181  */
2182 class css_style_marginleft extends css_style_margin {
2184     /**
2185      * A simple init, just a single style
2186      *
2187      * @param string $value The value the style has
2188      * @return css_style_margintop
2189      */
2190     public static function init($value) {
2191         return new css_style_marginleft('margin-left', $value);
2192     }
2194     /**
2195      * This style can be consolidated into a single margin style
2196      *
2197      * @return string
2198      */
2199     public function consolidate_to() {
2200         return 'margin';
2201     }
2204 /**
2205  * A border style
2206  *
2207  * @package core_css
2208  * @category css
2209  * @copyright 2012 Sam Hemelryk
2210  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2211  */
2212 class css_style_border extends css_style {
2214     /**
2215      * Initalises the border style into an array of individual style compontents
2216      *
2217      * @param string $value The value the style has
2218      * @return css_style_bordercolor
2219      */
2220     public static function init($value) {
2221         $value = preg_replace('#\s+#', ' ', $value);
2222         $bits = explode(' ', $value, 3);
2224         $return = array();
2225         if (count($bits) > 0) {
2226             $width = array_shift($bits);
2227             if (!css_is_width($width)) {
2228                 $width = '0';
2229             }
2230             $return[] = new css_style_borderwidth('border-top-width', $width);
2231             $return[] = new css_style_borderwidth('border-right-width', $width);
2232             $return[] = new css_style_borderwidth('border-bottom-width', $width);
2233             $return[] = new css_style_borderwidth('border-left-width', $width);
2234         }
2235         if (count($bits) > 0) {
2236             $style = array_shift($bits);
2237             $return[] = new css_style_borderstyle('border-top-style', $style);
2238             $return[] = new css_style_borderstyle('border-right-style', $style);
2239             $return[] = new css_style_borderstyle('border-bottom-style', $style);
2240             $return[] = new css_style_borderstyle('border-left-style', $style);
2241         }
2242         if (count($bits) > 0) {
2243             $colour = array_shift($bits);
2244             $return[] = new css_style_bordercolor('border-top-color', $colour);
2245             $return[] = new css_style_bordercolor('border-right-color', $colour);
2246             $return[] = new css_style_bordercolor('border-bottom-color', $colour);
2247             $return[] = new css_style_bordercolor('border-left-color', $colour);
2248         }
2249         return $return;
2250     }
2252     /**
2253      * Consolidates all border styles into a single style
2254      *
2255      * @param array $styles An array of border styles
2256      * @return array An optimised array of border styles
2257      */
2258     public static function consolidate(array $styles) {
2260         $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2261         $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2262         $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2264         foreach ($styles as $style) {
2265             switch ($style->get_name()) {
2266                 case 'border-top-width': $borderwidths['top'] = $style->get_value(); break;
2267                 case 'border-right-width': $borderwidths['right'] = $style->get_value(); break;
2268                 case 'border-bottom-width': $borderwidths['bottom'] = $style->get_value(); break;
2269                 case 'border-left-width': $borderwidths['left'] = $style->get_value(); break;
2271                 case 'border-top-style': $borderstyles['top'] = $style->get_value(); break;
2272                 case 'border-right-style': $borderstyles['right'] = $style->get_value(); break;
2273                 case 'border-bottom-style': $borderstyles['bottom'] = $style->get_value(); break;
2274                 case 'border-left-style': $borderstyles['left'] = $style->get_value(); break;
2276                 case 'border-top-color': $bordercolors['top'] = $style->get_value(); break;
2277                 case 'border-right-color': $bordercolors['right'] = $style->get_value(); break;
2278                 case 'border-bottom-color': $bordercolors['bottom'] = $style->get_value(); break;
2279                 case 'border-left-color': $bordercolors['left'] = $style->get_value(); break;
2280             }
2281         }
2283         $uniquewidths = count(array_unique($borderwidths));
2284         $uniquestyles = count(array_unique($borderstyles));
2285         $uniquecolors = count(array_unique($bordercolors));
2287         $nullwidths = in_array(null, $borderwidths, true);
2288         $nullstyles = in_array(null, $borderstyles, true);
2289         $nullcolors = in_array(null, $bordercolors, true);
2291         $allwidthsthesame = ($uniquewidths === 1)?1:0;
2292         $allstylesthesame = ($uniquestyles === 1)?1:0;
2293         $allcolorsthesame = ($uniquecolors === 1)?1:0;
2295         $allwidthsnull = $allwidthsthesame && $nullwidths;
2296         $allstylesnull = $allstylesthesame && $nullstyles;
2297         $allcolorsnull = $allcolorsthesame && $nullcolors;
2299         $return = array();
2300         if ($allwidthsnull && $allstylesnull && $allcolorsnull) {
2301             // Everything is null still... boo
2302             return array(new css_style_border('border', ''));
2304         } else if ($allwidthsnull && $allstylesnull) {
2306             self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2307             return $return;
2309         } else if ($allwidthsnull && $allcolorsnull) {
2311             self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2312             return $return;
2314         } else if ($allcolorsnull && $allstylesnull) {
2316             self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2317             return $return;
2319         }
2321         if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 3) {
2323             $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']);
2325         } else if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 2) {
2327             if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) {
2329                 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']);
2330                 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2332             } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) {
2334                 $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']);
2335                 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2337             } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) {
2339                 $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']);
2340                 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2342             } else {
2343                 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2344                 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2345                 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2346             }
2348         } else if (!$nullwidths && !$nullcolors && !$nullstyles && max(array_count_values($borderwidths)) == 3 && max(array_count_values($borderstyles)) == 3 && max(array_count_values($bordercolors)) == 3) {
2349             $widthkeys = array();
2350             $stylekeys = array();
2351             $colorkeys = array();
2353             foreach ($borderwidths as $key => $value) {
2354                 if (!array_key_exists($value, $widthkeys)) {
2355                     $widthkeys[$value] = array();
2356                 }
2357                 $widthkeys[$value][] = $key;
2358             }
2359             usort($widthkeys, 'css_sort_by_count');
2360             $widthkeys = array_values($widthkeys);
2362             foreach ($borderstyles as $key => $value) {
2363                 if (!array_key_exists($value, $stylekeys)) {
2364                     $stylekeys[$value] = array();
2365                 }
2366                 $stylekeys[$value][] = $key;
2367             }
2368             usort($stylekeys, 'css_sort_by_count');
2369             $stylekeys = array_values($stylekeys);
2371             foreach ($bordercolors as $key => $value) {
2372                 if (!array_key_exists($value, $colorkeys)) {
2373                     $colorkeys[$value] = array();
2374                 }
2375                 $colorkeys[$value][] = $key;
2376             }
2377             usort($colorkeys, 'css_sort_by_count');
2378             $colorkeys = array_values($colorkeys);
2380             if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) {
2381                 $key = $widthkeys[0][0];
2382                 self::build_style_string($return, 'css_style_border', 'border',  $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2383                 $key = $widthkeys[1][0];
2384                 self::build_style_string($return, 'css_style_border'.$key, 'border-'.$key,  $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2385             } else {
2386                 self::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2387                 self::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2388                 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2389                 self::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
2390             }
2391         } else {
2392             self::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2393             self::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2394             self::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2395             self::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
2396         }
2397         foreach ($return as $key => $style) {
2398             if ($style->get_value() == '') {
2399                 unset($return[$key]);
2400             }
2401         }
2402         return $return;
2403     }
2405     /**
2406      * Border styles get consolidated to a single border style.
2407      *
2408      * @return string
2409      */
2410     public function consolidate_to() {
2411         return 'border';
2412     }
2414     /**
2415      * Consolidates a series of border styles into an optimised array of border
2416      * styles by looking at the direction of the border and prioritising that
2417      * during the optimisation.
2418      *
2419      * @param array $array An array to add styles into during consolidation. Passed by reference.
2420      * @param string $class The class type to initalise
2421      * @param string $style The style to create
2422      * @param string|array $top The top value
2423      * @param string $right The right value
2424      * @param string $bottom The bottom value
2425      * @param string $left The left value
2426      * @return bool
2427      */
2428     public static function consolidate_styles_by_direction(&$array, $class, $style, $top, $right = null, $bottom = null, $left = null) {
2429         if (is_array($top)) {
2430             $right = $top['right'];
2431             $bottom = $top['bottom'];
2432             $left = $top['left'];
2433             $top = $top['top'];
2434         }
2436         if ($top == $bottom && $left == $right && $top == $left) {
2437             if (is_null($top)) {
2438                 $array[] = new $class($style, '');
2439             } else {
2440                 $array[] =  new $class($style, $top);
2441             }
2442         } else if ($top == null || $right == null || $bottom == null || $left == null) {
2443             if ($top !== null) {
2444                 $array[] = new $class(str_replace('border-', 'border-top-', $style), $top);
2445             }
2446             if ($right !== null) {
2447                 $array[] = new $class(str_replace('border-', 'border-right-', $style), $right);
2448             }
2449             if ($bottom !== null) {
2450                 $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom);
2451             }
2452             if ($left !== null) {
2453                 $array[] = new $class(str_replace('border-', 'border-left-', $style), $left);
2454             }
2455         } else if ($top == $bottom && $left == $right) {
2456             $array[] = new $class($style, $top.' '.$right);
2457         } else if ($left == $right) {
2458             $array[] = new $class($style, $top.' '.$right.' '.$bottom);
2459         } else {
2460             $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left);
2461         }
2462         return true;
2463     }
2465     /**
2466      * Builds a border style for a set of width, style, and colour values
2467      *
2468      * @param array $array An array into which the generated style is added
2469      * @param string $class The class type to initialise
2470      * @param string $cssstyle The style to use
2471      * @param string $width The width of the border
2472      * @param string $style The style of the border
2473      * @param string $color The colour of the border
2474      * @return bool
2475      */
2476     public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) {
2477         if (!is_null($width) && !is_null($style) && !is_null($color)) {
2478             $array[] = new $class($cssstyle, $width.' '.$style.' '.$color);
2479         } else if (!is_null($width) && !is_null($style) && is_null($color)) {
2480             $array[] = new $class($cssstyle, $width.' '.$style);
2481         } else if (!is_null($width) && is_null($style) && is_null($color)) {
2482             $array[] = new $class($cssstyle, $width);
2483         } else {
2484             if (!is_null($width)) $array[] = new $class($cssstyle, $width);
2485             if (!is_null($style)) $array[] = new $class($cssstyle, $style);
2486             if (!is_null($color)) $array[] = new $class($cssstyle, $color);
2487         }
2488         return true;
2489     }
2492 /**
2493  * A border colour style
2494  *
2495  * @package core_css
2496  * @category css
2497  * @copyright 2012 Sam Hemelryk
2498  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2499  */
2500 class css_style_bordercolor extends css_style_color {
2502     /**
2503      * Creates a new border colour style
2504      *
2505      * Based upon the colour style
2506      *
2507      * @param mixed $value
2508      * @return Array of css_style_bordercolor
2509      */
2510     public static function init($value) {
2511         $value = preg_replace('#\s+#', ' ', $value);
2512         $bits = explode(' ', $value, 4);
2514         $top = $right = $bottom = $left = null;
2515         if (count($bits) > 0) {
2516             $top = $right = $bottom = $left = array_shift($bits);
2517         }
2518         if (count($bits) > 0) {
2519             $right = $left = array_shift($bits);
2520         }
2521         if (count($bits) > 0) {
2522             $bottom = array_shift($bits);
2523         }
2524         if (count($bits) > 0) {
2525             $left = array_shift($bits);
2526         }
2527         return array(
2528             css_style_bordertopcolor::init($top),
2529             css_style_borderrightcolor::init($right),
2530             css_style_borderbottomcolor::init($bottom),
2531             css_style_borderleftcolor::init($left)
2532         );
2533     }
2535     /**
2536      * Consolidate this to a single border style
2537      *
2538      * @return string
2539      */
2540     public function consolidate_to() {
2541         return 'border';
2542     }
2544     /**
2545      * Cleans the value
2546      *
2547      * @param string $value Cleans the provided value optimising it if possible
2548      * @return string
2549      */
2550     protected function clean_value($value) {
2551         $values = explode(' ', $value);
2552         $values = array_map('parent::clean_value', $values);
2553         return join (' ', $values);
2554     }
2556     /**
2557      * Outputs this style
2558      *
2559      * @param string $overridevalue
2560      * @return string
2561      */
2562     public function out($overridevalue = null) {
2563         if ($overridevalue === null) {
2564             $overridevalue = $this->value;
2565         }
2566         $values = explode(' ', $overridevalue);
2567         $values = array_map('css_style_color::shrink_value', $values);
2568         return parent::out(join (' ', $values));
2569     }
2572 /**
2573  * A border left style
2574  *
2575  * @package core_css
2576  * @category css
2577  * @copyright 2012 Sam Hemelryk
2578  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2579  */
2580 class css_style_borderleft extends css_style_generic {
2582     /**
2583      * Initialises the border left style into individual components
2584      *
2585      * @param string $value
2586      * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor
2587      */
2588     public static function init($value) {
2589         $value = preg_replace('#\s+#', ' ', $value);
2590         $bits = explode(' ', $value, 3);
2592         $return = array();
2593         if (count($bits) > 0) {
2594             $return[] = css_style_borderleftwidth::init(array_shift($bits));
2595         }
2596         if (count($bits) > 0) {
2597             $return[] = css_style_borderleftstyle::init(array_shift($bits));
2598         }
2599         if (count($bits) > 0) {
2600             $return[] = css_style_borderleftcolor::init(array_shift($bits));
2601         }
2602         return $return;
2603     }
2605     /**
2606      * Consolidate this to a single border style
2607      *
2608      * @return string
2609      */
2610     public function consolidate_to() {
2611         return 'border';
2612     }
2615 /**
2616  * A border right style
2617  *
2618  * @package core_css
2619  * @category css
2620  * @copyright 2012 Sam Hemelryk
2621  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2622  */
2623 class css_style_borderright extends css_style_generic {
2625     /**
2626      * Initialises the border right style into individual components
2627      *
2628      * @param string $value The value of the style
2629      * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor
2630      */
2631     public static function init($value) {
2632         $value = preg_replace('#\s+#', ' ', $value);
2633         $bits = explode(' ', $value, 3);
2635         $return = array();
2636         if (count($bits) > 0) {
2637             $return[] = css_style_borderrightwidth::init(array_shift($bits));
2638         }
2639         if (count($bits) > 0) {
2640             $return[] = css_style_borderrightstyle::init(array_shift($bits));
2641         }
2642         if (count($bits) > 0) {
2643             $return[] = css_style_borderrightcolor::init(array_shift($bits));
2644         }
2645         return $return;
2646     }
2648     /**
2649      * Consolidate this to a single border style
2650      *
2651      * @return string
2652      */
2653     public function consolidate_to() {
2654         return 'border';
2655     }
2658 /**
2659  * A border top style
2660  *
2661  * @package core_css
2662  * @category css
2663  * @copyright 2012 Sam Hemelryk
2664  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2665  */
2666 class css_style_bordertop extends css_style_generic {
2668     /**
2669      * Initialises the border top style into individual components
2670      *
2671      * @param string $value The value of the style
2672      * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor
2673      */
2674     public static function init($value) {
2675         $value = preg_replace('#\s+#', ' ', $value);
2676         $bits = explode(' ', $value, 3);
2678         $return = array();
2679         if (count($bits) > 0) {
2680             $return[] = css_style_bordertopwidth::init(array_shift($bits));
2681         }
2682         if (count($bits) > 0) {
2683             $return[] = css_style_bordertopstyle::init(array_shift($bits));
2684         }
2685         if (count($bits) > 0) {
2686             $return[] = css_style_bordertopcolor::init(array_shift($bits));
2687         }
2688         return $return;
2689     }
2691     /**
2692      * Consolidate this to a single border style
2693      *
2694      * @return string
2695      */
2696     public function consolidate_to() {
2697         return 'border';
2698     }
2701 /**
2702  * A border bottom style
2703  *
2704  * @package core_css
2705  * @category css
2706  * @copyright 2012 Sam Hemelryk
2707  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2708  */
2709 class css_style_borderbottom extends css_style_generic {
2711     /**
2712      * Initialises the border bottom style into individual components
2713      *
2714      * @param string $value The value of the style
2715      * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor
2716      */
2717     public static function init($value) {
2718         $value = preg_replace('#\s+#', ' ', $value);
2719         $bits = explode(' ', $value, 3);
2721         $return = array();
2722         if (count($bits) > 0) {
2723             $return[] = css_style_borderbottomwidth::init(array_shift($bits));
2724         }
2725         if (count($bits) > 0) {
2726             $return[] = css_style_borderbottomstyle::init(array_shift($bits));
2727         }
2728         if (count($bits) > 0) {
2729             $return[] = css_style_borderbottomcolor::init(array_shift($bits));
2730         }
2731         return $return;
2732     }
2734     /**
2735      * Consolidate this to a single border style
2736      *
2737      * @return string
2738      */
2739     public function consolidate_to() {
2740         return 'border';
2741     }
2744 /**
2745  * A border width style
2746  *
2747  * @package core_css
2748  * @category css
2749  * @copyright 2012 Sam Hemelryk
2750  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2751  */
2752 class css_style_borderwidth extends css_style_width {
2754     /**
2755      * Creates a new border colour style
2756      *
2757      * Based upon the colour style
2758      *
2759      * @param string $value The value of the style
2760      * @return array Array of css_style_border*width
2761      */
2762     public static function init($value) {
2763         $value = preg_replace('#\s+#', ' ', $value);
2764         $bits = explode(' ', $value, 4);
2766         $top = $right = $bottom = $left = null;
2767         if (count($bits) > 0) {
2768             $top = $right = $bottom = $left = array_shift($bits);
2769         }
2770         if (count($bits) > 0) {
2771             $right = $left = array_shift($bits);
2772         }
2773         if (count($bits) > 0) {
2774             $bottom = array_shift($bits);
2775         }
2776         if (count($bits) > 0) {
2777             $left = array_shift($bits);
2778         }
2779         return array(
2780             css_style_bordertopwidth::init($top),
2781             css_style_borderrightwidth::init($right),
2782             css_style_borderbottomwidth::init($bottom),
2783             css_style_borderleftwidth::init($left)
2784         );
2785     }
2787     /**
2788      * Consolidate this to a single border style
2789      *
2790      * @return string
2791      */
2792     public function consolidate_to() {
2793         return 'border';
2794     }
2797 /**
2798  * A border style style
2799  *
2800  * @package core_css
2801  * @category css
2802  * @copyright 2012 Sam Hemelryk
2803  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2804  */
2805 class css_style_borderstyle extends css_style_generic {
2807     /**
2808      * Creates a new border colour style
2809      *
2810      * Based upon the colour style
2811      *
2812      * @param string $value The value of the style
2813      * @return array Array of css_style_border*style
2814      */
2815     public static function init($value) {
2816         $value = preg_replace('#\s+#', ' ', $value);
2817         $bits = explode(' ', $value, 4);
2819         $top = $right = $bottom = $left = null;
2820         if (count($bits) > 0) {
2821             $top = $right = $bottom = $left = array_shift($bits);
2822         }
2823         if (count($bits) > 0) {
2824             $right = $left = array_shift($bits);
2825         }
2826         if (count($bits) > 0) {
2827             $bottom = array_shift($bits);
2828         }
2829         if (count($bits) > 0) {
2830             $left = array_shift($bits);
2831         }
2832         return array(
2833             css_style_bordertopstyle::init($top),
2834             css_style_borderrightstyle::init($right),
2835             css_style_borderbottomstyle::init($bottom),
2836             css_style_borderleftstyle::init($left)
2837         );
2838     }
2840     /**
2841      * Consolidate this to a single border style
2842      *
2843      * @return string
2844      */
2845     public function consolidate_to() {
2846         return 'border';
2847     }
2850 /**
2851  * A border top colour style
2852  *
2853  * @package core_css
2854  * @category css
2855  * @copyright 2012 Sam Hemelryk
2856  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2857  */
2858 class css_style_bordertopcolor extends css_style_bordercolor {
2860     /**
2861      * Initialises this style object
2862      *
2863      * @param string $value The value of the style
2864      * @return css_style_bordertopcolor
2865      */
2866     public static function init($value) {
2867         return new css_style_bordertopcolor('border-top-color', $value);
2868     }
2870     /**
2871      * Consolidate this to a single border style
2872      *
2873      * @return string
2874      */
2875     public function consolidate_to() {
2876         return 'border';
2877     }
2880 /**
2881  * A border left colour style
2882  *
2883  * @package core_css
2884  * @category css
2885  * @copyright 2012 Sam Hemelryk
2886  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2887  */
2888 class css_style_borderleftcolor extends css_style_bordercolor {
2890     /**
2891      * Initialises this style object
2892      *
2893      * @param string $value The value of the style
2894      * @return css_style_borderleftcolor
2895      */
2896     public static function init($value) {
2897         return new css_style_borderleftcolor('border-left-color', $value);
2898     }
2900     /**
2901      * Consolidate this to a single border style
2902      *
2903      * @return string
2904      */
2905     public function consolidate_to() {
2906         return 'border';
2907     }
2910 /**
2911  * A border right colour style
2912  *
2913  * @package core_css
2914  * @category css
2915  * @copyright 2012 Sam Hemelryk
2916  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2917  */
2918 class css_style_borderrightcolor extends css_style_bordercolor {
2920     /**
2921      * Initialises this style object
2922      *
2923      * @param string $value The value of the style
2924      * @return css_style_borderrightcolor
2925      */
2926     public static function init($value) {
2927         return new css_style_borderrightcolor('border-right-color', $value);
2928     }
2930     /**
2931      * Consolidate this to a single border style
2932      *
2933      * @return string
2934      */
2935     public function consolidate_to() {
2936         return 'border';
2937     }
2940 /**
2941  * A border bottom colour style
2942  *
2943  * @package core_css
2944  * @category css
2945  * @copyright 2012 Sam Hemelryk
2946  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2947  */
2948 class css_style_borderbottomcolor extends css_style_bordercolor {
2950     /**
2951      * Initialises this style object
2952      *
2953      * @param string $value The value of the style
2954      * @return css_style_borderbottomcolor
2955      */
2956     public static function init($value) {
2957         return new css_style_borderbottomcolor('border-bottom-color', $value);
2958     }
2960     /**
2961      * Consolidate this to a single border style
2962      *
2963      * @return string
2964      */
2965     public function consolidate_to() {
2966         return 'border';
2967     }
2970 /**
2971  * A border width top style
2972  *
2973  * @package core_css
2974  * @category css
2975  * @copyright 2012 Sam Hemelryk
2976  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2977  */
2978 class css_style_bordertopwidth extends css_style_borderwidth {
2980     /**
2981      * Initialises this style object
2982      *
2983      * @param string $value The value of the style
2984      * @return css_style_bordertopwidth
2985      */
2986     public static function init($value) {
2987         return new css_style_bordertopwidth('border-top-width', $value);
2988     }
2990     /**
2991      * Consolidate this to a single border style
2992      *
2993      * @return string
2994      */
2995     public function consolidate_to() {
2996         return 'border';
2997     }
3000 /**
3001  * A border width left style
3002  *
3003  * @package core_css
3004  * @category css
3005  * @copyright 2012 Sam Hemelryk
3006  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3007  */
3008 class css_style_borderleftwidth extends css_style_borderwidth {
3010     /**
3011      * Initialises this style object
3012      *
3013      * @param string $value The value of the style
3014      * @return css_style_borderleftwidth
3015      */
3016     public static function init($value) {
3017         return new css_style_borderleftwidth('border-left-width', $value);
3018     }
3020     /**
3021      * Consolidate this to a single border style
3022      *
3023      * @return string
3024      */
3025     public function consolidate_to() {
3026         return 'border';
3027     }
3030 /**
3031  * A border width right style
3032  *
3033  * @package core_css
3034  * @category css
3035  * @copyright 2012 Sam Hemelryk
3036  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3037  */
3038 class css_style_borderrightwidth extends css_style_borderwidth {
3040     /**
3041      * Initialises this style object
3042      *
3043      * @param string $value The value of the style
3044      * @return css_style_borderrightwidth
3045      */
3046     public static function init($value) {
3047         return new css_style_borderrightwidth('border-right-width', $value);
3048     }
3050     /**
3051      * Consolidate this to a single border style
3052      *
3053      * @return string
3054      */
3055     public function consolidate_to() {
3056         return 'border';
3057     }
3060 /**
3061  * A border width bottom style
3062  *
3063  * @package core_css
3064  * @category css
3065  * @copyright 2012 Sam Hemelryk
3066  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3067  */
3068 class css_style_borderbottomwidth extends css_style_borderwidth {
3070     /**
3071      * Initialises this style object
3072      *
3073      * @param string $value The value of the style
3074      * @return css_style_borderbottomwidth
3075      */
3076     public static function init($value) {
3077         return new css_style_borderbottomwidth('border-bottom-width', $value);
3078     }
3080     /**
3081      * Consolidate this to a single border style
3082      *
3083      * @return string
3084      */
3085     public function consolidate_to() {
3086         return 'border';
3087     }
3090 /**
3091  * A border top style
3092  *
3093  * @package core_css
3094  * @category css
3095  * @copyright 2012 Sam Hemelryk
3096  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3097  */
3098 class css_style_bordertopstyle extends css_style_borderstyle {
3100     /**
3101      * Initialises this style object
3102      *
3103      * @param string $value The value of the style
3104      * @return css_style_bordertopstyle
3105      */
3106     public static function init($value) {
3107         return new css_style_bordertopstyle('border-top-style', $value);
3108     }
3110     /**
3111      * Consolidate this to a single border style
3112      *
3113      * @return string
3114      */
3115     public function consolidate_to() {
3116         return 'border';
3117     }
3120 /**
3121  * A border left style
3122  *
3123  * @package core_css
3124  * @category css
3125  * @copyright 2012 Sam Hemelryk
3126  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3127  */
3128 class css_style_borderleftstyle extends css_style_borderstyle {
3130     /**
3131      * Initialises this style object
3132      *
3133      * @param string $value The value of the style
3134      * @return css_style_borderleftstyle
3135      */
3136     public static function init($value) {
3137         return new css_style_borderleftstyle('border-left-style', $value);
3138     }
3140     /**
3141      * Consolidate this to a single border style
3142      *
3143      * @return string
3144      */
3145     public function consolidate_to() {
3146         return 'border';
3147     }
3150 /**
3151  * A border right style
3152  *
3153  * @package core_css
3154  * @category css
3155  * @copyright 2012 Sam Hemelryk
3156  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3157  */
3158 class css_style_borderrightstyle extends css_style_borderstyle {
3160     /**
3161      * Initialises this style object
3162      *
3163      * @param string $value The value of the style
3164      * @return css_style_borderrightstyle
3165      */
3166     public static function init($value) {
3167         return new css_style_borderrightstyle('border-right-style', $value);
3168     }
3170     /**
3171      * Consolidate this to a single border style
3172      *
3173      * @return string
3174      */
3175     public function consolidate_to() {
3176         return 'border';
3177     }
3180 /**
3181  * A border bottom style
3182  *
3183  * @package core_css
3184  * @category css
3185  * @copyright 2012 Sam Hemelryk
3186  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3187  */
3188 class css_style_borderbottomstyle extends css_style_borderstyle {
3190     /**
3191      * Initialises this style object
3192      *
3193      * @return css_style_borderbottomstyle
3194      */
3195     public static function init($value) {
3196         return new css_style_borderbottomstyle('border-bottom-style', $value);
3197     }
3199     /**
3200      * Consolidate this to a single border style
3201      *
3202      * @return string
3203      */
3204     public function consolidate_to() {
3205         return 'border';
3206     }
3209 /**
3210  * A background style
3211  *
3212  * @package core_css
3213  * @category css
3214  * @copyright 2012 Sam Hemelryk
3215  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3216  */
3217 class css_style_background extends css_style {
3219     /**
3220      * Initialises a background style
3221      *
3222      * @param type $value The value of the style
3223      * @return array An array of background component.
3224      */
3225     public static function init($value) {
3226         // colour - image - repeat - attachment - position
3228         $imageurl = null;
3229         if (preg_match('#url\(([^\)]+)\)#', $value, $matches)) {
3230             $imageurl = trim($matches[1]);
3231             $value = str_replace($matches[1], '', $value);
3232         }
3234         $value = preg_replace('#\s+#', ' ', $value);
3235         $bits = explode(' ', $value);
3237         $repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit');
3238         $attachments = array('scroll' , 'fixed', 'inherit');
3240         $return = array();
3241         $unknownbits = array();
3243         if (count($bits) > 0 && css_is_colour(reset($bits))) {
3244             $return[] = new css_style_backgroundcolor('background-color', array_shift($bits));
3245         }
3246         if (count($bits) > 0 && preg_match('#(none|inherit|url\(\))#', reset($bits))) {
3247             $image = array_shift($bits);
3248             if ($image == 'url()') {
3249                 $image = "url({$imageurl})";
3250             }
3251             $return[] = new css_style_backgroundimage('background-image', $image);
3252         }
3253         if (count($bits) > 0 && in_array(reset($bits), $repeats)) {
3254             $return[] = new css_style_backgroundrepeat('background-repeat', array_shift($bits));
3255         }
3256         if (count($bits) > 0 && in_array(reset($bits), $attachments)) {
3257             // scroll , fixed, inherit
3258             $return[] = new css_style_backgroundattachment('background-attachment', array_shift($bits));
3259         }
3260         if (count($bits) > 0) {
3261             $widthbits = array();
3262             foreach ($bits as $bit) {
3263                 if (in_array($bit, array('top', 'left', 'bottom', 'right', 'center')) || css_is_width($bit)) {
3264                     $widthbits[] = $bit;
3265                 } else {
3266                     $unknownbits[] = $bit;
3267                 }
3268             }
3269             $return[] = new css_style_backgroundposition('background-position', join(' ',$widthbits));
3270         }
3271         if (count($unknownbits)) {
3272             foreach ($unknownbits as $bit) {
3273                 if (css_is_colour($bit)) {
3274                     $return[] = new css_style_backgroundcolor('background-color', $bit);
3275                 } else if (in_array($bit, $repeats)) {
3276                     $return[] = new css_style_backgroundrepeat('background-repeat', $bit);
3277                 } else if (in_array($bit, $attachments)) {
3278                     $return[] = new css_style_backgroundattachment('background-attachment', $bit);
3279                 }
3280             }
3281         }
3282         return $return;
3283     }
3285     /**
3286      * Consolidates background styles into a single background style
3287      *
3288      * @param array $styles Consolidates the provided array of background styles
3289      * @return array Consolidated optimised background styles
3290      */
3291     public static function consolidate(array $styles) {
3293         if (count($styles) < 1) {
3294             return $styles;
3295         }
3297         $color = $image = $repeat = $attachment = $position = null;
3298         foreach ($styles as $style) {
3299             switch ($style->get_name()) {
3300                 case 'background-color' : $color = css_style_color::shrink_value($style->get_value()); break;
3301                 case 'background-image' : $image = $style->get_value(); break;
3302                 case 'background-repeat' : $repeat = $style->get_value(); break;
3303                 case 'background-attachment' : $attachment = $style->get_value(); break;
3304                 case 'background-position' : $position = $style->get_value(); break;
3305             }
3306         }
3308         if ((is_null($image) || is_null($position) || is_null($repeat)) && ($image!= null || $position != null || $repeat != null)) {
3309             return $styles;
3310         }
3312         $value = array();
3313         if (!is_null($color)) $value[] .= $color;
3314         if (!is_null($image)) $value[] .= $image;
3315         if (!is_null($repeat)) $value[] .= $repeat;
3316         if (!is_null($attachment)) $value[] .= $attachment;
3317         if (!is_null($position)) $value[] .= $position;
3318         return array(new css_style_background('background', join(' ', $value)));
3319     }
3322 /**
3323  * A background colour style.
3324  *
3325  * Based upon the colour style.
3326  *
3327  * @package core_css
3328  * @category css
3329  * @copyright 2012 Sam Hemelryk
3330  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3331  */
3332 class css_style_backgroundcolor extends css_style_color {
3334     /**
3335      * Creates a new background colour style
3336      *
3337      * @param string $value The value of the style
3338      * @return css_style_backgroundcolor
3339      */
3340     public static function init($value) {
3341         return new css_style_backgroundcolor('background-color', $value);
3342     }
3344     /**
3345      * css_style_backgroundcolor consolidates to css_style_background
3346      *
3347      * @return string
3348      */
3349     public function consolidate_to() {
3350         return 'background';
3351     }
3354 /**
3355  * A background image style.
3356  *
3357  * @package core_css
3358  * @category css
3359  * @copyright 2012 Sam Hemelryk
3360  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3361  */
3362 class css_style_backgroundimage extends css_style_generic {
3364     /**
3365      * Creates a new background colour style
3366      *
3367      * @param string $value The value of the style
3368      * @return css_style_backgroundimage
3369      */
3370     public static function init($value) {
3371         return new css_style_backgroundimage('background-image', $value);
3372     }
3374     /**
3375      * Consolidates this style into a single background style
3376      *
3377      * @return string
3378      */
3379     public function consolidate_to() {
3380         return 'background';
3381     }
3384 /**
3385  * A background repeat style.
3386  *
3387  * @package core_css
3388  * @category css
3389  * @copyright 2012 Sam Hemelryk
3390  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3391  */
3392 class css_style_backgroundrepeat extends css_style_generic {
3394     /**
3395      * Creates a new background colour style
3396      *
3397      * @param string $value The value of the style
3398      * @return css_style_backgroundrepeat
3399      */
3400     public static function init($value) {
3401         return new css_style_backgroundrepeat('background-repeat', $value);
3402     }
3404     /**
3405      * Consolidates this style into a single background style
3406      *
3407      * @return string
3408      */
3409     public function consolidate_to() {
3410         return 'background';
3411     }
3414 /**
3415  * A background attachment style.
3416  *
3417  * @package core_css
3418  * @category css
3419  * @copyright 2012 Sam Hemelryk
3420  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3421  */
3422 class css_style_backgroundattachment extends css_style_generic {
3424     /**
3425      * Creates a new background colour style
3426      *
3427      * @param string $value The value of the style
3428      * @return css_style_backgroundattachment
3429      */
3430     public static function init($value) {
3431         return new css_style_backgroundattachment('background-attachment', $value);
3432     }
3434     /**
3435      * Consolidates this style into a single background style
3436      *
3437      * @return string
3438      */
3439     public function consolidate_to() {
3440         return 'background';
3441     }
3444 /**
3445  * A background position style.
3446  *
3447  * @package core_css
3448  * @category css
3449  * @copyright 2012 Sam Hemelryk
3450  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3451  */
3452 class css_style_backgroundposition extends css_style_generic {
3454     /**
3455      * Creates a new background colour style
3456      *
3457      * @param string $value The value of the style
3458      * @return css_style_backgroundposition
3459      */
3460     public static function init($value) {
3461         return new css_style_backgroundposition('background-position', $value);
3462     }
3464     /**
3465      * Consolidates this style into a single background style
3466      *
3467      * @return string
3468      */
3469     public function consolidate_to() {
3470         return 'background';
3471     }
3474 /**
3475  * A padding style.
3476  *
3477  * @package core_css
3478  * @category css
3479  * @copyright 2012 Sam Hemelryk
3480  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3481  */
3482 class css_style_padding extends css_style_width {
3484     /**
3485      * Initialises this padding style into several individual padding styles
3486      *
3487      * @param string $value The value fo the style
3488      * @return array An array of padding styles
3489      */
3490     public static function init($value) {
3491         $important = '';
3492         if (strpos($value, '!important') !== false) {
3493             $important = ' !important';
3494             $value = str_replace('!important', '', $value);
3495         }
3497         $value = preg_replace('#\s+#', ' ', trim($value));
3498         $bits = explode(' ', $value, 4);
3500         $top = $right = $bottom = $left = null;
3501         if (count($bits) > 0) {
3502             $top = $right = $bottom = $left = array_shift($bits);
3503         }
3504         if (count($bits) > 0) {
3505             $right = $left = array_shift($bits);
3506         }
3507         if (count($bits) > 0) {
3508             $bottom = array_shift($bits);
3509         }
3510         if (count($bits) > 0) {
3511             $left = array_shift($bits);
3512         }
3513         return array(
3514             new css_style_paddingtop('padding-top', $top.$important),
3515             new css_style_paddingright('padding-right', $right.$important),
3516             new css_style_paddingbottom('padding-bottom', $bottom.$important),
3517             new css_style_paddingleft('padding-left', $left.$important)
3518         );
3519     }
3521     /**
3522      * Consolidates several padding styles into a single style.
3523      *
3524      * @param array $styles Array of padding styles
3525      * @return array Optimised+consolidated array of padding styles
3526      */
3527     public static function consolidate(array $styles) {
3528         if (count($styles) != 4) {
3529             return $styles;
3530         }
3531         $top = $right = $bottom = $left = null;
3532         foreach ($styles as $style) {
3533             switch ($style->get_name()) {
3534                 case 'padding-top' : $top = $style->get_value();break;
3535                 case 'padding-right' : $right = $style->get_value();break;
3536                 case 'padding-bottom' : $bottom = $style->get_value();break;
3537                 case 'padding-left' : $left = $style->get_value();break;
3538             }
3539         }
3540         if ($top == $bottom && $left == $right) {
3541             if ($top == $left) {
3542                 return array(new css_style_padding('padding', $top));
3543             } else {
3544                 return array(new css_style_padding('padding', "{$top} {$left}"));
3545             }
3546         } else if ($left == $right) {
3547             return array(new css_style_padding('padding', "{$top} {$right} {$bottom}"));
3548         } else {
3549             return array(new css_style_padding('padding', "{$top} {$right} {$bottom} {$left}"));
3550         }
3551     }
3554 /**
3555  * A padding top style.
3556  *
3557  * @package core_css
3558  * @category css
3559  * @copyright 2012 Sam Hemelryk
3560  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3561  */
3562 class css_style_paddingtop extends css_style_padding {
3564     /**
3565      * Initialises this style
3566      *
3567      * @param string $value The value of the style
3568      * @return css_style_paddingtop
3569      */
3570     public static function init($value) {
3571         return new css_style_paddingtop('padding-top', $value);
3572     }
3574     /**
3575      * Consolidates this style into a single padding style
3576      *
3577      * @return string
3578      */
3579     public function consolidate_to() {
3580         return 'padding';
3581     }
3584 /**
3585  * A padding right style.
3586  *
3587  * @package core_css
3588  * @category css
3589  * @copyright 2012 Sam Hemelryk
3590  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3591  */
3592 class css_style_paddingright extends css_style_padding {
3594     /**
3595      * Initialises this style
3596      *
3597      * @param string $value The value of the style
3598      * @return css_style_paddingright
3599      */
3600     public static function init($value) {
3601         return new css_style_paddingright('padding-right', $value);
3602     }
3604     /**
3605      * Consolidates this style into a single padding style
3606      *
3607      * @return string
3608      */
3609     public function consolidate_to() {
3610         return 'padding';
3611     }
3614 /**
3615  * A padding bottom style.
3616  *
3617  * @package core_css
3618  * @category css
3619  * @copyright 2012 Sam Hemelryk
3620  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3621  */
3622 class css_style_paddingbottom extends css_style_padding {
3624     /**
3625      * Initialises this style
3626      *
3627      * @param string $value The value of the style
3628      * @return css_style_paddingbottom
3629      */
3630     public static function init($value) {
3631         return new css_style_paddingbottom('padding-bottom', $value);
3632     }
3634     /**
3635      * Consolidates this style into a single padding style
3636      *
3637      * @return string
3638      */
3639     public function consolidate_to() {
3640         return 'padding';
3641     }
3644 /**
3645  * A padding left style.
3646  *
3647  * @package core_css
3648  * @category css
3649  * @copyright 2012 Sam Hemelryk
3650  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3651  */
3652 class css_style_paddingleft extends css_style_padding {
3654     /**
3655      * Initialises this style
3656      *
3657      * @param string $value The value of the style
3658      * @return css_style_paddingleft
3659      */
3660     public static function init($value) {
3661         return new css_style_paddingleft('padding-left', $value);
3662     }
3664     /**
3665      * Consolidates this style into a single padding style
3666      *
3667      * @return string
3668      */
3669     public function consolidate_to() {
3670         return 'padding';
3671     }