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