MDL-29941 csslib: A CSS optimiser has been added to process our mountain of CSS
[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/**
18 * This file contains CSS related methods and a CSS optimiser
19 *
20 * @package moodlecore
21 * @copyright 2011 Sam Hemelryk
22 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
23 */
24
25/**
26 * Stores CSS in a file at the given path.
27 *
28 * @param theme_config $theme
29 * @param string $csspath
30 * @param array $cssfiles
31 */
32function css_store_css(theme_config $theme, $csspath, array $cssfiles) {
33 $css = '';
34 foreach ($cssfiles as $file) {
35 $css .= "\n".file_get_contents($file);
36 }
37 $css = $theme->post_process($css);
38
39 $optimiser = new css_optimiser;
40 $css = $optimiser->process($css);
41
42 check_dir_exists(dirname($csspath));
43 $fp = fopen($csspath, 'w');
44 fwrite($fp, $css);
45 fclose($fp);
46 return true;
47}
48
49/**
50 * Sends IE specific CSS
51 *
52 * @param string $themename
53 * @param string $rev
54 */
55function css_send_ie_css($themename, $rev) {
56 $lifetime = 60*60*24*30; // 30 days
57
58 $css = "/** Unfortunately IE6/7 does not support more than 4096 selectors in one CSS file, which means we have to use some ugly hacks :-( **/";
59 $css = "@import url(styles.php?theme=$themename&rev=$rev&type=plugins);";
60 $css = "@import url(styles.php?theme=$themename&rev=$rev&type=parents);";
61 $css = "@import url(styles.php?theme=$themename&rev=$rev&type=theme);";
62
63 header('Etag: '.md5($rev));
64 header('Content-Disposition: inline; filename="styles.php"');
65 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
66 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
67 header('Pragma: ');
68 header('Cache-Control: max-age='.$lifetime);
69 header('Accept-Ranges: none');
70 header('Content-Type: text/css; charset=utf-8');
71 header('Content-Length: '.strlen($css));
72
73 echo $css;
74 die;
75}
76
77/**
78 * Sends a cached CSS file
79 *
80 * @param string $csspath
81 * @param string $rev
82 */
83function css_send_cached_css($csspath, $rev) {
84 $lifetime = 60*60*24*30; // 30 days
85
86 header('Content-Disposition: inline; filename="styles.php"');
87 header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
88 header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
89 header('Pragma: ');
90 header('Cache-Control: max-age='.$lifetime);
91 header('Accept-Ranges: none');
92 header('Content-Type: text/css; charset=utf-8');
93 if (!min_enable_zlib_compression()) {
94 header('Content-Length: '.filesize($csspath));
95 }
96
97 readfile($csspath);
98 die;
99}
100
101/**
102 * Sends CSS directly without caching it.
103 *
104 * @param string CSS
105 */
106function css_send_uncached_css($css) {
107
108 header('Content-Disposition: inline; filename="styles_debug.php"');
109 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
110 header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT');
111 header('Pragma: ');
112 header('Accept-Ranges: none');
113 header('Content-Type: text/css; charset=utf-8');
114
115 if (is_array($css)) {
116 $css = implode("\n\n", $css);
117 }
118 $css = str_replace("\n", "\r\n", $css);
119 $optimiser = new css_optimiser;
120 echo $optimiser->process($css);
121
122 die;
123}
124
125/**
126 * Sends a 404 message about CSS not being found.
127 */
128function css_send_css_not_found() {
129 header('HTTP/1.0 404 not found');
130 die('CSS was not found, sorry.');
131}
132
133/**
134 * A basic CSS optimiser that strips out unwanted things and then processing the
135 * CSS organising styles and moving duplicates and useless CSS.
136 *
137 * @package moodlecore
138 * @copyright 2011 Sam Hemelryk
139 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
140 */
141class css_optimiser {
142
143 /**#@+
144 * Processing states. Used internally.
145 */
146 const PROCESSING_START = 0;
147 const PROCESSING_SELECTORS = 0;
148 const PROCESSING_STYLES = 1;
149 const PROCESSING_COMMENT = 2;
150 const PROCESSING_ATRULE = 3;
151 /**#@-*/
152
153 /**#@+
154 * Stats variables set during and after processing
155 * @var int
156 */
157 protected $rawstrlen = 0;
158 protected $commentsincss = 0;
159 protected $rawrules = 0;
160 protected $rawselectors = 0;
161 protected $optimisedstrlen = 0;
162 protected $optimisedrules = 0;
163 protected $optimisedselectors = 0;
164 protected $timestart = 0;
165 protected $timecomplete = 0;
166 /**#@-*/
167
168 /**
169 * Processes incoming CSS optimising it and then returning it.
170 *
171 * @param string $css The raw CSS to optimise
172 * @return string The optimised CSS
173 */
174 public function process($css) {
175 global $CFG;
176
177 $this->reset_stats();
178 $this->timestart = microtime(true);
179 $this->rawstrlen = strlen($css);
180
181 // First up we need to remove all line breaks - this allows us to instantly
182 // reduce our processing requirements and as we will process everything
183 // into a new structure there's really nothing lost.
184 $css = preg_replace('#\r?\n#', ' ', $css);
185
186 // Next remove the comments... no need to them in an optimised world and
187 // knowing they're all gone allows us to REALLY make our processing simpler
188 $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
189
190 $medias = array(
191 'all' => new css_media()
192 );
193 $imports = array();
194 $charset = false;
195
196 $currentprocess = self::PROCESSING_START;
197 $currentstyle = css_rule::init();
198 $currentselector = css_selector::init();
199 $inquotes = false; // ' or "
200 $inbraces = false; // {
201 $inbrackets = false; // [
202 $inparenthesis = false; // (
203 $currentmedia = $medias['all'];
204 $currentatrule = null;
205 $suspectatrule = false;
206
207 $buffer = '';
208 $char = null;
209
210 // Next we are going to iterate over every single character in $css.
211 // This is why re removed line breaks and comments!
212 for ($i = 0; $i < $this->rawstrlen; $i++) {
213 $lastchar = $char;
214 $char = substr($css, $i, 1);
215 if ($char == '@' && $buffer == '') {
216 $suspectatrule = true;
217 }
218 switch ($currentprocess) {
219 // Start processing an at rule e.g. @media, @page
220 case self::PROCESSING_ATRULE:
221 switch ($char) {
222 case ';':
223 if (!$inbraces) {
224 $buffer .= $char;
225 if ($currentatrule == 'import') {
226 $imports[] = $buffer;
227 $currentprocess = self::PROCESSING_SELECTORS;
228 } else if ($currentatrule == 'charset') {
229 $charset = $buffer;
230 $currentprocess = self::PROCESSING_SELECTORS;
231 }
232 }
233 $buffer = '';
234 $currentatrule = false;
235 continue 3;
236 case '{':
237 if ($currentatrule == 'media' && preg_match('#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)#', $buffer, $matches)) {
238 $mediatypes = str_replace(' ', '', $matches[1]);
239 if (!array_key_exists($mediatypes, $medias)) {
240 $medias[$mediatypes] = new css_media($mediatypes);
241 }
242 $currentmedia = $medias[$mediatypes];
243 $currentprocess = self::PROCESSING_SELECTORS;
244 $buffer = '';
245 }
246 continue 3;
247 }
248 break;
249 // Start processing selectors
250 case self::PROCESSING_START:
251 case self::PROCESSING_SELECTORS:
252 switch ($char) {
253 case '[':
254 $inbrackets ++;
255 $buffer .= $char;
256 continue 3;
257 case ']':
258 $inbrackets --;
259 $buffer .= $char;
260 continue 3;
261 case ' ':
262 if ($inbrackets) {
263 continue 3;
264 }
265 if (!empty($buffer)) {
266 if ($suspectatrule && preg_match('#@(media|import|charset)\s*#', $buffer, $matches)) {
267 $currentatrule = $matches[1];
268 $currentprocess = self::PROCESSING_ATRULE;
269 $buffer .= $char;
270 } else {
271 $currentselector->add($buffer);
272 $buffer = '';
273 }
274 }
275 $suspectatrule = false;
276 continue 3;
277 case '{':
278 if ($inbrackets) {
279 continue 3;
280 }
281
282 $currentselector->add($buffer);
283 $currentstyle->add_selector($currentselector);
284 $currentselector = css_selector::init();
285 $currentprocess = self::PROCESSING_STYLES;
286
287 $buffer = '';
288 continue 3;
289 case '}':
290 if ($inbrackets) {
291 continue 3;
292 }
293 if ($currentatrule == 'media') {
294 $currentmedia = $medias['all'];
295 $currentatrule = false;
296 $buffer = '';
297 }
298 continue 3;
299 case ',':
300 if ($inbrackets) {
301 continue 3;
302 }
303 $currentselector->add($buffer);
304 $currentstyle->add_selector($currentselector);
305 $currentselector = css_selector::init();
306 $buffer = '';
307 continue 3;
308 }
309 break;
310 // Start processing styles
311 case self::PROCESSING_STYLES:
312 if ($char == '"' || $char == "'") {
313 if ($inquotes === false) {
314 $inquotes = $char;
315 }
316 if ($inquotes === $char && $lastchar !== '\\') {
317 $inquotes = false;
318 }
319 }
320 if ($inquotes) {
321 $buffer .= $char;
322 continue 2;
323 }
324 switch ($char) {
325 case ';':
326 $currentstyle->add_style($buffer);
327 $buffer = '';
328 $inquotes = false;
329 continue 3;
330 case '}':
331 $currentstyle->add_style($buffer);
332 $this->rawselectors += $currentstyle->get_selector_count();
333
334 $currentmedia->add_rule($currentstyle);
335
336 $currentstyle = css_rule::init();
337 $currentprocess = self::PROCESSING_SELECTORS;
338 $this->rawrules++;
339 $buffer = '';
340 $inquotes = false;
341 continue 3;
342 }
343 break;
344 }
345 $buffer .= $char;
346 }
347
348 $css = '';
349 if (!empty($charset)) {
350 $imports[] = $charset;
351 }
352 if (!empty($imports)) {
353 $css .= implode("\n", $imports);
354 $css .= "\n\n";
355 }
356 foreach ($medias as $media) {
357 $media->organise_rules_by_selectors();
358 $this->optimisedrules += $media->count_rules();
359 $this->optimisedselectors += $media->count_selectors();
360 $css .= $media->out();
361 }
362 $this->optimisedstrlen = strlen($css);
363
364 $this->timecomplete = microtime(true);
365 if (!empty($CFG->includecssstats)) {
366 $css = $this->get_stats().$css;
367 }
368 return trim($css);
369 }
370
371 /**
372 * Returns a string to display the stats generated during the processing of
373 * raw CSS.
374 * @return string
375 */
376 public function get_stats() {
377
378 $strlenimprovement = round(($this->optimisedstrlen / $this->rawstrlen) * 100, 1);
379 $ruleimprovement = round(($this->optimisedrules / $this->rawrules) * 100, 1);
380 $selectorimprovement = round(($this->optimisedselectors / $this->rawselectors) * 100, 1);
381 $timetaken = round($this->timecomplete - $this->timestart, 4);
382
383 $computedcss = "/****************************************\n";
384 $computedcss .= " *------- CSS Optimisation stats --------\n";
385 $computedcss .= " * ".date('r')."\n";
386 $computedcss .= " * {$this->commentsincss} \t comments removed\n";
387 $computedcss .= " * Optimization took $timetaken seconds\n";
388 $computedcss .= " *--------------- before ----------------\n";
389 $computedcss .= " * {$this->rawstrlen} \t chars read in\n";
390 $computedcss .= " * {$this->rawrules} \t rules read in\n";
391 $computedcss .= " * {$this->rawselectors} \t total selectors\n";
392 $computedcss .= " *---------------- after ----------------\n";
393 $computedcss .= " * {$this->optimisedstrlen} \t chars once optimized\n";
394 $computedcss .= " * {$this->optimisedrules} \t optimized rules\n";
395 $computedcss .= " * {$this->optimisedselectors} \t total selectors once optimized\n";
396 $computedcss .= " *---------------- stats ----------------\n";
397 $computedcss .= " * {$strlenimprovement}% \t improvement in chars\n";
398 $computedcss .= " * {$ruleimprovement}% \t improvement in rules\n";
399 $computedcss .= " * {$selectorimprovement}% \t improvement in selectors\n";
400 $computedcss .= " ****************************************/\n\n";
401
402 return $computedcss;
403 }
404
405 /**
406 * Resets the stats ready for another fresh processing
407 */
408 public function reset_stats() {
409 $this->commentsincss = 0;
410 $this->optimisedrules = 0;
411 $this->optimisedselectors = 0;
412 $this->optimisedstrlen = 0;
413 $this->rawrules = 0;
414 $this->rawselectors = 0;
415 $this->rawstrlen = 0;
416 $this->timecomplete = 0;
417 $this->timestart = 0;
418 }
419
420 /**
421 * An array of the common HTML colours that are supported by most browsers.
422 *
423 * This reference table is used to allow us to unify colours, and will aid
424 * us in identifying buggy CSS using unsupported colours.
425 *
426 * @staticvar array
427 * @var array
428 */
429 public static $htmlcolours = array(
430 'aliceblue' => '#F0F8FF',
431 'antiquewhite' => '#FAEBD7',
432 'aqua' => '#00FFFF',
433 'aquamarine' => '#7FFFD4',
434 'azure' => '#F0FFFF',
435 'beige' => '#F5F5DC',
436 'bisque' => '#FFE4C4',
437 'black' => '#000000',
438 'blanchedalmond' => '#FFEBCD',
439 'blue' => '#0000FF',
440 'blueviolet' => '#8A2BE2',
441 'brown' => '#A52A2A',
442 'burlywood' => '#DEB887',
443 'cadetblue' => '#5F9EA0',
444 'chartreuse' => '#7FFF00',
445 'chocolate' => '#D2691E',
446 'coral' => '#FF7F50',
447 'cornflowerblue' => '#6495ED',
448 'cornsilk' => '#FFF8DC',
449 'crimson' => '#DC143C',
450 'cyan' => '#00FFFF',
451 'darkblue' => '#00008B',
452 'darkcyan' => '#008B8B',
453 'darkgoldenrod' => '#B8860B',
454 'darkgray' => '#A9A9A9',
455 'darkgrey' => '#A9A9A9',
456 'darkgreen' => '#006400',
457 'darkKhaki' => '#BDB76B',
458 'darkmagenta' => '#8B008B',
459 'darkolivegreen' => '#556B2F',
460 'arkorange' => '#FF8C00',
461 'darkorchid' => '#9932CC',
462 'darkred' => '#8B0000',
463 'darksalmon' => '#E9967A',
464 'darkseagreen' => '#8FBC8F',
465 'darkslateblue' => '#483D8B',
466 'darkslategray' => '#2F4F4F',
467 'darkslategrey' => '#2F4F4F',
468 'darkturquoise' => '#00CED1',
469 'darkviolet' => '#9400D3',
470 'deeppink' => '#FF1493',
471 'deepskyblue' => '#00BFFF',
472 'dimgray' => '#696969',
473 'dimgrey' => '#696969',
474 'dodgerblue' => '#1E90FF',
475 'firebrick' => '#B22222',
476 'floralwhite' => '#FFFAF0',
477 'forestgreen' => '#228B22',
478 'fuchsia' => '#FF00FF',
479 'gainsboro' => '#DCDCDC',
480 'ghostwhite' => '#F8F8FF',
481 'gold' => '#FFD700',
482 'goldenrod' => '#DAA520',
483 'gray' => '#808080',
484 'grey' => '#808080',
485 'green' => '#008000',
486 'greenyellow' => '#ADFF2F',
487 'honeydew' => '#F0FFF0',
488 'hotpink' => '#FF69B4',
489 'indianred ' => '#CD5C5C',
490 'indigo ' => '#4B0082',
491 'ivory' => '#FFFFF0',
492 'khaki' => '#F0E68C',
493 'lavender' => '#E6E6FA',
494 'lavenderblush' => '#FFF0F5',
495 'lawngreen' => '#7CFC00',
496 'lemonchiffon' => '#FFFACD',
497 'lightblue' => '#ADD8E6',
498 'lightcoral' => '#F08080',
499 'lightcyan' => '#E0FFFF',
500 'lightgoldenrodyellow' => '#FAFAD2',
501 'lightgray' => '#D3D3D3',
502 'lightgrey' => '#D3D3D3',
503 'lightgreen' => '#90EE90',
504 'lightpink' => '#FFB6C1',
505 'lightsalmon' => '#FFA07A',
506 'lightseagreen' => '#20B2AA',
507 'lightskyblue' => '#87CEFA',
508 'lightslategray' => '#778899',
509 'lightslategrey' => '#778899',
510 'lightsteelblue' => '#B0C4DE',
511 'lightyellow' => '#FFFFE0',
512 'lime' => '#00FF00',
513 'limegreen' => '#32CD32',
514 'linen' => '#FAF0E6',
515 'magenta' => '#FF00FF',
516 'maroon' => '#800000',
517 'mediumaquamarine' => '#66CDAA',
518 'mediumblue' => '#0000CD',
519 'mediumorchid' => '#BA55D3',
520 'mediumpurple' => '#9370D8',
521 'mediumseagreen' => '#3CB371',
522 'mediumslateblue' => '#7B68EE',
523 'mediumspringgreen' => '#00FA9A',
524 'mediumturquoise' => '#48D1CC',
525 'mediumvioletred' => '#C71585',
526 'midnightblue' => '#191970',
527 'mintcream' => '#F5FFFA',
528 'mistyrose' => '#FFE4E1',
529 'moccasin' => '#FFE4B5',
530 'navajowhite' => '#FFDEAD',
531 'navy' => '#000080',
532 'oldlace' => '#FDF5E6',
533 'olive' => '#808000',
534 'olivedrab' => '#6B8E23',
535 'orange' => '#FFA500',
536 'orangered' => '#FF4500',
537 'orchid' => '#DA70D6',
538 'palegoldenrod' => '#EEE8AA',
539 'palegreen' => '#98FB98',
540 'paleturquoise' => '#AFEEEE',
541 'palevioletred' => '#D87093',
542 'papayawhip' => '#FFEFD5',
543 'peachpuff' => '#FFDAB9',
544 'peru' => '#CD853F',
545 'pink' => '#FFC0CB',
546 'plum' => '#DDA0DD',
547 'powderblue' => '#B0E0E6',
548 'purple' => '#800080',
549 'red' => '#FF0000',
550 'rosybrown' => '#BC8F8F',
551 'royalblue' => '#4169E1',
552 'saddlebrown' => '#8B4513',
553 'salmon' => '#FA8072',
554 'sandybrown' => '#F4A460',
555 'seagreen' => '#2E8B57',
556 'seashell' => '#FFF5EE',
557 'sienna' => '#A0522D',
558 'silver' => '#C0C0C0',
559 'skyblue' => '#87CEEB',
560 'slateblue' => '#6A5ACD',
561 'slategray' => '#708090',
562 'slategrey' => '#708090',
563 'snow' => '#FFFAFA',
564 'springgreen' => '#00FF7F',
565 'steelblue' => '#4682B4',
566 'tan' => '#D2B48C',
567 'teal' => '#008080',
568 'thistle' => '#D8BFD8',
569 'tomato' => '#FF6347',
570 'turquoise' => '#40E0D0',
571 'violet' => '#EE82EE',
572 'wheat' => '#F5DEB3',
573 'white' => '#FFFFFF',
574 'whitesmoke' => '#F5F5F5',
575 'yellow' => '#FFFF00',
576 'yellowgreen' => '#9ACD32'
577 );
578}
579
580/**
581 * A structure to represent a CSS selector.
582 *
583 * The selector is the classes, id, elements, and psuedo bits that make up a CSS
584 * rule.
585 *
586 * @package moodlecore
587 * @copyright 2011 Sam Hemelryk
588 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
589 */
590class css_selector {
591
592 /**
593 * An array of selector bits
594 * @var array
595 */
596 protected $selectors = array();
597
598 /**
599 * The number of selectors.
600 * @var int
601 */
602 protected $count = 0;
603
604 /**
605 * Initialises a new CSS selector
606 * @return css_selector
607 */
608 public static function init() {
609 return new css_selector();
610 }
611
612 /**
613 * CSS selectors can only be created through the init method above.
614 */
615 protected function __construct() {}
616
617 /**
618 * Adds a selector to the end of the current selector
619 * @param string $selector
620 */
621 public function add($selector) {
622 $selector = trim($selector);
623 $count = 0;
624 $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
625 if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
626 $count ++;
627 }
628 $this->count = $count;
629 $this->selectors[] = $selector;
630 }
631 /**
632 * Returns the number of individual components that make up this selector
633 * @return int
634 */
635 public function get_selector_count() {
636 return $this->count;
637 }
638
639 /**
640 * Returns the selector for use in a CSS rule
641 * @return string
642 */
643 public function out() {
644 return trim(join(' ', $this->selectors));
645 }
646}
647
648/**
649 * A structure to represent a CSS rule.
650 *
651 * @package moodlecore
652 * @copyright 2011 Sam Hemelryk
653 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
654 */
655class css_rule {
656
657 /**
658 * An array of CSS selectors {@see css_selector}
659 * @var array
660 */
661 protected $selectors = array();
662
663 /**
664 * An array of CSS styles {@see css_style}
665 * @var array
666 */
667 protected $styles = array();
668
669 /**
670 * Created a new CSS rule. This is the only way to create a new CSS rule externally.
671 * @return css_rule
672 */
673 public static function init() {
674 return new css_rule();
675 }
676
677 /**
678 * Constructs a new css rule - this can only be called from within the scope of
679 * this class or its descendants.
680 *
681 * @param type $selector
682 * @param array $styles
683 */
684 protected function __construct($selector = null, array $styles = array()) {
685 if ($selector != null) {
686 if (is_array($selector)) {
687 $this->selectors = $selector;
688 } else {
689 $this->selectors = array($selector);
690 }
691 $this->add_styles($styles);
692 }
693 }
694
695 /**
696 * Adds a new CSS selector to this rule
697 *
698 * @param css_selector $selector
699 */
700 public function add_selector(css_selector $selector) {
701 $this->selectors[] = $selector;
702 }
703
704 /**
705 * Adds a new CSS style to this rule.
706 *
707 * @param css_style|string $style
708 */
709 public function add_style($style) {
710 if (is_string($style)) {
711 $style = trim($style);
712 if (empty($style)) {
713 return;
714 }
715 $bits = explode(':', $style, 2);
716 if (count($bits) == 2) {
717 list($name, $value) = array_map('trim', $bits);
718 }
719 if (isset($name) && isset($value) && $name !== '' && $value !== '') {
720 $style = css_style::init($name, $value);
721 }
722 }
723 if ($style instanceof css_style) {
724 $name = $style->get_name();
725 if (array_key_exists($name, $this->styles)) {
726 $this->styles[$name]->set_value($style->get_value());
727 } else {
728 $this->styles[$name] = $style;
729 }
730 }
731 }
732
733 /**
734 * An easy method of adding several styles at once. Just calls add_style.
735 *
736 * @param array $styles
737 */
738 public function add_styles(array $styles) {
739 foreach ($styles as $style) {
740 $this->add_style($style);
741 }
742 }
743
744 /**
745 * Returns all of the styles as a single string that can be used in a CSS
746 * rule.
747 *
748 * @return string
749 */
750 protected function get_style_sting() {
751 $bits = array();
752 foreach ($this->styles as $style) {
753 $bits[] = $style->out();
754 }
755 return join('', $bits);
756 }
757
758 /**
759 * Returns all of the selectors as a single string that can be used in a
760 * CSS rule
761 *
762 * @return string
763 */
764 protected function get_selector_string() {
765 $selectors = array();
766 foreach ($this->selectors as $selector) {
767 $selectors[] = $selector->out();
768 }
769 return join(",\n", $selectors);
770 }
771
772 /**
773 * Returns the array of selectors
774 * @return array
775 */
776 public function get_selectors() {
777 return $this->selectors;
778 }
779
780 /**
781 * Returns the array of styles
782 * @return array
783 */
784 public function get_styles() {
785 return $this->styles;
786 }
787
788 /**
789 * Outputs this rule as a fragment of CSS
790 * @return string
791 */
792 public function out() {
793 $css = $this->get_selector_string();
794 $css .= '{';
795 $css .= $this->get_style_sting();
796 $css .= '}';
797 return $css;
798 }
799
800 /**
801 * Splits this rules into an array of CSS rules. One for each of the selectors
802 * that make up this rule.
803 *
804 * @return array(css_rule)
805 */
806 public function split_by_selector() {
807 $return = array();
808 foreach ($this->selectors as $selector) {
809 $return[] = new css_rule($selector, $this->styles);
810 }
811 return $return;
812 }
813
814 /**
815 * Splits this rule into an array of rules. One for each of the styles that
816 * make up this rule
817 *
818 * @return array(css_rule)
819 */
820 public function split_by_style() {
821 $return = array();
822 foreach ($this->styles as $style) {
823 $return[] = new css_rule($this->selectors, array($style));
824 }
825 return $return;
826 }
827
828 /**
829 * Gets a hash for the styles of this rule
830 * @return string
831 */
832 public function get_style_hash() {
833 $styles = $this->get_style_sting();
834 return md5($styles);
835 }
836
837 /**
838 * Gets a hash for the selectors of this rule
839 * @return string
840 */
841 public function get_selector_hash() {
842 $selector = $this->get_selector_string();
843 return md5($selector);
844 }
845
846 /**
847 * Gets the number of selectors that make up this rule.
848 * @return int
849 */
850 public function get_selector_count() {
851 $count = 0;
852 foreach ($this->selectors as $selector) {
853 $count += $selector->get_selector_count();
854 }
855 return $count;
856 }
857}
858
859/**
860 * A media class to organise rules by the media they apply to.
861 *
862 * @package moodlecore
863 * @copyright 2011 Sam Hemelryk
864 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
865 */
866class css_media {
867
868 /**
869 * An array of the different media types this instance applies to.
870 * @var array
871 */
872 protected $types = array();
873
874 /**
875 * An array of rules within this media instance
876 * @var array
877 */
878 protected $rules = array();
879
880 /**
881 * Initalises a new media instance
882 *
883 * @param type $for
884 */
885 public function __construct($for = 'all') {
886 $types = explode(',', $for);
887 $this->types = array_map('trim', $types);
888 }
889
890 /**
891 * Adds a new CSS rule to this media instance
892 *
893 * @param css_rule $newrule
894 */
895 public function add_rule(css_rule $newrule) {
896 foreach ($newrule->split_by_selector() as $rule) {
897 $hash = $rule->get_selector_hash();
898 if (!array_key_exists($hash, $this->rules)) {
899 $this->rules[$hash] = $rule;
900 } else {
901 $this->rules[$hash]->add_styles($rule->get_styles());
902 }
903 }
904 }
905
906 /**
907 * Returns the rules used by this
908 *
909 * @return array
910 */
911 public function get_rules() {
912 return $this->rules;
913 }
914
915 /**
916 * Organises rules by gropuing selectors based upon the styles and consolidating
917 * those selectors into single rules.
918 *
919 * @return array An array of optimised styles
920 */
921 public function organise_rules_by_selectors() {
922 $optimised = array();
923 $beforecount = count($this->rules);
924 foreach ($this->rules as $rule) {
925 $hash = $rule->get_style_hash();
926 if (!array_key_exists($hash, $optimised)) {
927 $optimised[$hash] = clone($rule);
928 } else {
929 foreach ($rule->get_selectors() as $selector) {
930 $optimised[$hash]->add_selector($selector);
931 }
932 }
933 }
934 $this->rules = $optimised;
935 $aftercount = count($this->rules);
936 return ($beforecount < $aftercount);
937 }
938
939 /**
940 * Returns the total number of rules that exist within this media set
941 *
942 * @return int
943 */
944 public function count_rules() {
945 return count($this->rules);
946 }
947
948 /**
949 * Returns the total number of selectors that exist within this media set
950 *
951 * @return int
952 */
953 public function count_selectors() {
954 $count = 0;
955 foreach ($this->rules as $rule) {
956 $count += $rule->get_selector_count();
957 }
958 return $count;
959 }
960
961 /**
962 * Returns the CSS for this media and all of its rules.
963 *
964 * @return string
965 */
966 public function out() {
967 $output = '';
968 $types = join(',', $this->types);
969 if ($types !== 'all') {
970 $output .= "\n\n/***** New media declaration *****/\n";
971 $output .= "@media {$types} {\n";
972 }
973 foreach ($this->rules as $rule) {
974 $output .= $rule->out()."\n";
975 }
976 if ($types !== 'all') {
977 $output .= '}';
978 $output .= "\n/***** Media declaration end for $types *****/";
979 }
980 return $output;
981 }
982
983 /**
984 * Returns an array of media that this media instance applies to
985 *
986 * @return array
987 */
988 public function get_types() {
989 return $this->types;
990 }
991}
992
993/**
994 * An absract class to represent CSS styles
995 *
996 * @package moodlecore
997 * @copyright 2011 Sam Hemelryk
998 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
999 */
1000abstract class css_style {
1001
1002 /**
1003 * The name of the style
1004 * @var string
1005 */
1006 protected $name;
1007
1008 /**
1009 * The value for the style
1010 * @var mixed
1011 */
1012 protected $value;
1013
1014 /**
1015 * If set to true this style was defined with the !important rule.
1016 * @var bool
1017 */
1018 protected $important = false;
1019
1020 /**
1021 * Initialises a new style.
1022 *
1023 * This is the only public way to create a style to ensure they that appropriate
1024 * style class is used if it exists.
1025 *
1026 * @param type $name
1027 * @param type $value
1028 * @return css_style_generic
1029 */
1030 public static function init($name, $value) {
1031 $specificclass = 'css_style_'.preg_replace('#[^a-zA-Z0-9]+#', '', $name);
1032 if (class_exists($specificclass)) {
1033 return $specificclass::init($value);
1034 }
1035 return new css_style_generic($name, $value);
1036 }
1037
1038 /**
1039 * Creates a new style when given its name and value
1040 *
1041 * @param string $name
1042 * @param string $value
1043 */
1044 protected function __construct($name, $value) {
1045 $this->name = $name;
1046 $this->set_value($value);
1047 }
1048
1049 /**
1050 * Sets the value for the style
1051 *
1052 * @param string $value
1053 */
1054 final public function set_value($value) {
1055 $value = trim($value);
1056 $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
1057 if ($important) {
1058 $value = substr($value, 0, -(strlen($matches[1])));
1059 }
1060 if (!$this->important || $important) {
1061 $this->value = $this->clean_value($value);
1062 $this->important = $important;
1063 }
1064 }
1065
1066 /**
1067 * Returns the name for the style
1068 *
1069 * @return string
1070 */
1071 public function get_name() {
1072 return $this->name;
1073 }
1074
1075 /**
1076 * Returns the value for the style
1077 *
1078 * @return string
1079 */
1080 public function get_value() {
1081 $value = $this->value;
1082 if ($this->important) {
1083 $value .= ' !important';
1084 }
1085 return $value;
1086 }
1087
1088 /**
1089 * Returns the style ready for use in CSS
1090 *
1091 * @param string|null $value
1092 * @return string
1093 */
1094 public function out($value = null) {
1095 if ($value === null) {
1096 $value = $this->get_value();
1097 } else if ($this->important && strpos($value, '!important') === false) {
1098 $value .= ' !important';
1099 }
1100 return "{$this->name}:{$value};";
1101 }
1102
1103 /**
1104 * This can be overridden by a specific style allowing it to clean its values
1105 * consistently.
1106 *
1107 * @param mixed $value
1108 * @return mixed
1109 */
1110 protected function clean_value($value) {
1111 return $value;
1112 }
1113}
1114
1115/**
1116 * A generic CSS style class to use when a more specific class does not exist.
1117 *
1118 * @package moodlecore
1119 * @copyright 2011 Sam Hemelryk
1120 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1121 */
1122class css_style_generic extends css_style {
1123 /**
1124 * Cleans incoming values for typical things that can be optimised.
1125 *
1126 * @param mixed $value
1127 * @return string
1128 */
1129 protected function clean_value($value) {
1130 if (trim($value) == '0px') {
1131 $value = 0;
1132 } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
1133 $value = '#'.strtoupper($matches[1]);
1134 }
1135 return $value;
1136 }
1137}
1138
1139/**
1140 * A colour CSS style
1141 *
1142 * @package moodlecore
1143 * @copyright 2011 Sam Hemelryk
1144 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1145 */
1146class css_style_color extends css_style {
1147 /**
1148 * Creates a new colour style
1149 *
1150 * @param mixed $value
1151 * @return css_style_color
1152 */
1153 public static function init($value) {
1154 return new css_style_color('color', $value);
1155 }
1156
1157 /**
1158 * Cleans the colour unifing it to a 6 char hash colour if possible
1159 * Doing this allows us to associate identical colours being specified in
1160 * different ways. e.g. Red, red, #F00, and #F00000
1161 *
1162 * @param mixed $value
1163 * @return string
1164 */
1165 protected function clean_value($value) {
1166 $value = trim($value);
1167 if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
1168 $value = '#'.strtoupper($matches[1]);
1169 } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
1170 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
1171 $value = '#'.strtoupper($value);
1172 } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
1173 $value = css_optimiser::$htmlcolours[strtolower($value)];
1174 }
1175 return $value;
1176 }
1177
1178 /**
1179 * Returns the colour style for use within CSS.
1180 * Will return an optimised hash colour.
1181 *
1182 * e.g #123456
1183 * #123 instead of #112233
1184 * #F00 instead of red
1185 *
1186 * @param string $overridevalue
1187 * @return string
1188 */
1189 public function out($overridevalue = null) {
1190 if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $this->value, $matches)) {
1191 $overridevalue = '#'.$matches[1].$matches[2].$matches[3];
1192 }
1193 return parent::out($overridevalue);
1194 }
1195}
1196
1197/**
1198 * A background colour style.
1199 *
1200 * Based upon the colour style.
1201 *
1202 * @package moodlecore
1203 * @copyright 2011 Sam Hemelryk
1204 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1205 */
1206class css_style_backgroundcolor extends css_style_color {
1207 /**
1208 * Creates a new background colour style
1209 *
1210 * @param mixed $value
1211 * @return css_style_backgroundcolor
1212 */
1213 public static function init($value) {
1214 return new css_style_backgroundcolor('background-color', $value);
1215 }
1216}
1217
1218/**
1219 * A border colour style
1220 *
1221 * @package moodlecore
1222 * @copyright 2011 Sam Hemelryk
1223 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1224 */
1225class css_style_bordercolor extends css_style_color {
1226 /**
1227 * Creates a new border colour style
1228 *
1229 * Based upon the colour style
1230 *
1231 * @param mixed $value
1232 * @return css_style_bordercolor
1233 */
1234 public static function init($value) {
1235 return new css_style_bordercolor('border-color', $value);
1236 }
1237}
1238
1239/**
1240 * A border style
1241 *
1242 * @package moodlecore
1243 * @copyright 2011 Sam Hemelryk
1244 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1245 */
1246class css_style_border extends css_style {
1247 /**
1248 * Created a new border style
1249 *
1250 * @param mixed $value
1251 * @return css_style_border
1252 */
1253 public static function init($value) {
1254 return new css_style_border('border', $value);
1255 }
1256}