Merge branch 'wip-MDL-33825-m23' of git://github.com/samhemelryk/moodle
[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 {@link css_optimiser} class for greater detail.
21  *
22  * @package core
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 {@link 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);
311     $hex  = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/';
312     $rgb  = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i';
313     $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
314     $hsl  = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i';
315     $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
317     if (in_array(strtolower($value), array('inherit'))) {
318         return true;
319     } else if (preg_match($hex, $value)) {
320         return true;
321     } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
322         return true;
323     } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
324         // It is an RGB colour
325         return true;
326     } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
327         // It is an RGBA colour
328         return true;
329     } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
330         // It is an HSL colour
331         return true;
332     } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
333         // It is an HSLA colour
334         return true;
335     }
336     // Doesn't look like a colour.
337     return false;
340 /**
341  * Returns true is the passed value looks like a CSS width.
342  * In order to pass this test the value must be purely numerical or end with a
343  * valid CSS unit term.
344  *
345  * @param string|int $value
346  * @return boolean
347  */
348 function css_is_width($value) {
349     $value = trim($value);
350     if (in_array(strtolower($value), array('auto', 'inherit'))) {
351         return true;
352     }
353     if ((string)$value === '0' || preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) {
354         return true;
355     }
356     return false;
359 /**
360  * A simple sorting function to sort two array values on the number of items they contain
361  *
362  * @param array $a
363  * @param array $b
364  * @return int
365  */
366 function css_sort_by_count(array $a, array $b) {
367     $a = count($a);
368     $b = count($b);
369     if ($a == $b) {
370         return 0;
371     }
372     return ($a > $b) ? -1 : 1;
375 /**
376  * A basic CSS optimiser that strips out unwanted things and then processing the
377  * CSS organising styles and moving duplicates and useless CSS.
378  *
379  * This CSS optimiser works by reading through a CSS string one character at a
380  * time and building an object structure of the CSS.
381  * As part of that processing styles are expanded out as much as they can be to
382  * ensure we collect all mappings, at the end of the processing those styles are
383  * then combined into an optimised form to keep them as short as possible.
384  *
385  * @package core
386  * @category css
387  * @copyright 2012 Sam Hemelryk
388  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
389  */
390 class css_optimiser {
392     /**
393      * Used when the processor is about to start processing.
394      * Processing states. Used internally.
395      */
396     const PROCESSING_START = 0;
398     /**
399      * Used when the processor is currently processing a selector.
400      * Processing states. Used internally.
401      */
402     const PROCESSING_SELECTORS = 0;
404     /**
405      * Used when the processor is currently processing a style.
406      * Processing states. Used internally.
407      */
408     const PROCESSING_STYLES = 1;
410     /**
411      * Used when the processor is currently processing a comment.
412      * Processing states. Used internally.
413      */
414     const PROCESSING_COMMENT = 2;
416     /**
417      * Used when the processor is currently processing an @ rule.
418      * Processing states. Used internally.
419      */
420     const PROCESSING_ATRULE = 3;
422     /**
423      * The raw string length before optimisation.
424      * Stats variables set during and after processing
425      * @var int
426      */
427     protected $rawstrlen = 0;
429     /**
430      * The number of comments that were removed during optimisation.
431      * Stats variables set during and after processing
432      * @var int
433      */
434     protected $commentsincss = 0;
436     /**
437      * The number of rules in the CSS before optimisation.
438      * Stats variables set during and after processing
439      * @var int
440      */
441     protected $rawrules = 0;
443     /**
444      * The number of selectors using in CSS rules before optimisation.
445      * Stats variables set during and after processing
446      * @var int
447      */
448     protected $rawselectors = 0;
450     /**
451      * The string length after optimisation.
452      * Stats variables set during and after processing
453      * @var int
454      */
455     protected $optimisedstrlen = 0;
457     /**
458      * The number of rules after optimisation.
459      * Stats variables set during and after processing
460      * @var int
461      */
462     protected $optimisedrules = 0;
464     /**
465      * The number of selectors used in rules after optimisation.
466      * Stats variables set during and after processing
467      * @var int
468      */
469     protected $optimisedselectors = 0;
471     /**
472      * The start time of the optimisation.
473      * Stats variables set during and after processing
474      * @var int
475      */
476     protected $timestart = 0;
478     /**
479      * The end time of the optimisation.
480      * Stats variables set during and after processing
481      * @var int
482      */
483     protected $timecomplete = 0;
485     /**
486      * Will be set to any errors that may have occured during processing.
487      * This is updated only at the end of processing NOT during.
488      *
489      * @var array
490      */
491     protected $errors = array();
493     /**
494      * Processes incoming CSS optimising it and then returning it.
495      *
496      * @param string $css The raw CSS to optimise
497      * @return string The optimised CSS
498      */
499     public function process($css) {
500         global $CFG;
502         // Easiest win there is
503         $css = trim($css);
505         $this->reset_stats();
506         $this->timestart = microtime(true);
507         $this->rawstrlen = strlen($css);
509         // Don't try to process files with no content... it just doesn't make sense.
510         // But we should produce an error for them, an empty CSS file will lead to a
511         // useless request for those running theme designer mode.
512         if ($this->rawstrlen === 0) {
513             $this->errors[] = 'Skipping file as it has no content.';
514             return '';
515         }
517         // First up we need to remove all line breaks - this allows us to instantly
518         // reduce our processing requirements and as we will process everything
519         // into a new structure there's really nothing lost.
520         $css = preg_replace('#\r?\n#', ' ', $css);
522         // Next remove the comments... no need to them in an optimised world and
523         // knowing they're all gone allows us to REALLY make our processing simpler
524         $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
526         $medias = array(
527             'all' => new css_media()
528         );
529         $imports = array();
530         $charset = false;
531         // Keyframes are used for CSS animation they will be processed right at the very end.
532         $keyframes = array();
534         $currentprocess = self::PROCESSING_START;
535         $currentrule = css_rule::init();
536         $currentselector = css_selector::init();
537         $inquotes = false;      // ' or "
538         $inbraces = false;      // {
539         $inbrackets = false;    // [
540         $inparenthesis = false; // (
541         $currentmedia = $medias['all'];
542         $currentatrule = null;
543         $suspectatrule = false;
545         $buffer = '';
546         $char = null;
548         // Next we are going to iterate over every single character in $css.
549         // This is why we removed line breaks and comments!
550         for ($i = 0; $i < $this->rawstrlen; $i++) {
551             $lastchar = $char;
552             $char = substr($css, $i, 1);
553             if ($char == '@' && $buffer == '') {
554                 $suspectatrule = true;
555             }
556             switch ($currentprocess) {
557                 // Start processing an @ rule e.g. @media, @page, @keyframes
558                 case self::PROCESSING_ATRULE:
559                     switch ($char) {
560                         case ';':
561                             if (!$inbraces) {
562                                 $buffer .= $char;
563                                 if ($currentatrule == 'import') {
564                                     $imports[] = $buffer;
565                                     $currentprocess = self::PROCESSING_SELECTORS;
566                                 } else if ($currentatrule == 'charset') {
567                                     $charset = $buffer;
568                                     $currentprocess = self::PROCESSING_SELECTORS;
569                                 }
570                             }
571                             if ($currentatrule !== 'media') {
572                                 $buffer = '';
573                                 $currentatrule = false;
574                             }
575                             // continue 1: The switch processing chars
576                             // continue 2: The switch processing the state
577                             // continue 3: The for loop
578                             continue 3;
579                         case '{':
580                             if ($currentatrule == 'media' && preg_match('#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#', $buffer, $matches)) {
581                                 // Basic media declaration
582                                 $mediatypes = str_replace(' ', '', $matches[1]);
583                                 if (!array_key_exists($mediatypes, $medias)) {
584                                     $medias[$mediatypes] = new css_media($mediatypes);
585                                 }
586                                 $currentmedia = $medias[$mediatypes];
587                                 $currentprocess = self::PROCESSING_SELECTORS;
588                                 $buffer = '';
589                             } else if ($currentatrule == 'media' && preg_match('#\s*@media\s*([^{]+)#', $buffer, $matches)) {
590                                 // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/
591                                 $mediatypes = $matches[1];
592                                 $hash = md5($mediatypes);
593                                 $medias[$hash] = new css_media($mediatypes);
594                                 $currentmedia = $medias[$hash];
595                                 $currentprocess = self::PROCESSING_SELECTORS;
596                                 $buffer = '';
597                             } else if ($currentatrule == 'keyframes' && preg_match('#@((\-moz\-|\-webkit\-)?keyframes)\s*([^\s]+)#', $buffer, $matches)) {
598                                 // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow
599                                 // them to be overridden to ensure we don't mess anything up. (means we keep everything in order)
600                                 $keyframefor = $matches[1];
601                                 $keyframename = $matches[3];
602                                 $keyframe = new css_keyframe($keyframefor, $keyframename);
603                                 $keyframes[] = $keyframe;
604                                 $currentmedia = $keyframe;
605                                 $currentprocess = self::PROCESSING_SELECTORS;
606                                 $buffer = '';
607                             }
608                             // continue 1: The switch processing chars
609                             // continue 2: The switch processing the state
610                             // continue 3: The for loop
611                             continue 3;
612                     }
613                     break;
614                 // Start processing selectors
615                 case self::PROCESSING_START:
616                 case self::PROCESSING_SELECTORS:
617                     switch ($char) {
618                         case '[':
619                             $inbrackets ++;
620                             $buffer .= $char;
621                             // continue 1: The switch processing chars
622                             // continue 2: The switch processing the state
623                             // continue 3: The for loop
624                             continue 3;
625                         case ']':
626                             $inbrackets --;
627                             $buffer .= $char;
628                             // continue 1: The switch processing chars
629                             // continue 2: The switch processing the state
630                             // continue 3: The for loop
631                             continue 3;
632                         case ' ':
633                             if ($inbrackets) {
634                                 // continue 1: The switch processing chars
635                                 // continue 2: The switch processing the state
636                                 // continue 3: The for loop
637                                 continue 3;
638                             }
639                             if (!empty($buffer)) {
640                                 // Check for known @ rules
641                                 if ($suspectatrule && preg_match('#@(media|import|charset|(\-moz\-|\-webkit\-)?(keyframes))\s*#', $buffer, $matches)) {
642                                     $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
643                                     $currentprocess = self::PROCESSING_ATRULE;
644                                     $buffer .= $char;
645                                 } else {
646                                     $currentselector->add($buffer);
647                                     $buffer = '';
648                                 }
649                             }
650                             $suspectatrule = false;
651                             // continue 1: The switch processing chars
652                             // continue 2: The switch processing the state
653                             // continue 3: The for loop
654                             continue 3;
655                         case '{':
656                             if ($inbrackets) {
657                                 // continue 1: The switch processing chars
658                                 // continue 2: The switch processing the state
659                                 // continue 3: The for loop
660                                 continue 3;
661                             }
662                             if ($buffer !== '') {
663                                 $currentselector->add($buffer);
664                             }
665                             $currentrule->add_selector($currentselector);
666                             $currentselector = css_selector::init();
667                             $currentprocess = self::PROCESSING_STYLES;
669                             $buffer = '';
670                             // continue 1: The switch processing chars
671                             // continue 2: The switch processing the state
672                             // continue 3: The for loop
673                             continue 3;
674                         case '}':
675                             if ($inbrackets) {
676                                 // continue 1: The switch processing chars
677                                 // continue 2: The switch processing the state
678                                 // continue 3: The for loop
679                                 continue 3;
680                             }
681                             if ($currentatrule == 'media') {
682                                 $currentmedia = $medias['all'];
683                                 $currentatrule = false;
684                                 $buffer = '';
685                             } else if (strpos($currentatrule, 'keyframes') !== false) {
686                                 $currentmedia = $medias['all'];
687                                 $currentatrule = false;
688                                 $buffer = '';
689                             }
690                             // continue 1: The switch processing chars
691                             // continue 2: The switch processing the state
692                             // continue 3: The for loop
693                             continue 3;
694                         case ',':
695                             if ($inbrackets) {
696                                 // continue 1: The switch processing chars
697                                 // continue 2: The switch processing the state
698                                 // continue 3: The for loop
699                                 continue 3;
700                             }
701                             $currentselector->add($buffer);
702                             $currentrule->add_selector($currentselector);
703                             $currentselector = css_selector::init();
704                             $buffer = '';
705                             // continue 1: The switch processing chars
706                             // continue 2: The switch processing the state
707                             // continue 3: The for loop
708                             continue 3;
709                     }
710                     break;
711                 // Start processing styles
712                 case self::PROCESSING_STYLES:
713                     if ($char == '"' || $char == "'") {
714                         if ($inquotes === false) {
715                             $inquotes = $char;
716                         }
717                         if ($inquotes === $char && $lastchar !== '\\') {
718                             $inquotes = false;
719                         }
720                     }
721                     if ($inquotes) {
722                         $buffer .= $char;
723                         continue 2;
724                     }
725                     switch ($char) {
726                         case ';':
727                             if ($inparenthesis) {
728                                 $buffer .= $char;
729                                 // continue 1: The switch processing chars
730                                 // continue 2: The switch processing the state
731                                 // continue 3: The for loop
732                                 continue 3;
733                             }
734                             $currentrule->add_style($buffer);
735                             $buffer = '';
736                             $inquotes = false;
737                             // continue 1: The switch processing chars
738                             // continue 2: The switch processing the state
739                             // continue 3: The for loop
740                             continue 3;
741                         case '}':
742                             $currentrule->add_style($buffer);
743                             $this->rawselectors += $currentrule->get_selector_count();
745                             $currentmedia->add_rule($currentrule);
747                             $currentrule = css_rule::init();
748                             $currentprocess = self::PROCESSING_SELECTORS;
749                             $this->rawrules++;
750                             $buffer = '';
751                             $inquotes = false;
752                             $inparenthesis = false;
753                             // continue 1: The switch processing chars
754                             // continue 2: The switch processing the state
755                             // continue 3: The for loop
756                             continue 3;
757                         case '(':
758                             $inparenthesis = true;
759                             $buffer .= $char;
760                             // continue 1: The switch processing chars
761                             // continue 2: The switch processing the state
762                             // continue 3: The for loop
763                             continue 3;
764                         case ')':
765                             $inparenthesis = false;
766                             $buffer .= $char;
767                             // continue 1: The switch processing chars
768                             // continue 2: The switch processing the state
769                             // continue 3: The for loop
770                             continue 3;
771                     }
772                     break;
773             }
774             $buffer .= $char;
775         }
777         foreach ($medias as $media) {
778             $this->optimise($media);
779         }
780         $css = $this->produce_css($charset, $imports, $medias, $keyframes);
782         $this->timecomplete = microtime(true);
783         return trim($css);
784     }
786     /**
787      * Produces CSS for the given charset, imports, media, and keyframes
788      * @param string $charset
789      * @param array $imports
790      * @param array $medias
791      * @param array $keyframes
792      * @return string
793      */
794     protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
795         $css = '';
796         if (!empty($charset)) {
797             $imports[] = $charset;
798         }
799         if (!empty($imports)) {
800             $css .= implode("\n", $imports);
801             $css .= "\n\n";
802         }
804         $cssreset = array();
805         $cssstandard = array();
806         $csskeyframes = array();
808         // Process each media declaration individually
809         foreach ($medias as $media) {
810             // If this declaration applies to all media types
811             if (in_array('all', $media->get_types())) {
812                 // Collect all rules that represet reset rules and remove them from the media object at the same time.
813                 // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they
814                 // can't end up out of order because of optimisation.
815                 $resetrules = $media->get_reset_rules(true);
816                 if (!empty($resetrules)) {
817                     $cssreset[] = css_writer::media('all', $resetrules);
818                 }
819             }
820             // Get the standard cSS
821             $cssstandard[] = $media->out();
822         }
824         // Finally if there are any keyframe declarations process them now.
825         if (count($keyframes) > 0) {
826             foreach ($keyframes as $keyframe) {
827                 $this->optimisedrules += $keyframe->count_rules();
828                 $this->optimisedselectors +=  $keyframe->count_selectors();
829                 if ($keyframe->has_errors()) {
830                     $this->errors += $keyframe->get_errors();
831                 }
832                 $csskeyframes[] = $keyframe->out();
833             }
834         }
836         // Join it all together
837         $css .= join('', $cssreset);
838         $css .= join('', $cssstandard);
839         $css .= join('', $csskeyframes);
841         // Record the strlenght of the now optimised CSS.
842         $this->optimisedstrlen = strlen($css);
844         // Return the now produced CSS
845         return $css;
846     }
848     /**
849      * Optimises the CSS rules within a rule collection of one form or another
850      *
851      * @param css_rule_collection $media
852      * @return void This function acts in reference
853      */
854     protected function optimise(css_rule_collection $media) {
855         $media->organise_rules_by_selectors();
856         $this->optimisedrules += $media->count_rules();
857         $this->optimisedselectors +=  $media->count_selectors();
858         if ($media->has_errors()) {
859             $this->errors += $media->get_errors();
860         }
861     }
863     /**
864      * Returns an array of stats from the last processing run
865      * @return string
866      */
867     public function get_stats() {
868         $stats = array(
869             'timestart'             => $this->timestart,
870             'timecomplete'          => $this->timecomplete,
871             'timetaken'             => round($this->timecomplete - $this->timestart, 4),
872             'commentsincss'         => $this->commentsincss,
873             'rawstrlen'             => $this->rawstrlen,
874             'rawselectors'          => $this->rawselectors,
875             'rawrules'              => $this->rawrules,
876             'optimisedstrlen'       => $this->optimisedstrlen,
877             'optimisedrules'        => $this->optimisedrules,
878             'optimisedselectors'    => $this->optimisedselectors,
879             'improvementstrlen'     => '-',
880             'improvementrules'     => '-',
881             'improvementselectors'     => '-',
882         );
883         // Avoid division by 0 errors by checking we have valid raw values
884         if ($this->rawstrlen > 0) {
885             $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%';
886         }
887         if ($this->rawrules > 0) {
888             $stats['improvementrules'] = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%';
889         }
890         if ($this->rawselectors > 0) {
891             $stats['improvementselectors'] = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%';
892         }
893         return $stats;
894     }
896     /**
897      * Returns true if any errors have occured during processing
898      *
899      * @return bool
900      */
901     public function has_errors() {
902         return !empty($this->errors);
903     }
905     /**
906      * Returns an array of errors that have occured
907      *
908      * @param bool $clear If set to true the errors will be cleared after being returned.
909      * @return array
910      */
911     public function get_errors($clear = false) {
912         $errors = $this->errors;
913         if ($clear) {
914             // Reset the error array
915             $this->errors = array();
916         }
917         return $errors;
918     }
920     /**
921      * Returns any errors as a string that can be included in CSS.
922      *
923      * @return string
924      */
925     public function output_errors_css() {
926         $computedcss  = "/****************************************\n";
927         $computedcss .= " *--- Errors found during processing ----\n";
928         foreach ($this->errors as $error) {
929             $computedcss .= preg_replace('#^#m', '* ', $error);
930         }
931         $computedcss .= " ****************************************/\n\n";
932         return $computedcss;
933     }
935     /**
936      * Returns a string to display stats about the last generation within CSS output
937      *
938      * @return string
939      */
940     public function output_stats_css() {
942         $computedcss  = "/****************************************\n";
943         $computedcss .= " *------- CSS Optimisation stats --------\n";
945         if ($this->rawstrlen === 0) {
946             $computedcss .= " File not processed as it has no content /\n\n";
947             $computedcss .= " ****************************************/\n\n";
948             return $computedcss;
949         } else if ($this->rawrules === 0) {
950             $computedcss .= " File contained no rules to be processed /\n\n";
951             $computedcss .= " ****************************************/\n\n";
952             return $computedcss;
953         }
955         $stats = $this->get_stats();
957         $computedcss .= " *  ".date('r')."\n";
958         $computedcss .= " *  {$stats['commentsincss']}  \t comments removed\n";
959         $computedcss .= " *  Optimisation took {$stats['timetaken']} seconds\n";
960         $computedcss .= " *--------------- before ----------------\n";
961         $computedcss .= " *  {$stats['rawstrlen']}  \t chars read in\n";
962         $computedcss .= " *  {$stats['rawrules']}  \t rules read in\n";
963         $computedcss .= " *  {$stats['rawselectors']}  \t total selectors\n";
964         $computedcss .= " *---------------- after ----------------\n";
965         $computedcss .= " *  {$stats['optimisedstrlen']}  \t chars once optimized\n";
966         $computedcss .= " *  {$stats['optimisedrules']}  \t optimized rules\n";
967         $computedcss .= " *  {$stats['optimisedselectors']}  \t total selectors once optimized\n";
968         $computedcss .= " *---------------- stats ----------------\n";
969         $computedcss .= " *  {$stats['improvementstrlen']}  \t reduction in chars\n";
970         $computedcss .= " *  {$stats['improvementrules']}  \t reduction in rules\n";
971         $computedcss .= " *  {$stats['improvementselectors']}  \t reduction in selectors\n";
972         $computedcss .= " ****************************************/\n\n";
974         return $computedcss;
975     }
977     /**
978      * Resets the stats ready for another fresh processing
979      */
980     public function reset_stats() {
981         $this->commentsincss = 0;
982         $this->optimisedrules = 0;
983         $this->optimisedselectors = 0;
984         $this->optimisedstrlen = 0;
985         $this->rawrules = 0;
986         $this->rawselectors = 0;
987         $this->rawstrlen = 0;
988         $this->timecomplete = 0;
989         $this->timestart = 0;
990     }
992     /**
993      * An array of the common HTML colours that are supported by most browsers.
994      *
995      * This reference table is used to allow us to unify colours, and will aid
996      * us in identifying buggy CSS using unsupported colours.
997      *
998      * @staticvar array
999      * @var array
1000      */
1001     public static $htmlcolours = array(
1002         'aliceblue' => '#F0F8FF',
1003         'antiquewhite' => '#FAEBD7',
1004         'aqua' => '#00FFFF',
1005         'aquamarine' => '#7FFFD4',
1006         'azure' => '#F0FFFF',
1007         'beige' => '#F5F5DC',
1008         'bisque' => '#FFE4C4',
1009         'black' => '#000000',
1010         'blanchedalmond' => '#FFEBCD',
1011         'blue' => '#0000FF',
1012         'blueviolet' => '#8A2BE2',
1013         'brown' => '#A52A2A',
1014         'burlywood' => '#DEB887',
1015         'cadetblue' => '#5F9EA0',
1016         'chartreuse' => '#7FFF00',
1017         'chocolate' => '#D2691E',
1018         'coral' => '#FF7F50',
1019         'cornflowerblue' => '#6495ED',
1020         'cornsilk' => '#FFF8DC',
1021         'crimson' => '#DC143C',
1022         'cyan' => '#00FFFF',
1023         'darkblue' => '#00008B',
1024         'darkcyan' => '#008B8B',
1025         'darkgoldenrod' => '#B8860B',
1026         'darkgray' => '#A9A9A9',
1027         'darkgrey' => '#A9A9A9',
1028         'darkgreen' => '#006400',
1029         'darkKhaki' => '#BDB76B',
1030         'darkmagenta' => '#8B008B',
1031         'darkolivegreen' => '#556B2F',
1032         'arkorange' => '#FF8C00',
1033         'darkorchid' => '#9932CC',
1034         'darkred' => '#8B0000',
1035         'darksalmon' => '#E9967A',
1036         'darkseagreen' => '#8FBC8F',
1037         'darkslateblue' => '#483D8B',
1038         'darkslategray' => '#2F4F4F',
1039         'darkslategrey' => '#2F4F4F',
1040         'darkturquoise' => '#00CED1',
1041         'darkviolet' => '#9400D3',
1042         'deeppink' => '#FF1493',
1043         'deepskyblue' => '#00BFFF',
1044         'dimgray' => '#696969',
1045         'dimgrey' => '#696969',
1046         'dodgerblue' => '#1E90FF',
1047         'firebrick' => '#B22222',
1048         'floralwhite' => '#FFFAF0',
1049         'forestgreen' => '#228B22',
1050         'fuchsia' => '#FF00FF',
1051         'gainsboro' => '#DCDCDC',
1052         'ghostwhite' => '#F8F8FF',
1053         'gold' => '#FFD700',
1054         'goldenrod' => '#DAA520',
1055         'gray' => '#808080',
1056         'grey' => '#808080',
1057         'green' => '#008000',
1058         'greenyellow' => '#ADFF2F',
1059         'honeydew' => '#F0FFF0',
1060         'hotpink' => '#FF69B4',
1061         'indianred ' => '#CD5C5C',
1062         'indigo ' => '#4B0082',
1063         'ivory' => '#FFFFF0',
1064         'khaki' => '#F0E68C',
1065         'lavender' => '#E6E6FA',
1066         'lavenderblush' => '#FFF0F5',
1067         'lawngreen' => '#7CFC00',
1068         'lemonchiffon' => '#FFFACD',
1069         'lightblue' => '#ADD8E6',
1070         'lightcoral' => '#F08080',
1071         'lightcyan' => '#E0FFFF',
1072         'lightgoldenrodyellow' => '#FAFAD2',
1073         'lightgray' => '#D3D3D3',
1074         'lightgrey' => '#D3D3D3',
1075         'lightgreen' => '#90EE90',
1076         'lightpink' => '#FFB6C1',
1077         'lightsalmon' => '#FFA07A',
1078         'lightseagreen' => '#20B2AA',
1079         'lightskyblue' => '#87CEFA',
1080         'lightslategray' => '#778899',
1081         'lightslategrey' => '#778899',
1082         'lightsteelblue' => '#B0C4DE',
1083         'lightyellow' => '#FFFFE0',
1084         'lime' => '#00FF00',
1085         'limegreen' => '#32CD32',
1086         'linen' => '#FAF0E6',
1087         'magenta' => '#FF00FF',
1088         'maroon' => '#800000',
1089         'mediumaquamarine' => '#66CDAA',
1090         'mediumblue' => '#0000CD',
1091         'mediumorchid' => '#BA55D3',
1092         'mediumpurple' => '#9370D8',
1093         'mediumseagreen' => '#3CB371',
1094         'mediumslateblue' => '#7B68EE',
1095         'mediumspringgreen' => '#00FA9A',
1096         'mediumturquoise' => '#48D1CC',
1097         'mediumvioletred' => '#C71585',
1098         'midnightblue' => '#191970',
1099         'mintcream' => '#F5FFFA',
1100         'mistyrose' => '#FFE4E1',
1101         'moccasin' => '#FFE4B5',
1102         'navajowhite' => '#FFDEAD',
1103         'navy' => '#000080',
1104         'oldlace' => '#FDF5E6',
1105         'olive' => '#808000',
1106         'olivedrab' => '#6B8E23',
1107         'orange' => '#FFA500',
1108         'orangered' => '#FF4500',
1109         'orchid' => '#DA70D6',
1110         'palegoldenrod' => '#EEE8AA',
1111         'palegreen' => '#98FB98',
1112         'paleturquoise' => '#AFEEEE',
1113         'palevioletred' => '#D87093',
1114         'papayawhip' => '#FFEFD5',
1115         'peachpuff' => '#FFDAB9',
1116         'peru' => '#CD853F',
1117         'pink' => '#FFC0CB',
1118         'plum' => '#DDA0DD',
1119         'powderblue' => '#B0E0E6',
1120         'purple' => '#800080',
1121         'red' => '#FF0000',
1122         'rosybrown' => '#BC8F8F',
1123         'royalblue' => '#4169E1',
1124         'saddlebrown' => '#8B4513',
1125         'salmon' => '#FA8072',
1126         'sandybrown' => '#F4A460',
1127         'seagreen' => '#2E8B57',
1128         'seashell' => '#FFF5EE',
1129         'sienna' => '#A0522D',
1130         'silver' => '#C0C0C0',
1131         'skyblue' => '#87CEEB',
1132         'slateblue' => '#6A5ACD',
1133         'slategray' => '#708090',
1134         'slategrey' => '#708090',
1135         'snow' => '#FFFAFA',
1136         'springgreen' => '#00FF7F',
1137         'steelblue' => '#4682B4',
1138         'tan' => '#D2B48C',
1139         'teal' => '#008080',
1140         'thistle' => '#D8BFD8',
1141         'tomato' => '#FF6347',
1142         'transparent' => 'transparent',
1143         'turquoise' => '#40E0D0',
1144         'violet' => '#EE82EE',
1145         'wheat' => '#F5DEB3',
1146         'white' => '#FFFFFF',
1147         'whitesmoke' => '#F5F5F5',
1148         'yellow' => '#FFFF00',
1149         'yellowgreen' => '#9ACD32'
1150     );
1153 /**
1154  * Used to prepare CSS strings
1155  *
1156  * @package core
1157  * @category css
1158  * @copyright 2012 Sam Hemelryk
1159  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1160  */
1161 abstract class css_writer {
1163     /**
1164      * The current indent level
1165      * @var int
1166      */
1167     protected static $indent = 0;
1169     /**
1170      * Returns true if the output should still maintain minimum formatting.
1171      * @return bool
1172      */
1173     protected static function is_pretty() {
1174         global $CFG;
1175         return (!empty($CFG->cssoptimiserpretty));
1176     }
1178     /**
1179      * Returns the indenting char to use for indenting things nicely.
1180      * @return string
1181      */
1182     protected static function get_indent() {
1183         if (self::is_pretty()) {
1184             return str_repeat("  ", self::$indent);
1185         }
1186         return '';
1187     }
1189     /**
1190      * Increases the current indent
1191      */
1192     protected static function increase_indent() {
1193         self::$indent++;
1194     }
1196     /**
1197      * Decreases the current indent
1198      */
1199     protected static function decrease_indent() {
1200         self::$indent--;
1201     }
1203     /**
1204      * Returns the string to use as a separator
1205      * @return string
1206      */
1207     protected static function get_separator() {
1208         return (self::is_pretty())?"\n":' ';
1209     }
1211     /**
1212      * Returns CSS for media
1213      *
1214      * @param string $typestring
1215      * @param array $rules An array of css_rule objects
1216      * @return string
1217      */
1218     public static function media($typestring, array &$rules) {
1219         $nl = self::get_separator();
1221         $output = '';
1222         if ($typestring !== 'all') {
1223             $output .= "\n@media {$typestring} {".$nl;
1224             self::increase_indent();
1225         }
1226         foreach ($rules as $rule) {
1227             $output .= $rule->out().$nl;
1228         }
1229         if ($typestring !== 'all') {
1230             self::decrease_indent();
1231             $output .= '}';
1232         }
1233         return $output;
1234     }
1236     /**
1237      * Returns CSS for a keyframe
1238      *
1239      * @param string $for The desired declaration. e.g. keyframes, -moz-keyframes, -webkit-keyframes
1240      * @param string $name The name for the keyframe
1241      * @param array $rules An array of rules belonging to the keyframe
1242      * @return string
1243      */
1244     public static function keyframe($for, $name, array &$rules) {
1245         $nl = self::get_separator();
1247         $output = "\n@{$for} {$name} {";
1248         foreach ($rules as $rule) {
1249             $output .= $rule->out();
1250         }
1251         $output .= '}';
1252         return $output;
1253     }
1255     /**
1256      * Returns CSS for a rule
1257      *
1258      * @param string $selector
1259      * @param string $styles
1260      * @return string
1261      */
1262     public static function rule($selector, $styles) {
1263         $css = self::get_indent()."{$selector}{{$styles}}";
1264         return $css;
1265     }
1267     /**
1268      * Returns CSS for the selectors of a rule
1269      *
1270      * @param array $selectors Array of css_selector objects
1271      * @return string
1272      */
1273     public static function selectors(array $selectors) {
1274         $nl = self::get_separator();
1275         $selectorstrings = array();
1276         foreach ($selectors as $selector) {
1277             $selectorstrings[] = $selector->out();
1278         }
1279         return join(','.$nl, $selectorstrings);
1280     }
1282     /**
1283      * Returns a selector given the components that make it up.
1284      *
1285      * @param array $components
1286      * @return string
1287      */
1288     public static function selector(array $components) {
1289         return trim(join(' ', $components));
1290     }
1292     /**
1293      * Returns a CSS string for the provided styles
1294      *
1295      * @param array $styles Array of css_style objects
1296      * @return string
1297      */
1298     public static function styles(array $styles) {
1299         $bits = array();
1300         foreach ($styles as $style) {
1301             // Check if the style is an array. If it is then we are outputing an advanced style.
1302             // An advanced style is a style with one or more values, and can occur in situations like background-image
1303             // where browse specific values are being used.
1304             if (is_array($style)) {
1305                 foreach ($style as $advstyle) {
1306                     $bits[] = $advstyle->out();
1307                 }
1308                 continue;
1309             }
1310             $bits[] = $style->out();
1311         }
1312         return join('', $bits);
1313     }
1315     /**
1316      * Returns a style CSS
1317      *
1318      * @param string $name
1319      * @param string $value
1320      * @param bool $important
1321      * @return string
1322      */
1323     public static function style($name, $value, $important = false) {
1324         $value = trim($value);
1325         if ($important && strpos($value, '!important') === false) {
1326             $value .= ' !important';
1327         }
1328         return "{$name}:{$value};";
1329     }
1332 /**
1333  * A structure to represent a CSS selector.
1334  *
1335  * The selector is the classes, id, elements, and psuedo bits that make up a CSS
1336  * rule.
1337  *
1338  * @package core
1339  * @category css
1340  * @copyright 2012 Sam Hemelryk
1341  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1342  */
1343 class css_selector {
1345     /**
1346      * An array of selector bits
1347      * @var array
1348      */
1349     protected $selectors = array();
1351     /**
1352      * The number of selectors.
1353      * @var int
1354      */
1355     protected $count = 0;
1357     /**
1358      * Is null if there are no selectors, true if all selectors are basic and false otherwise.
1359      * A basic selector is one that consists of just the element type. e.g. div, span, td, a
1360      * @var bool|null
1361      */
1362     protected $isbasic = null;
1364     /**
1365      * Initialises a new CSS selector
1366      * @return css_selector
1367      */
1368     public static function init() {
1369         return new css_selector();
1370     }
1372     /**
1373      * CSS selectors can only be created through the init method above.
1374      */
1375     protected function __construct() {}
1377     /**
1378      * Adds a selector to the end of the current selector
1379      * @param string $selector
1380      */
1381     public function add($selector) {
1382         $selector = trim($selector);
1383         $count = 0;
1384         $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
1385         if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
1386             $count ++;
1387         }
1388         // If its already false then no need to continue, its not basic
1389         if ($this->isbasic !== false) {
1390             // If theres more than one part making up this selector its not basic
1391             if ($count > 1) {
1392                 $this->isbasic = false;
1393             } else {
1394                 // Check whether it is a basic element (a-z+) with possible psuedo selector
1395                 $this->isbasic = (bool)preg_match('#^[a-z]+(:[a-zA-Z]+)?$#', $selector);
1396             }
1397         }
1398         $this->count = $count;
1399         $this->selectors[] = $selector;
1400     }
1401     /**
1402      * Returns the number of individual components that make up this selector
1403      * @return int
1404      */
1405     public function get_selector_count() {
1406         return $this->count;
1407     }
1409     /**
1410      * Returns the selector for use in a CSS rule
1411      * @return string
1412      */
1413     public function out() {
1414         return css_writer::selector($this->selectors);
1415     }
1417     /**
1418      * Returns true is all of the selectors act only upon basic elements (no classes/ids)
1419      * @return bool
1420      */
1421     public function is_basic() {
1422         return ($this->isbasic === true);
1423     }
1426 /**
1427  * A structure to represent a CSS rule.
1428  *
1429  * @package core
1430  * @category css
1431  * @copyright 2012 Sam Hemelryk
1432  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1433  */
1434 class css_rule {
1436     /**
1437      * An array of CSS selectors {@link css_selector}
1438      * @var array
1439      */
1440     protected $selectors = array();
1442     /**
1443      * An array of CSS styles {@link css_style}
1444      * @var array
1445      */
1446     protected $styles = array();
1448     /**
1449      * Created a new CSS rule. This is the only way to create a new CSS rule externally.
1450      * @return css_rule
1451      */
1452     public static function init() {
1453         return new css_rule();
1454     }
1456     /**
1457      * Constructs a new css rule.
1458      *
1459      * @param string $selector The selector or array of selectors that make up this rule.
1460      * @param array $styles An array of styles that belong to this rule.
1461      */
1462     protected function __construct($selector = null, array $styles = array()) {
1463         if ($selector != null) {
1464             if (is_array($selector)) {
1465                 $this->selectors = $selector;
1466             } else {
1467                 $this->selectors = array($selector);
1468             }
1469             $this->add_styles($styles);
1470         }
1471     }
1473     /**
1474      * Adds a new CSS selector to this rule
1475      *
1476      * e.g. $rule->add_selector('.one #two.two');
1477      *
1478      * @param css_selector $selector Adds a CSS selector to this rule.
1479      */
1480     public function add_selector(css_selector $selector) {
1481         $this->selectors[] = $selector;
1482     }
1484     /**
1485      * Adds a new CSS style to this rule.
1486      *
1487      * @param css_style|string $style Adds a new style to this rule
1488      */
1489     public function add_style($style) {
1490         if (is_string($style)) {
1491             $style = trim($style);
1492             if (empty($style)) {
1493                 return;
1494             }
1495             $bits = explode(':', $style, 2);
1496             if (count($bits) == 2) {
1497                 list($name, $value) = array_map('trim', $bits);
1498             }
1499             if (isset($name) && isset($value) && $name !== '' && $value !== '') {
1500                 $style = css_style::init_automatic($name, $value);
1501             }
1502         } else if ($style instanceof css_style) {
1503             // Clone the style as it may be coming from another rule and we don't
1504             // want references as it will likely be overwritten by proceeding
1505             // rules
1506             $style = clone($style);
1507         }
1508         if ($style instanceof css_style) {
1509             $name = $style->get_name();
1510             $exists = array_key_exists($name, $this->styles);
1511             // We need to find out if the current style support multiple values, or whether the style
1512             // is already set up to record multiple values. This can happen with background images which can have single
1513             // and multiple values.
1514             if ($style->allows_multiple_values() || ($exists && is_array($this->styles[$name]))) {
1515                 if (!$exists) {
1516                     $this->styles[$name] = array();
1517                 } else if ($this->styles[$name] instanceof css_style) {
1518                     $this->styles[$name] = array($this->styles[$name]);
1519                 }
1520                 $this->styles[$name][] = $style;
1521             } else if ($exists) {
1522                 $this->styles[$name]->set_value($style->get_value());
1523             } else {
1524                 $this->styles[$name] = $style;
1525             }
1526         } else if (is_array($style)) {
1527             // We probably shouldn't worry about processing styles here but to
1528             // be truthful it doesn't hurt.
1529             foreach ($style as $astyle) {
1530                 $this->add_style($astyle);
1531             }
1532         }
1533     }
1535     /**
1536      * An easy method of adding several styles at once. Just calls add_style.
1537      *
1538      * This method simply iterates over the array and calls {@link css_rule::add_style()}
1539      * with each.
1540      *
1541      * @param array $styles Adds an array of styles
1542      */
1543     public function add_styles(array $styles) {
1544         foreach ($styles as $style) {
1545             $this->add_style($style);
1546         }
1547     }
1549     /**
1550      * Returns the array of selectors
1551      *
1552      * @return array
1553      */
1554     public function get_selectors() {
1555         return $this->selectors;
1556     }
1558     /**
1559      * Returns the array of styles
1560      *
1561      * @return array
1562      */
1563     public function get_styles() {
1564         return $this->styles;
1565     }
1567     /**
1568      * Outputs this rule as a fragment of CSS
1569      *
1570      * @return string
1571      */
1572     public function out() {
1573         $selectors = css_writer::selectors($this->selectors);
1574         $styles = css_writer::styles($this->get_consolidated_styles());
1575         return css_writer::rule($selectors, $styles);
1576     }
1578     /**
1579      * Consolidates all styles associated with this rule
1580      *
1581      * @return array An array of consolidated styles
1582      */
1583     public function get_consolidated_styles() {
1584         $organisedstyles = array();
1585         $finalstyles = array();
1586         $consolidate = array();
1587         $advancedstyles = array();
1588         foreach ($this->styles as $style) {
1589             // If the style is an array then we are processing an advanced style. An advanced style is a style that can have
1590             // one or more values. Background-image is one such example as it can have browser specific styles.
1591             if (is_array($style)) {
1592                 $single = null;
1593                 $count = 0;
1594                 foreach ($style as $advstyle) {
1595                     $key = $count++;
1596                     $advancedstyles[$key] = $advstyle;
1597                     if (!$advstyle->allows_multiple_values()) {
1598                         if (!is_null($single)) {
1599                             unset($advancedstyles[$single]);
1600                         }
1601                         $single = $key;
1602                     }
1603                 }
1604                 if (!is_null($single)) {
1605                     $style = $advancedstyles[$single];
1607                     $consolidatetoclass = $style->consolidate_to();
1608                     if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) {
1609                         $class = 'css_style_'.$consolidatetoclass;
1610                         if (!array_key_exists($class, $consolidate)) {
1611                             $consolidate[$class] = array();
1612                             $organisedstyles[$class] = true;
1613                         }
1614                         $consolidate[$class][] = $style;
1615                         unset($advancedstyles[$single]);
1616                     }
1617                 }
1619                 continue;
1620             }
1621             $consolidatetoclass = $style->consolidate_to();
1622             if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) {
1623                 $class = 'css_style_'.$consolidatetoclass;
1624                 if (!array_key_exists($class, $consolidate)) {
1625                     $consolidate[$class] = array();
1626                     $organisedstyles[$class] = true;
1627                 }
1628                 $consolidate[$class][] = $style;
1629             } else {
1630                 $organisedstyles[$style->get_name()] = $style;
1631             }
1632         }
1634         foreach ($consolidate as $class => $styles) {
1635             $organisedstyles[$class] = $class::consolidate($styles);
1636         }
1638         foreach ($organisedstyles as $style) {
1639             if (is_array($style)) {
1640                 foreach ($style as $s) {
1641                     $finalstyles[] = $s;
1642                 }
1643             } else {
1644                 $finalstyles[] = $style;
1645             }
1646         }
1647         $finalstyles = array_merge($finalstyles, $advancedstyles);
1648         return $finalstyles;
1649     }
1651     /**
1652      * Splits this rules into an array of CSS rules. One for each of the selectors
1653      * that make up this rule.
1654      *
1655      * @return array(css_rule)
1656      */
1657     public function split_by_selector() {
1658         $return = array();
1659         foreach ($this->selectors as $selector) {
1660             $return[] = new css_rule($selector, $this->styles);
1661         }
1662         return $return;
1663     }
1665     /**
1666      * Splits this rule into an array of rules. One for each of the styles that
1667      * make up this rule
1668      *
1669      * @return array Array of css_rule objects
1670      */
1671     public function split_by_style() {
1672         $return = array();
1673         foreach ($this->styles as $style) {
1674             if (is_array($style)) {
1675                 $return[] = new css_rule($this->selectors, $style);
1676                 continue;
1677             }
1678             $return[] = new css_rule($this->selectors, array($style));
1679         }
1680         return $return;
1681     }
1683     /**
1684      * Gets a hash for the styles of this rule
1685      *
1686      * @return string
1687      */
1688     public function get_style_hash() {
1689         return md5(css_writer::styles($this->styles));
1690     }
1692     /**
1693      * Gets a hash for the selectors of this rule
1694      *
1695      * @return string
1696      */
1697     public function get_selector_hash() {
1698         return md5(css_writer::selectors($this->selectors));
1699     }
1701     /**
1702      * Gets the number of selectors that make up this rule.
1703      *
1704      * @return int
1705      */
1706     public function get_selector_count() {
1707         $count = 0;
1708         foreach ($this->selectors as $selector) {
1709             $count += $selector->get_selector_count();
1710         }
1711         return $count;
1712     }
1714     /**
1715      * Returns true if there are any errors with this rule.
1716      *
1717      * @return bool
1718      */
1719     public function has_errors() {
1720         foreach ($this->styles as $style) {
1721             if (is_array($style)) {
1722                 foreach ($style as $advstyle) {
1723                     if ($advstyle->has_error()) {
1724                         return true;
1725                     }
1726                 }
1727                 continue;
1728             }
1729             if ($style->has_error()) {
1730                 return true;
1731             }
1732         }
1733         return false;
1734     }
1736     /**
1737      * Returns the error strings that were recorded when processing this rule.
1738      *
1739      * Before calling this function you should first call {@link css_rule::has_errors()}
1740      * to make sure there are errors (hopefully there arn't).
1741      *
1742      * @return string
1743      */
1744     public function get_error_string() {
1745         $css = $this->out();
1746         $errors = array();
1747         foreach ($this->styles as $style) {
1748             if ($style->has_error()) {
1749                 $errors[] = "  * ".$style->get_last_error();
1750             }
1751         }
1752         return $css." has the following errors:\n".join("\n", $errors);
1753     }
1755     /**
1756      * Returns true if this rule could be considered a reset rule.
1757      *
1758      * A reset rule is a rule that acts upon an HTML element and does not include any other parts to its selector.
1759      *
1760      * @return bool
1761      */
1762     public function is_reset_rule() {
1763         foreach ($this->selectors as $selector) {
1764             if (!$selector->is_basic()) {
1765                 return false;
1766             }
1767         }
1768         return true;
1769     }
1772 /**
1773  * An abstract CSS rule collection class.
1774  *
1775  * This class is extended by things such as media and keyframe declaration. They are declarations that
1776  * group rules together for a purpose.
1777  * When no declaration is specified rules accumulate into @media all.
1778  *
1779  * @package core
1780  * @category css
1781  * @copyright 2012 Sam Hemelryk
1782  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1783  */
1784 abstract class css_rule_collection {
1785     /**
1786      * An array of rules within this collection instance
1787      * @var array
1788      */
1789     protected $rules = array();
1791     /**
1792      * The collection must be able to print itself.
1793      */
1794     abstract public function out();
1796     /**
1797      * Adds a new CSS rule to this collection instance
1798      *
1799      * @param css_rule $newrule
1800      */
1801     public function add_rule(css_rule $newrule) {
1802         foreach ($newrule->split_by_selector() as $rule) {
1803             $hash = $rule->get_selector_hash();
1804             if (!array_key_exists($hash, $this->rules)) {
1805                 $this->rules[$hash] = $rule;
1806             } else {
1807                 $this->rules[$hash]->add_styles($rule->get_styles());
1808             }
1809         }
1810     }
1812     /**
1813      * Returns the rules used by this collection
1814      *
1815      * @return array
1816      */
1817     public function get_rules() {
1818         return $this->rules;
1819     }
1821     /**
1822      * Organises rules by gropuing selectors based upon the styles and consolidating
1823      * those selectors into single rules.
1824      *
1825      * @return bool True if the CSS was optimised by this method
1826      */
1827     public function organise_rules_by_selectors() {
1828         $optimised = array();
1829         $beforecount = count($this->rules);
1830         $lasthash = null;
1831         $lastrule = null;
1832         foreach ($this->rules as $rule) {
1833             $hash = $rule->get_style_hash();
1834             if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) {
1835                 foreach ($rule->get_selectors() as $selector) {
1836                     $lastrule->add_selector($selector);
1837                 }
1838                 continue;
1839             }
1840             $lastrule = clone($rule);
1841             $lasthash = $hash;
1842             $optimised[] = $lastrule;
1843         }
1844         $this->rules = array();
1845         foreach ($optimised as $optimised) {
1846             $this->rules[$optimised->get_selector_hash()] = $optimised;
1847         }
1848         $aftercount = count($this->rules);
1849         return ($beforecount < $aftercount);
1850     }
1852     /**
1853      * Returns the total number of rules that exist within this collection
1854      *
1855      * @return int
1856      */
1857     public function count_rules() {
1858         return count($this->rules);
1859     }
1861     /**
1862      * Returns the total number of selectors that exist within this collection
1863      *
1864      * @return int
1865      */
1866     public function count_selectors() {
1867         $count = 0;
1868         foreach ($this->rules as $rule) {
1869             $count += $rule->get_selector_count();
1870         }
1871         return $count;
1872     }
1874     /**
1875      * Returns true if the collection has any rules that have errors
1876      *
1877      * @return boolean
1878      */
1879     public function has_errors() {
1880         foreach ($this->rules as $rule) {
1881             if ($rule->has_errors()) {
1882                 return true;
1883             }
1884         }
1885         return false;
1886     }
1888     /**
1889      * Returns any errors that have happened within rules in this collection.
1890      *
1891      * @return string
1892      */
1893     public function get_errors() {
1894         $errors = array();
1895         foreach ($this->rules as $rule) {
1896             if ($rule->has_errors()) {
1897                 $errors[] = $rule->get_error_string();
1898             }
1899         }
1900         return $errors;
1901     }
1904 /**
1905  * A media class to organise rules by the media they apply to.
1906  *
1907  * @package core
1908  * @category css
1909  * @copyright 2012 Sam Hemelryk
1910  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1911  */
1912 class css_media extends css_rule_collection {
1914     /**
1915      * An array of the different media types this instance applies to.
1916      * @var array
1917      */
1918     protected $types = array();
1920     /**
1921      * Initalises a new media instance
1922      *
1923      * @param string $for The media that the contained rules are destined for.
1924      */
1925     public function __construct($for = 'all') {
1926         $types = explode(',', $for);
1927         $this->types = array_map('trim', $types);
1928     }
1930     /**
1931      * Returns the CSS for this media and all of its rules.
1932      *
1933      * @return string
1934      */
1935     public function out() {
1936         return css_writer::media(join(',', $this->types), $this->rules);
1937     }
1939     /**
1940      * Returns an array of media that this media instance applies to
1941      *
1942      * @return array
1943      */
1944     public function get_types() {
1945         return $this->types;
1946     }
1948     /**
1949      * Returns all of the reset rules known by this media set.
1950      * @param bool $remove If set to true reset rules will be removed before being returned.
1951      * @return array
1952      */
1953     public function get_reset_rules($remove = false) {
1954         $resetrules = array();
1955         foreach ($this->rules as $key => $rule) {
1956             if ($rule->is_reset_rule()) {
1957                 $resetrules[] = clone $rule;
1958                 if ($remove) {
1959                     unset($this->rules[$key]);
1960                 }
1961             }
1962         }
1963         return $resetrules;
1964     }
1967 /**
1968  * A media class to organise rules by the media they apply to.
1969  *
1970  * @package core
1971  * @category css
1972  * @copyright 2012 Sam Hemelryk
1973  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1974  */
1975 class css_keyframe extends css_rule_collection {
1977     /** @var string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes  */
1978     protected $for;
1980     /** @var string $name The name for the keyframes */
1981     protected $name;
1982     /**
1983      * Constructs a new keyframe
1984      *
1985      * @param string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
1986      * @param string $name The name for the keyframes
1987      */
1988     public function __construct($for, $name) {
1989         $this->for = $for;
1990         $this->name = $name;
1991     }
1992     /**
1993      * Returns the directive of this keyframe
1994      *
1995      * e.g. keyframes, -moz-keyframes, -webkit-keyframes
1996      * @return string
1997      */
1998     public function get_for() {
1999         return $this->for;
2000     }
2001     /**
2002      * Returns the name of this keyframe
2003      * @return string
2004      */
2005     public function get_name() {
2006         return $this->name;
2007     }
2008     /**
2009      * Returns the CSS for this collection of keyframes and all of its rules.
2010      *
2011      * @return string
2012      */
2013     public function out() {
2014         return css_writer::keyframe($this->for, $this->name, $this->rules);
2015     }
2018 /**
2019  * An absract class to represent CSS styles
2020  *
2021  * @package core
2022  * @category css
2023  * @copyright 2012 Sam Hemelryk
2024  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2025  */
2026 abstract class css_style {
2028     /** Constant used for recongise a special empty value in a CSS style */
2029     const NULL_VALUE = '@@$NULL$@@';
2031     /**
2032      * The name of the style
2033      * @var string
2034      */
2035     protected $name;
2037     /**
2038      * The value for the style
2039      * @var mixed
2040      */
2041     protected $value;
2043     /**
2044      * If set to true this style was defined with the !important rule.
2045      * Only trolls use !important.
2046      * Don't hide under bridges.. its not good for your skin. Do the proper thing
2047      * and fix the issue don't just force a fix that will undoubtedly one day
2048      * lead to further frustration.
2049      * @var bool
2050      */
2051     protected $important = false;
2053     /**
2054      * Gets set to true if this style has an error
2055      * @var bool
2056      */
2057     protected $error = false;
2059     /**
2060      * The last error message that occured
2061      * @var string
2062      */
2063     protected $errormessage = null;
2065     /**
2066      * Initialises a new style.
2067      *
2068      * This is the only public way to create a style to ensure they that appropriate
2069      * style class is used if it exists.
2070      *
2071      * @param string $name The name of the style.
2072      * @param string $value The value of the style.
2073      * @return css_style_generic
2074      */
2075     public static function init_automatic($name, $value) {
2076         $specificclass = 'css_style_'.preg_replace('#[^a-zA-Z0-9]+#', '', $name);
2077         if (class_exists($specificclass)) {
2078             return $specificclass::init($value);
2079         }
2080         return new css_style_generic($name, $value);
2081     }
2083     /**
2084      * Creates a new style when given its name and value
2085      *
2086      * @param string $name The name of the style.
2087      * @param string $value The value of the style.
2088      */
2089     protected function __construct($name, $value) {
2090         $this->name = $name;
2091         $this->set_value($value);
2092     }
2094     /**
2095      * Sets the value for the style
2096      *
2097      * @param string $value
2098      */
2099     final public function set_value($value) {
2100         $value = trim($value);
2101         $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
2102         if ($important) {
2103             $value = substr($value, 0, -(strlen($matches[1])));
2104             $value = rtrim($value);
2105         }
2106         if (!$this->important || $important) {
2107             $this->value = $this->clean_value($value);
2108             $this->important = $important;
2109         }
2110         if (!$this->is_valid()) {
2111             $this->set_error('Invalid value for '.$this->name);
2112         }
2113     }
2115     /**
2116      * Returns true if the value associated with this style is valid
2117      *
2118      * @return bool
2119      */
2120     public function is_valid() {
2121         return true;
2122     }
2124     /**
2125      * Returns the name for the style
2126      *
2127      * @return string
2128      */
2129     public function get_name() {
2130         return $this->name;
2131     }
2133     /**
2134      * Returns the value for the style
2135      *
2136      * @param bool $includeimportant If set to true and the rule is important !important postfix will be used.
2137      * @return string
2138      */
2139     public function get_value($includeimportant = true) {
2140         $value = $this->value;
2141         if ($includeimportant && $this->important) {
2142             $value .= ' !important';
2143         }
2144         return $value;
2145     }
2147     /**
2148      * Returns the style ready for use in CSS
2149      *
2150      * @param string|null $value A value to use to override the value for this style.
2151      * @return string
2152      */
2153     public function out($value = null) {
2154         if (is_null($value)) {
2155             $value = $this->get_value();
2156         }
2157         return css_writer::style($this->name, $value, $this->important);
2158     }
2160     /**
2161      * This can be overridden by a specific style allowing it to clean its values
2162      * consistently.
2163      *
2164      * @param mixed $value
2165      * @return mixed
2166      */
2167     protected function clean_value($value) {
2168         return $value;
2169     }
2171     /**
2172      * If this particular style can be consolidated into another style this function
2173      * should return the style that it can be consolidated into.
2174      *
2175      * @return string|null
2176      */
2177     public function consolidate_to() {
2178         return null;
2179     }
2181     /**
2182      * Sets the last error message.
2183      *
2184      * @param string $message
2185      */
2186     protected function set_error($message) {
2187         $this->error = true;
2188         $this->errormessage = $message;
2189     }
2191     /**
2192      * Returns true if an error has occured
2193      *
2194      * @return bool
2195      */
2196     public function has_error() {
2197         return $this->error;
2198     }
2200     /**
2201      * Returns the last error that occured or null if no errors have happened.
2202      *
2203      * @return string
2204      */
2205     public function get_last_error() {
2206         return $this->errormessage;
2207     }
2209     /**
2210      * Returns true if the value for this style is the special null value.
2211      *
2212      * This should only be overriden in circumstances where a shorthand style can lead
2213      * to move explicit styles being overwritten. Not a common place occurenace.
2214      *
2215      * Example:
2216      *   This occurs if the shorthand background property was used but no proper value
2217      *   was specified for this style.
2218      *   This leads to a null value being used unless otherwise overridden.
2219      *
2220      * @return bool
2221      */
2222     public function is_special_empty_value() {
2223         return false;
2224     }
2226     /**
2227      * Returns true if this style permits multiple values.
2228      *
2229      * This occurs for styles such as background image that can have browser specific values that need to be maintained because
2230      * of course we don't know what browser the user is using, and optimisation occurs before caching.
2231      * Thus we must always server all values we encounter in the order we encounter them for when this is set to true.
2232      *
2233      * @return boolean False by default, true if the style supports muliple values.
2234      */
2235     public function allows_multiple_values() {
2236         return false;
2237     }
2239     /**
2240      * Returns true if this style was marked important.
2241      * @return bool
2242      */
2243     public function is_important() {
2244         return !empty($this->important);
2245     }
2247     /**
2248      * Sets the important flag for this style and its current value.
2249      * @param bool $important
2250      */
2251     public function set_important($important = true) {
2252         $this->important = (bool) $important;
2253     }
2256 /**
2257  * A generic CSS style class to use when a more specific class does not exist.
2258  *
2259  * @package core
2260  * @category css
2261  * @copyright 2012 Sam Hemelryk
2262  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2263  */
2264 class css_style_generic extends css_style {
2266     /**
2267      * Cleans incoming values for typical things that can be optimised.
2268      *
2269      * @param mixed $value Cleans the provided value optimising it if possible
2270      * @return string
2271      */
2272     protected function clean_value($value) {
2273         if (trim($value) == '0px') {
2274             $value = 0;
2275         } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
2276             $value = '#'.strtoupper($matches[1]);
2277         }
2278         return $value;
2279     }
2282 /**
2283  * A colour CSS style
2284  *
2285  * @package core
2286  * @category css
2287  * @copyright 2012 Sam Hemelryk
2288  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2289  */
2290 class css_style_color extends css_style {
2292     /**
2293      * Creates a new colour style
2294      *
2295      * @param mixed $value Initialises a new colour style
2296      * @return css_style_color
2297      */
2298     public static function init($value) {
2299         return new css_style_color('color', $value);
2300     }
2302     /**
2303      * Cleans the colour unifing it to a 6 char hash colour if possible
2304      * Doing this allows us to associate identical colours being specified in
2305      * different ways. e.g. Red, red, #F00, and #F00000
2306      *
2307      * @param mixed $value Cleans the provided value optimising it if possible
2308      * @return string
2309      */
2310     protected function clean_value($value) {
2311         $value = trim($value);
2312         if (css_is_colour($value)) {
2313             if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
2314                 $value = '#'.strtoupper($matches[1]);
2315             } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
2316                 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
2317                 $value = '#'.strtoupper($value);
2318             } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
2319                 $value = css_optimiser::$htmlcolours[strtolower($value)];
2320             }
2321         }
2322         return $value;
2323     }
2325     /**
2326      * Returns the colour style for use within CSS.
2327      * Will return an optimised hash colour.
2328      *
2329      * e.g #123456
2330      *     #123 instead of #112233
2331      *     #F00 instead of red
2332      *
2333      * @param string $overridevalue If provided then this value will be used instead
2334      *     of the styles current value.
2335      * @return string
2336      */
2337     public function out($overridevalue = null) {
2338         if ($overridevalue === null) {
2339             $overridevalue = $this->value;
2340         }
2341         return parent::out(self::shrink_value($overridevalue));
2342     }
2344     /**
2345      * Shrinks the colour value is possible.
2346      *
2347      * @param string $value Shrinks the current value to an optimial form if possible
2348      * @return string
2349      */
2350     public static function shrink_value($value) {
2351         if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) {
2352             return '#'.$matches[1].$matches[2].$matches[3];
2353         }
2354         return $value;
2355     }
2357     /**
2358      * Returns true if the value is a valid colour.
2359      *
2360      * @return bool
2361      */
2362     public function is_valid() {
2363         return css_is_colour($this->value);
2364     }
2367 /**
2368  * A width style
2369  *
2370  * @package core
2371  * @category css
2372  * @copyright 2012 Sam Hemelryk
2373  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2374  */
2375 class css_style_width extends css_style {
2377     /**
2378      * Checks if the width is valid
2379      * @return bool
2380      */
2381     public function is_valid() {
2382         return css_is_width($this->value);
2383     }
2385     /**
2386      * Cleans the provided value
2387      *
2388      * @param mixed $value Cleans the provided value optimising it if possible
2389      * @return string
2390      */
2391     protected function clean_value($value) {
2392         if (!css_is_width($value)) {
2393             // Note we don't actually change the value to something valid. That
2394             // would be bad for futureproofing.
2395             $this->set_error('Invalid width specified for '.$this->name);
2396         } else if (preg_match('#^0\D+$#', $value)) {
2397             $value = 0;
2398         }
2399         return trim($value);
2400     }
2402     /**
2403      * Initialises a new width style
2404      *
2405      * @param mixed $value The value this style has
2406      * @return css_style_width
2407      */
2408     public static function init($value) {
2409         return new css_style_width('width', $value);
2410     }
2413 /**
2414  * A margin style
2415  *
2416  * @package core
2417  * @category css
2418  * @copyright 2012 Sam Hemelryk
2419  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2420  */
2421 class css_style_margin extends css_style_width {
2423     /**
2424      * Initialises a margin style.
2425      *
2426      * In this case we split the margin into several other margin styles so that
2427      * we can properly condense overrides and then reconsolidate them later into
2428      * an optimal form.
2429      *
2430      * @param string $value The value the style has
2431      * @return array An array of margin values that can later be consolidated
2432      */
2433     public static function init($value) {
2434         $important = '';
2435         if (strpos($value, '!important') !== false) {
2436             $important = ' !important';
2437             $value = str_replace('!important', '', $value);
2438         }
2440         $value = preg_replace('#\s+#', ' ', trim($value));
2441         $bits = explode(' ', $value, 4);
2443         $top = $right = $bottom = $left = null;
2444         if (count($bits) > 0) {
2445             $top = $right = $bottom = $left = array_shift($bits);
2446         }
2447         if (count($bits) > 0) {
2448             $right = $left = array_shift($bits);
2449         }
2450         if (count($bits) > 0) {
2451             $bottom = array_shift($bits);
2452         }
2453         if (count($bits) > 0) {
2454             $left = array_shift($bits);
2455         }
2456         return array(
2457             new css_style_margintop('margin-top', $top.$important),
2458             new css_style_marginright('margin-right', $right.$important),
2459             new css_style_marginbottom('margin-bottom', $bottom.$important),
2460             new css_style_marginleft('margin-left', $left.$important)
2461         );
2462     }
2464     /**
2465      * Consolidates individual margin styles into a single margin style
2466      *
2467      * @param array $styles
2468      * @return array An array of consolidated styles
2469      */
2470     public static function consolidate(array $styles) {
2471         if (count($styles) != 4) {
2472             return $styles;
2473         }
2475         $someimportant = false;
2476         $allimportant = null;
2477         $notimportantequal = null;
2478         $firstvalue = null;
2479         foreach ($styles as $style) {
2480             if ($style->is_important()) {
2481                 $someimportant = true;
2482                 if ($allimportant === null) {
2483                     $allimportant = true;
2484                 }
2485             } else {
2486                 if ($allimportant === true) {
2487                     $allimportant = false;
2488                 }
2489                 if ($firstvalue == null) {
2490                     $firstvalue = $style->get_value(false);
2491                     $notimportantequal = true;
2492                 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
2493                     $notimportantequal = false;
2494                 }
2495             }
2496         }
2498         if ($someimportant && !$allimportant && !$notimportantequal) {
2499             return $styles;
2500         }
2502         if ($someimportant && !$allimportant && $notimportantequal) {
2503             $return = array(
2504                 new css_style_margin('margin', $firstvalue)
2505             );
2506             foreach ($styles as $style) {
2507                 if ($style->is_important()) {
2508                     $return[] = $style;
2509                 }
2510             }
2511             return $return;
2512         } else {
2513             $top = null;
2514             $right = null;
2515             $bottom = null;
2516             $left = null;
2517             foreach ($styles as $style) {
2518                 switch ($style->get_name()) {
2519                     case 'margin-top' :
2520                         $top = $style->get_value(false);
2521                         break;
2522                     case 'margin-right' :
2523                         $right = $style->get_value(false);
2524                         break;
2525                     case 'margin-bottom' :
2526                         $bottom = $style->get_value(false);
2527                         break;
2528                     case 'margin-left' :
2529                         $left = $style->get_value(false);
2530                         break;
2531                 }
2532             }
2533             if ($top == $bottom && $left == $right) {
2534                 if ($top == $left) {
2535                     $returnstyle = new css_style_margin('margin', $top);
2536                 } else {
2537                     $returnstyle = new css_style_margin('margin', "{$top} {$left}");
2538                 }
2539             } else if ($left == $right) {
2540                 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom}");
2541             } else {
2542                 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}");
2543             }
2544             if ($allimportant) {
2545                 $returnstyle->set_important();
2546             }
2547             return array($returnstyle);
2548         }
2549     }
2552 /**
2553  * A margin top style
2554  *
2555  * @package core
2556  * @category css
2557  * @copyright 2012 Sam Hemelryk
2558  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2559  */
2560 class css_style_margintop extends css_style_margin {
2562     /**
2563      * A simple init, just a single style
2564      *
2565      * @param string $value The value the style has
2566      * @return css_style_margintop
2567      */
2568     public static function init($value) {
2569         return new css_style_margintop('margin-top', $value);
2570     }
2572     /**
2573      * This style can be consolidated into a single margin style
2574      *
2575      * @return string
2576      */
2577     public function consolidate_to() {
2578         return 'margin';
2579     }
2582 /**
2583  * A margin right style
2584  *
2585  * @package core
2586  * @category css
2587  * @copyright 2012 Sam Hemelryk
2588  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2589  */
2590 class css_style_marginright extends css_style_margin {
2592     /**
2593      * A simple init, just a single style
2594      *
2595      * @param string $value The value the style has
2596      * @return css_style_margintop
2597      */
2598     public static function init($value) {
2599         return new css_style_marginright('margin-right', $value);
2600     }
2602     /**
2603      * This style can be consolidated into a single margin style
2604      *
2605      * @return string
2606      */
2607     public function consolidate_to() {
2608         return 'margin';
2609     }
2612 /**
2613  * A margin bottom style
2614  *
2615  * @package core
2616  * @category css
2617  * @copyright 2012 Sam Hemelryk
2618  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2619  */
2620 class css_style_marginbottom extends css_style_margin {
2622     /**
2623      * A simple init, just a single style
2624      *
2625      * @param string $value The value the style has
2626      * @return css_style_margintop
2627      */
2628     public static function init($value) {
2629         return new css_style_marginbottom('margin-bottom', $value);
2630     }
2632     /**
2633      * This style can be consolidated into a single margin style
2634      *
2635      * @return string
2636      */
2637     public function consolidate_to() {
2638         return 'margin';
2639     }
2642 /**
2643  * A margin left style
2644  *
2645  * @package core
2646  * @category css
2647  * @copyright 2012 Sam Hemelryk
2648  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2649  */
2650 class css_style_marginleft extends css_style_margin {
2652     /**
2653      * A simple init, just a single style
2654      *
2655      * @param string $value The value the style has
2656      * @return css_style_margintop
2657      */
2658     public static function init($value) {
2659         return new css_style_marginleft('margin-left', $value);
2660     }
2662     /**
2663      * This style can be consolidated into a single margin style
2664      *
2665      * @return string
2666      */
2667     public function consolidate_to() {
2668         return 'margin';
2669     }
2672 /**
2673  * A border style
2674  *
2675  * @package core
2676  * @category css
2677  * @copyright 2012 Sam Hemelryk
2678  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2679  */
2680 class css_style_border extends css_style {
2682     /**
2683      * Initalises the border style into an array of individual style compontents
2684      *
2685      * @param string $value The value the style has
2686      * @return css_style_bordercolor
2687      */
2688     public static function init($value) {
2689         $value = preg_replace('#\s+#', ' ', $value);
2690         $bits = explode(' ', $value, 3);
2692         $return = array();
2693         if (count($bits) > 0) {
2694             $width = array_shift($bits);
2695             if (!css_style_borderwidth::is_border_width($width)) {
2696                 $width = '0';
2697             }
2698             $return[] = new css_style_borderwidth('border-top-width', $width);
2699             $return[] = new css_style_borderwidth('border-right-width', $width);
2700             $return[] = new css_style_borderwidth('border-bottom-width', $width);
2701             $return[] = new css_style_borderwidth('border-left-width', $width);
2702         }
2703         if (count($bits) > 0) {
2704             $style = array_shift($bits);
2705             $return[] = new css_style_borderstyle('border-top-style', $style);
2706             $return[] = new css_style_borderstyle('border-right-style', $style);
2707             $return[] = new css_style_borderstyle('border-bottom-style', $style);
2708             $return[] = new css_style_borderstyle('border-left-style', $style);
2709         }
2710         if (count($bits) > 0) {
2711             $colour = array_shift($bits);
2712             $return[] = new css_style_bordercolor('border-top-color', $colour);
2713             $return[] = new css_style_bordercolor('border-right-color', $colour);
2714             $return[] = new css_style_bordercolor('border-bottom-color', $colour);
2715             $return[] = new css_style_bordercolor('border-left-color', $colour);
2716         }
2717         return $return;
2718     }
2720     /**
2721      * Consolidates all border styles into a single style
2722      *
2723      * @param array $styles An array of border styles
2724      * @return array An optimised array of border styles
2725      */
2726     public static function consolidate(array $styles) {
2728         $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2729         $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2730         $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2732         foreach ($styles as $style) {
2733             switch ($style->get_name()) {
2734                 case 'border-top-width':
2735                     $borderwidths['top'] = $style->get_value();
2736                     break;
2737                 case 'border-right-width':
2738                     $borderwidths['right'] = $style->get_value();
2739                     break;
2740                 case 'border-bottom-width':
2741                     $borderwidths['bottom'] = $style->get_value();
2742                     break;
2743                 case 'border-left-width':
2744                     $borderwidths['left'] = $style->get_value();
2745                     break;
2747                 case 'border-top-style':
2748                     $borderstyles['top'] = $style->get_value();
2749                     break;
2750                 case 'border-right-style':
2751                     $borderstyles['right'] = $style->get_value();
2752                     break;
2753                 case 'border-bottom-style':
2754                     $borderstyles['bottom'] = $style->get_value();
2755                     break;
2756                 case 'border-left-style':
2757                     $borderstyles['left'] = $style->get_value();
2758                     break;
2760                 case 'border-top-color':
2761                     $bordercolors['top'] = css_style_color::shrink_value($style->get_value());
2762                     break;
2763                 case 'border-right-color':
2764                     $bordercolors['right'] = css_style_color::shrink_value($style->get_value());
2765                     break;
2766                 case 'border-bottom-color':
2767                     $bordercolors['bottom'] = css_style_color::shrink_value($style->get_value());
2768                     break;
2769                 case 'border-left-color':
2770                     $bordercolors['left'] = css_style_color::shrink_value($style->get_value());
2771                     break;
2772             }
2773         }
2775         $uniquewidths = count(array_unique($borderwidths));
2776         $uniquestyles = count(array_unique($borderstyles));
2777         $uniquecolors = count(array_unique($bordercolors));
2779         $nullwidths = in_array(null, $borderwidths, true);
2780         $nullstyles = in_array(null, $borderstyles, true);
2781         $nullcolors = in_array(null, $bordercolors, true);
2783         $allwidthsthesame = ($uniquewidths === 1)?1:0;
2784         $allstylesthesame = ($uniquestyles === 1)?1:0;
2785         $allcolorsthesame = ($uniquecolors === 1)?1:0;
2787         $allwidthsnull = $allwidthsthesame && $nullwidths;
2788         $allstylesnull = $allstylesthesame && $nullstyles;
2789         $allcolorsnull = $allcolorsthesame && $nullcolors;
2791         $return = array();
2792         if ($allwidthsnull && $allstylesnull && $allcolorsnull) {
2793             // Everything is null still... boo
2794             return array(new css_style_border('border', ''));
2796         } else if ($allwidthsnull && $allstylesnull) {
2798             self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2799             return $return;
2801         } else if ($allwidthsnull && $allcolorsnull) {
2803             self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2804             return $return;
2806         } else if ($allcolorsnull && $allstylesnull) {
2808             self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2809             return $return;
2811         }
2813         if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 3) {
2815             $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']);
2817         } else if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 2) {
2819             if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) {
2821                 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']);
2822                 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2824             } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) {
2826                 $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']);
2827                 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2829             } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) {
2831                 $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']);
2832                 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2834             } else {
2835                 self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2836                 self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2837                 self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2838             }
2840         } else if (!$nullwidths && !$nullcolors && !$nullstyles && max(array_count_values($borderwidths)) == 3 && max(array_count_values($borderstyles)) == 3 && max(array_count_values($bordercolors)) == 3) {
2841             $widthkeys = array();
2842             $stylekeys = array();
2843             $colorkeys = array();
2845             foreach ($borderwidths as $key => $value) {
2846                 if (!array_key_exists($value, $widthkeys)) {
2847                     $widthkeys[$value] = array();
2848                 }
2849                 $widthkeys[$value][] = $key;
2850             }
2851             usort($widthkeys, 'css_sort_by_count');
2852             $widthkeys = array_values($widthkeys);
2854             foreach ($borderstyles as $key => $value) {
2855                 if (!array_key_exists($value, $stylekeys)) {
2856                     $stylekeys[$value] = array();
2857                 }
2858                 $stylekeys[$value][] = $key;
2859             }
2860             usort($stylekeys, 'css_sort_by_count');
2861             $stylekeys = array_values($stylekeys);
2863             foreach ($bordercolors as $key => $value) {
2864                 if (!array_key_exists($value, $colorkeys)) {
2865                     $colorkeys[$value] = array();
2866                 }
2867                 $colorkeys[$value][] = $key;
2868             }
2869             usort($colorkeys, 'css_sort_by_count');
2870             $colorkeys = array_values($colorkeys);
2872             if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) {
2873                 $key = $widthkeys[0][0];
2874                 self::build_style_string($return, 'css_style_border', 'border',  $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2875                 $key = $widthkeys[1][0];
2876                 self::build_style_string($return, 'css_style_border'.$key, 'border-'.$key,  $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2877             } else {
2878                 self::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2879                 self::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2880                 self::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2881                 self::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
2882             }
2883         } else {
2884             self::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2885             self::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2886             self::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2887             self::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
2888         }
2889         foreach ($return as $key => $style) {
2890             if ($style->get_value() == '') {
2891                 unset($return[$key]);
2892             }
2893         }
2894         return $return;
2895     }
2897     /**
2898      * Border styles get consolidated to a single border style.
2899      *
2900      * @return string
2901      */
2902     public function consolidate_to() {
2903         return 'border';
2904     }
2906     /**
2907      * Consolidates a series of border styles into an optimised array of border
2908      * styles by looking at the direction of the border and prioritising that
2909      * during the optimisation.
2910      *
2911      * @param array $array An array to add styles into during consolidation. Passed by reference.
2912      * @param string $class The class type to initalise
2913      * @param string $style The style to create
2914      * @param string|array $top The top value
2915      * @param string $right The right value
2916      * @param string $bottom The bottom value
2917      * @param string $left The left value
2918      * @return bool
2919      */
2920     public static function consolidate_styles_by_direction(&$array, $class, $style, $top, $right = null, $bottom = null, $left = null) {
2921         if (is_array($top)) {
2922             $right = $top['right'];
2923             $bottom = $top['bottom'];
2924             $left = $top['left'];
2925             $top = $top['top'];
2926         }
2928         if ($top == $bottom && $left == $right && $top == $left) {
2929             if (is_null($top)) {
2930                 $array[] = new $class($style, '');
2931             } else {
2932                 $array[] =  new $class($style, $top);
2933             }
2934         } else if ($top == null || $right == null || $bottom == null || $left == null) {
2935             if ($top !== null) {
2936                 $array[] = new $class(str_replace('border-', 'border-top-', $style), $top);
2937             }
2938             if ($right !== null) {
2939                 $array[] = new $class(str_replace('border-', 'border-right-', $style), $right);
2940             }
2941             if ($bottom !== null) {
2942                 $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom);
2943             }
2944             if ($left !== null) {
2945                 $array[] = new $class(str_replace('border-', 'border-left-', $style), $left);
2946             }
2947         } else if ($top == $bottom && $left == $right) {
2948             $array[] = new $class($style, $top.' '.$right);
2949         } else if ($left == $right) {
2950             $array[] = new $class($style, $top.' '.$right.' '.$bottom);
2951         } else {
2952             $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left);
2953         }
2954         return true;
2955     }
2957     /**
2958      * Builds a border style for a set of width, style, and colour values
2959      *
2960      * @param array $array An array into which the generated style is added
2961      * @param string $class The class type to initialise
2962      * @param string $cssstyle The style to use
2963      * @param string $width The width of the border
2964      * @param string $style The style of the border
2965      * @param string $color The colour of the border
2966      * @return bool
2967      */
2968     public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) {
2969         if (!is_null($width) && !is_null($style) && !is_null($color)) {
2970             $array[] = new $class($cssstyle, $width.' '.$style.' '.$color);
2971         } else if (!is_null($width) && !is_null($style) && is_null($color)) {
2972             $array[] = new $class($cssstyle, $width.' '.$style);
2973         } else if (!is_null($width) && is_null($style) && is_null($color)) {
2974             $array[] = new $class($cssstyle, $width);
2975         } else {
2976             if (!is_null($width)) {
2977                 $array[] = new $class($cssstyle, $width);
2978             }
2979             if (!is_null($style)) {
2980                 $array[] = new $class($cssstyle, $style);
2981             }
2982             if (!is_null($color)) {
2983                 $array[] = new $class($cssstyle, $color);
2984             }
2985         }
2986         return true;
2987     }
2990 /**
2991  * A border colour style
2992  *
2993  * @package core
2994  * @category css
2995  * @copyright 2012 Sam Hemelryk
2996  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2997  */
2998 class css_style_bordercolor extends css_style_color {
3000     /**
3001      * Creates a new border colour style
3002      *
3003      * Based upon the colour style
3004      *
3005      * @param mixed $value
3006      * @return Array of css_style_bordercolor
3007      */
3008     public static function init($value) {
3009         $value = preg_replace('#\s+#', ' ', $value);
3010         $bits = explode(' ', $value, 4);
3012         $top = $right = $bottom = $left = null;
3013         if (count($bits) > 0) {
3014             $top = $right = $bottom = $left = array_shift($bits);
3015         }
3016         if (count($bits) > 0) {
3017             $right = $left = array_shift($bits);
3018         }
3019         if (count($bits) > 0) {
3020             $bottom = array_shift($bits);
3021         }
3022         if (count($bits) > 0) {
3023             $left = array_shift($bits);
3024         }
3025         return array(
3026             css_style_bordertopcolor::init($top),
3027             css_style_borderrightcolor::init($right),
3028             css_style_borderbottomcolor::init($bottom),
3029             css_style_borderleftcolor::init($left)
3030         );
3031     }
3033     /**
3034      * Consolidate this to a single border style
3035      *
3036      * @return string
3037      */
3038     public function consolidate_to() {
3039         return 'border';
3040     }
3042     /**
3043      * Cleans the value
3044      *
3045      * @param string $value Cleans the provided value optimising it if possible
3046      * @return string
3047      */
3048     protected function clean_value($value) {
3049         $values = explode(' ', $value);
3050         $values = array_map('parent::clean_value', $values);
3051         return join (' ', $values);
3052     }
3054     /**
3055      * Outputs this style
3056      *
3057      * @param string $overridevalue
3058      * @return string
3059      */
3060     public function out($overridevalue = null) {
3061         if ($overridevalue === null) {
3062             $overridevalue = $this->value;
3063         }
3064         $values = explode(' ', $overridevalue);
3065         $values = array_map('css_style_color::shrink_value', $values);
3066         return parent::out(join (' ', $values));
3067     }
3070 /**
3071  * A border left style
3072  *
3073  * @package core
3074  * @category css
3075  * @copyright 2012 Sam Hemelryk
3076  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3077  */
3078 class css_style_borderleft extends css_style_generic {
3080     /**
3081      * Initialises the border left style into individual components
3082      *
3083      * @param string $value
3084      * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor
3085      */
3086     public static function init($value) {
3087         $value = preg_replace('#\s+#', ' ', $value);
3088         $bits = explode(' ', $value, 3);
3090         $return = array();
3091         if (count($bits) > 0) {
3092             $return[] = css_style_borderleftwidth::init(array_shift($bits));
3093         }
3094         if (count($bits) > 0) {
3095             $return[] = css_style_borderleftstyle::init(array_shift($bits));
3096         }
3097         if (count($bits) > 0) {
3098             $return[] = css_style_borderleftcolor::init(array_shift($bits));
3099         }
3100         return $return;
3101     }
3103     /**
3104      * Consolidate this to a single border style
3105      *
3106      * @return string
3107      */
3108     public function consolidate_to() {
3109         return 'border';
3110     }
3113 /**
3114  * A border right style
3115  *
3116  * @package core
3117  * @category css
3118  * @copyright 2012 Sam Hemelryk
3119  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3120  */
3121 class css_style_borderright extends css_style_generic {
3123     /**
3124      * Initialises the border right style into individual components
3125      *
3126      * @param string $value The value of the style
3127      * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor
3128      */
3129     public static function init($value) {
3130         $value = preg_replace('#\s+#', ' ', $value);
3131         $bits = explode(' ', $value, 3);
3133         $return = array();
3134         if (count($bits) > 0) {
3135             $return[] = css_style_borderrightwidth::init(array_shift($bits));
3136         }
3137         if (count($bits) > 0) {
3138             $return[] = css_style_borderrightstyle::init(array_shift($bits));
3139         }
3140         if (count($bits) > 0) {
3141             $return[] = css_style_borderrightcolor::init(array_shift($bits));
3142         }
3143         return $return;
3144     }
3146     /**
3147      * Consolidate this to a single border style
3148      *
3149      * @return string
3150      */
3151     public function consolidate_to() {
3152         return 'border';
3153     }
3156 /**
3157  * A border top style
3158  *
3159  * @package core
3160  * @category css
3161  * @copyright 2012 Sam Hemelryk
3162  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3163  */
3164 class css_style_bordertop extends css_style_generic {
3166     /**
3167      * Initialises the border top style into individual components
3168      *
3169      * @param string $value The value of the style
3170      * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor
3171      */
3172     public static function init($value) {
3173         $value = preg_replace('#\s+#', ' ', $value);
3174         $bits = explode(' ', $value, 3);
3176         $return = array();
3177         if (count($bits) > 0) {
3178             $return[] = css_style_bordertopwidth::init(array_shift($bits));
3179         }
3180         if (count($bits) > 0) {
3181             $return[] = css_style_bordertopstyle::init(array_shift($bits));
3182         }
3183         if (count($bits) > 0) {
3184             $return[] = css_style_bordertopcolor::init(array_shift($bits));
3185         }
3186         return $return;
3187     }
3189     /**
3190      * Consolidate this to a single border style
3191      *
3192      * @return string
3193      */
3194     public function consolidate_to() {
3195         return 'border';
3196     }
3199 /**
3200  * A border bottom style
3201  *
3202  * @package core
3203  * @category css
3204  * @copyright 2012 Sam Hemelryk
3205  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3206  */
3207 class css_style_borderbottom extends css_style_generic {
3209     /**
3210      * Initialises the border bottom style into individual components
3211      *
3212      * @param string $value The value of the style
3213      * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor
3214      */
3215     public static function init($value) {
3216         $value = preg_replace('#\s+#', ' ', $value);
3217         $bits = explode(' ', $value, 3);
3219         $return = array();
3220         if (count($bits) > 0) {
3221             $return[] = css_style_borderbottomwidth::init(array_shift($bits));
3222         }
3223         if (count($bits) > 0) {
3224             $return[] = css_style_borderbottomstyle::init(array_shift($bits));
3225         }
3226         if (count($bits) > 0) {
3227             $return[] = css_style_borderbottomcolor::init(array_shift($bits));
3228         }
3229         return $return;
3230     }
3232     /**
3233      * Consolidate this to a single border style
3234      *
3235      * @return string
3236      */
3237     public function consolidate_to() {
3238         return 'border';
3239     }
3242 /**
3243  * A border width style
3244  *
3245  * @package core
3246  * @category css
3247  * @copyright 2012 Sam Hemelryk
3248  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3249  */
3250 class css_style_borderwidth extends css_style_width {
3252     /**
3253      * Creates a new border colour style
3254      *
3255      * Based upon the colour style
3256      *
3257      * @param string $value The value of the style
3258      * @return array Array of css_style_border*width
3259      */
3260     public static function init($value) {
3261         $value = preg_replace('#\s+#', ' ', $value);
3262         $bits = explode(' ', $value, 4);
3264         $top = $right = $bottom = $left = null;
3265         if (count($bits) > 0) {
3266             $top = $right = $bottom = $left = array_shift($bits);
3267         }
3268         if (count($bits) > 0) {
3269             $right = $left = array_shift($bits);
3270         }
3271         if (count($bits) > 0) {
3272             $bottom = array_shift($bits);
3273         }
3274         if (count($bits) > 0) {
3275             $left = array_shift($bits);
3276         }
3277         return array(
3278             css_style_bordertopwidth::init($top),
3279             css_style_borderrightwidth::init($right),
3280             css_style_borderbottomwidth::init($bottom),
3281             css_style_borderleftwidth::init($left)
3282         );
3283     }
3285     /**
3286      * Consolidate this to a single border style
3287      *
3288      * @return string
3289      */
3290     public function consolidate_to() {
3291         return 'border';
3292     }
3294     /**
3295      * Checks if the width is valid
3296      * @return bool
3297      */
3298     public function is_valid() {
3299         return self::is_border_width($this->value);
3300     }
3302     /**
3303      * Cleans the provided value
3304      *
3305      * @param mixed $value Cleans the provided value optimising it if possible
3306      * @return string
3307      */
3308     protected function clean_value($value) {
3309         $isvalid = self::is_border_width($value);
3310         if (!$isvalid) {
3311             $this->set_error('Invalid width specified for '.$this->name);
3312         } else if (preg_match('#^0\D+$#', $value)) {
3313             return '0';
3314         }
3315         return trim($value);
3316     }
3318     /**
3319      * Returns true if the provided value is a permitted border width
3320      * @param string $value The value to check
3321      * @return bool
3322      */
3323     public static function is_border_width($value) {
3324         $altwidthvalues = array('thin', 'medium', 'thick');
3325         return css_is_width($value) || in_array($value, $altwidthvalues);
3326     }
3329 /**
3330  * A border style style
3331  *
3332  * @package core
3333  * @category css
3334  * @copyright 2012 Sam Hemelryk
3335  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3336  */
3337 class css_style_borderstyle extends css_style_generic {
3339     /**
3340      * Creates a new border colour style
3341      *
3342      * Based upon the colour style
3343      *
3344      * @param string $value The value of the style
3345      * @return array Array of css_style_border*style
3346      */
3347     public static function init($value) {
3348         $value = preg_replace('#\s+#', ' ', $value);
3349         $bits = explode(' ', $value, 4);
3351         $top = $right = $bottom = $left = null;
3352         if (count($bits) > 0) {
3353             $top = $right = $bottom = $left = array_shift($bits);
3354         }
3355         if (count($bits) > 0) {
3356             $right = $left = array_shift($bits);
3357         }
3358         if (count($bits) > 0) {
3359             $bottom = array_shift($bits);
3360         }
3361         if (count($bits) > 0) {
3362             $left = array_shift($bits);
3363         }
3364         return array(
3365             css_style_bordertopstyle::init($top),
3366             css_style_borderrightstyle::init($right),
3367             css_style_borderbottomstyle::init($bottom),
3368             css_style_borderleftstyle::init($left)
3369         );
3370     }
3372     /**
3373      * Consolidate this to a single border style
3374      *
3375      * @return string
3376      */
3377     public function consolidate_to() {
3378         return 'border';
3379     }
3382 /**
3383  * A border top colour style
3384  *
3385  * @package core
3386  * @category css
3387  * @copyright 2012 Sam Hemelryk
3388  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3389  */
3390 class css_style_bordertopcolor extends css_style_bordercolor {
3392     /**
3393      * Initialises this style object
3394      *
3395      * @param string $value The value of the style
3396      * @return css_style_bordertopcolor
3397      */
3398     public static function init($value) {
3399         return new css_style_bordertopcolor('border-top-color', $value);
3400     }
3402     /**
3403      * Consolidate this to a single border style
3404      *
3405      * @return string
3406      */
3407     public function consolidate_to() {
3408         return 'border';
3409     }
3412 /**
3413  * A border left colour style
3414  *
3415  * @package core
3416  * @category css
3417  * @copyright 2012 Sam Hemelryk
3418  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3419  */
3420 class css_style_borderleftcolor extends css_style_bordercolor {
3422     /**
3423      * Initialises this style object
3424      *
3425      * @param string $value The value of the style
3426      * @return css_style_borderleftcolor
3427      */
3428     public static function init($value) {
3429         return new css_style_borderleftcolor('border-left-color', $value);
3430     }
3432     /**
3433      * Consolidate this to a single border style
3434      *
3435      * @return string
3436      */
3437     public function consolidate_to() {
3438         return 'border';
3439     }
3442 /**
3443  * A border right colour style
3444  *
3445  * @package core
3446  * @category css
3447  * @copyright 2012 Sam Hemelryk
3448  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3449  */
3450 class css_style_borderrightcolor extends css_style_bordercolor {
3452     /**
3453      * Initialises this style object
3454      *
3455      * @param string $value The value of the style
3456      * @return css_style_borderrightcolor
3457      */
3458     public static function init($value) {
3459         return new css_style_borderrightcolor('border-right-color', $value);
3460     }
3462     /**
3463      * Consolidate this to a single border style
3464      *
3465      * @return string
3466      */
3467     public function consolidate_to() {
3468         return 'border';
3469     }
3472 /**
3473  * A border bottom colour style
3474  *
3475  * @package core
3476  * @category css
3477  * @copyright 2012 Sam Hemelryk
3478  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3479  */
3480 class css_style_borderbottomcolor extends css_style_bordercolor {
3482     /**
3483      * Initialises this style object
3484      *
3485      * @param string $value The value of the style
3486      * @return css_style_borderbottomcolor
3487      */
3488     public static function init($value) {
3489         return new css_style_borderbottomcolor('border-bottom-color', $value);
3490     }
3492     /**
3493      * Consolidate this to a single border style
3494      *
3495      * @return string
3496      */
3497     public function consolidate_to() {
3498         return 'border';
3499     }
3502 /**
3503  * A border width top style
3504  *
3505  * @package core
3506  * @category css
3507  * @copyright 2012 Sam Hemelryk
3508  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3509  */
3510 class css_style_bordertopwidth extends css_style_borderwidth {
3512     /**
3513      * Initialises this style object
3514      *
3515      * @param string $value The value of the style
3516      * @return css_style_bordertopwidth
3517      */
3518     public static function init($value) {
3519         return new css_style_bordertopwidth('border-top-width', $value);
3520     }
3522     /**
3523      * Consolidate this to a single border style
3524      *
3525      * @return string
3526      */
3527     public function consolidate_to() {
3528         return 'border';
3529     }
3532 /**
3533  * A border width left style
3534  *
3535  * @package core
3536  * @category css
3537  * @copyright 2012 Sam Hemelryk
3538  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3539  */
3540 class css_style_borderleftwidth extends css_style_borderwidth {
3542     /**
3543      * Initialises this style object
3544      *
3545      * @param string $value The value of the style
3546      * @return css_style_borderleftwidth
3547      */
3548     public static function init($value) {
3549         return new css_style_borderleftwidth('border-left-width', $value);
3550     }
3552     /**
3553      * Consolidate this to a single border style
3554      *
3555      * @return string
3556      */
3557     public function consolidate_to() {
3558         return 'border';
3559     }
3562 /**
3563  * A border width right style
3564  *
3565  * @package core
3566  * @category css
3567  * @copyright 2012 Sam Hemelryk
3568  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3569  */
3570 class css_style_borderrightwidth extends css_style_borderwidth {
3572     /**
3573      * Initialises this style object
3574      *
3575      * @param string $value The value of the style
3576      * @return css_style_borderrightwidth
3577      */
3578     public static function init($value) {
3579         return new css_style_borderrightwidth('border-right-width', $value);
3580     }
3582     /**
3583      * Consolidate this to a single border style
3584      *
3585      * @return string
3586      */
3587     public function consolidate_to() {
3588         return 'border';
3589     }
3592 /**
3593  * A border width bottom style
3594  *
3595  * @package core
3596  * @category css
3597  * @copyright 2012 Sam Hemelryk
3598  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3599  */
3600 class css_style_borderbottomwidth extends css_style_borderwidth {
3602     /**
3603      * Initialises this style object
3604      *
3605      * @param string $value The value of the style
3606      * @return css_style_borderbottomwidth
3607      */
3608     public static function init($value) {
3609         return new css_style_borderbottomwidth('border-bottom-width', $value);
3610     }
3612     /**
3613      * Consolidate this to a single border style
3614      *
3615      * @return string
3616      */
3617     public function consolidate_to() {
3618         return 'border';
3619     }
3622 /**
3623  * A border top style
3624  *
3625  * @package core
3626  * @category css
3627  * @copyright 2012 Sam Hemelryk
3628  * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3629  */
3630 class css_style_bordertopstyle extends css_style_borderstyle {
3632     /**
3633      * Initialises this style object
3634      *
3635      * @param string $value The value of the style
3636      * @return css_style_bordertopstyle
3637      */
3638     public static function init($value) {
3639         return new css_style_bordertopstyle('border-top-style', $value);
3640  &nb