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