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