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