MDL-29941 csslib: Improved the stats generation and output
[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);
8589a4a5
SH
365 if (!empty($CFG->cssincludestats)) {
366 $css = $this->output_stats_css().$css;
0e641c74
SH
367 }
368 return trim($css);
369 }
370
371 /**
8589a4a5 372 * Returns an array of stats from the last processing run
0e641c74
SH
373 * @return string
374 */
375 public function get_stats() {
376
8589a4a5
SH
377 $stats = array(
378 'timestart' => $this->timestart,
379 'timecomplete' => $this->timecomplete,
380 'timetaken' => round($this->timecomplete - $this->timestart, 4),
381 'commentsincss' => $this->commentsincss,
382 'rawstrlen' => $this->rawstrlen,
383 'rawselectors' => $this->rawselectors,
384 'rawrules' => $this->rawrules,
385 'optimisedstrlen' => $this->optimisedstrlen,
386 'optimisedrules' => $this->optimisedrules,
387 'optimiedselectors' => $this->optimisedselectors,
388 'improvementstrlen' => round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%',
389 'improvementrules' => round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%',
390 'improvementselectors' => round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%',
391 );
392 return $stats;
393 }
394
395 /**
396 * Returns a string to display stats about the last generation within CSS output
397 * @return string
398 */
399 public function output_stats_css() {
400 $stats = $this->get_stats();
401
402 $strlenimprovement = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1);
403 $ruleimprovement = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1);
404 $selectorimprovement = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1);
0e641c74
SH
405 $timetaken = round($this->timecomplete - $this->timestart, 4);
406
407 $computedcss = "/****************************************\n";
408 $computedcss .= " *------- CSS Optimisation stats --------\n";
409 $computedcss .= " * ".date('r')."\n";
8589a4a5
SH
410 $computedcss .= " * {$stats[commentsincss]} \t comments removed\n";
411 $computedcss .= " * Optimisation took {$stats[timetaken]} seconds\n";
0e641c74 412 $computedcss .= " *--------------- before ----------------\n";
8589a4a5
SH
413 $computedcss .= " * {$stats[rawstrlen]} \t chars read in\n";
414 $computedcss .= " * {$stats[rawrules]} \t rules read in\n";
415 $computedcss .= " * {$stats[rawselectors]} \t total selectors\n";
0e641c74 416 $computedcss .= " *---------------- after ----------------\n";
8589a4a5
SH
417 $computedcss .= " * {$stats[optimisedstrlen]} \t chars once optimized\n";
418 $computedcss .= " * {$stats[optimisedrules]} \t optimized rules\n";
419 $computedcss .= " * {$stats[optimisedselectors]} \t total selectors once optimized\n";
0e641c74 420 $computedcss .= " *---------------- stats ----------------\n";
8589a4a5
SH
421 $computedcss .= " * {$stats[strlenimprovement]}% \t reduction in chars\n";
422 $computedcss .= " * {$stats[ruleimprovement]}% \t reduction in rules\n";
423 $computedcss .= " * {$stats[selectorimprovement]}% \t reduction in selectors\n";
0e641c74
SH
424 $computedcss .= " ****************************************/\n\n";
425
426 return $computedcss;
427 }
428
429 /**
430 * Resets the stats ready for another fresh processing
431 */
432 public function reset_stats() {
433 $this->commentsincss = 0;
434 $this->optimisedrules = 0;
435 $this->optimisedselectors = 0;
436 $this->optimisedstrlen = 0;
437 $this->rawrules = 0;
438 $this->rawselectors = 0;
439 $this->rawstrlen = 0;
440 $this->timecomplete = 0;
441 $this->timestart = 0;
442 }
443
444 /**
445 * An array of the common HTML colours that are supported by most browsers.
446 *
447 * This reference table is used to allow us to unify colours, and will aid
448 * us in identifying buggy CSS using unsupported colours.
449 *
450 * @staticvar array
451 * @var array
452 */
453 public static $htmlcolours = array(
454 'aliceblue' => '#F0F8FF',
455 'antiquewhite' => '#FAEBD7',
456 'aqua' => '#00FFFF',
457 'aquamarine' => '#7FFFD4',
458 'azure' => '#F0FFFF',
459 'beige' => '#F5F5DC',
460 'bisque' => '#FFE4C4',
461 'black' => '#000000',
462 'blanchedalmond' => '#FFEBCD',
463 'blue' => '#0000FF',
464 'blueviolet' => '#8A2BE2',
465 'brown' => '#A52A2A',
466 'burlywood' => '#DEB887',
467 'cadetblue' => '#5F9EA0',
468 'chartreuse' => '#7FFF00',
469 'chocolate' => '#D2691E',
470 'coral' => '#FF7F50',
471 'cornflowerblue' => '#6495ED',
472 'cornsilk' => '#FFF8DC',
473 'crimson' => '#DC143C',
474 'cyan' => '#00FFFF',
475 'darkblue' => '#00008B',
476 'darkcyan' => '#008B8B',
477 'darkgoldenrod' => '#B8860B',
478 'darkgray' => '#A9A9A9',
479 'darkgrey' => '#A9A9A9',
480 'darkgreen' => '#006400',
481 'darkKhaki' => '#BDB76B',
482 'darkmagenta' => '#8B008B',
483 'darkolivegreen' => '#556B2F',
484 'arkorange' => '#FF8C00',
485 'darkorchid' => '#9932CC',
486 'darkred' => '#8B0000',
487 'darksalmon' => '#E9967A',
488 'darkseagreen' => '#8FBC8F',
489 'darkslateblue' => '#483D8B',
490 'darkslategray' => '#2F4F4F',
491 'darkslategrey' => '#2F4F4F',
492 'darkturquoise' => '#00CED1',
493 'darkviolet' => '#9400D3',
494 'deeppink' => '#FF1493',
495 'deepskyblue' => '#00BFFF',
496 'dimgray' => '#696969',
497 'dimgrey' => '#696969',
498 'dodgerblue' => '#1E90FF',
499 'firebrick' => '#B22222',
500 'floralwhite' => '#FFFAF0',
501 'forestgreen' => '#228B22',
502 'fuchsia' => '#FF00FF',
503 'gainsboro' => '#DCDCDC',
504 'ghostwhite' => '#F8F8FF',
505 'gold' => '#FFD700',
506 'goldenrod' => '#DAA520',
507 'gray' => '#808080',
508 'grey' => '#808080',
509 'green' => '#008000',
510 'greenyellow' => '#ADFF2F',
511 'honeydew' => '#F0FFF0',
512 'hotpink' => '#FF69B4',
513 'indianred ' => '#CD5C5C',
514 'indigo ' => '#4B0082',
515 'ivory' => '#FFFFF0',
516 'khaki' => '#F0E68C',
517 'lavender' => '#E6E6FA',
518 'lavenderblush' => '#FFF0F5',
519 'lawngreen' => '#7CFC00',
520 'lemonchiffon' => '#FFFACD',
521 'lightblue' => '#ADD8E6',
522 'lightcoral' => '#F08080',
523 'lightcyan' => '#E0FFFF',
524 'lightgoldenrodyellow' => '#FAFAD2',
525 'lightgray' => '#D3D3D3',
526 'lightgrey' => '#D3D3D3',
527 'lightgreen' => '#90EE90',
528 'lightpink' => '#FFB6C1',
529 'lightsalmon' => '#FFA07A',
530 'lightseagreen' => '#20B2AA',
531 'lightskyblue' => '#87CEFA',
532 'lightslategray' => '#778899',
533 'lightslategrey' => '#778899',
534 'lightsteelblue' => '#B0C4DE',
535 'lightyellow' => '#FFFFE0',
536 'lime' => '#00FF00',
537 'limegreen' => '#32CD32',
538 'linen' => '#FAF0E6',
539 'magenta' => '#FF00FF',
540 'maroon' => '#800000',
541 'mediumaquamarine' => '#66CDAA',
542 'mediumblue' => '#0000CD',
543 'mediumorchid' => '#BA55D3',
544 'mediumpurple' => '#9370D8',
545 'mediumseagreen' => '#3CB371',
546 'mediumslateblue' => '#7B68EE',
547 'mediumspringgreen' => '#00FA9A',
548 'mediumturquoise' => '#48D1CC',
549 'mediumvioletred' => '#C71585',
550 'midnightblue' => '#191970',
551 'mintcream' => '#F5FFFA',
552 'mistyrose' => '#FFE4E1',
553 'moccasin' => '#FFE4B5',
554 'navajowhite' => '#FFDEAD',
555 'navy' => '#000080',
556 'oldlace' => '#FDF5E6',
557 'olive' => '#808000',
558 'olivedrab' => '#6B8E23',
559 'orange' => '#FFA500',
560 'orangered' => '#FF4500',
561 'orchid' => '#DA70D6',
562 'palegoldenrod' => '#EEE8AA',
563 'palegreen' => '#98FB98',
564 'paleturquoise' => '#AFEEEE',
565 'palevioletred' => '#D87093',
566 'papayawhip' => '#FFEFD5',
567 'peachpuff' => '#FFDAB9',
568 'peru' => '#CD853F',
569 'pink' => '#FFC0CB',
570 'plum' => '#DDA0DD',
571 'powderblue' => '#B0E0E6',
572 'purple' => '#800080',
573 'red' => '#FF0000',
574 'rosybrown' => '#BC8F8F',
575 'royalblue' => '#4169E1',
576 'saddlebrown' => '#8B4513',
577 'salmon' => '#FA8072',
578 'sandybrown' => '#F4A460',
579 'seagreen' => '#2E8B57',
580 'seashell' => '#FFF5EE',
581 'sienna' => '#A0522D',
582 'silver' => '#C0C0C0',
583 'skyblue' => '#87CEEB',
584 'slateblue' => '#6A5ACD',
585 'slategray' => '#708090',
586 'slategrey' => '#708090',
587 'snow' => '#FFFAFA',
588 'springgreen' => '#00FF7F',
589 'steelblue' => '#4682B4',
590 'tan' => '#D2B48C',
591 'teal' => '#008080',
592 'thistle' => '#D8BFD8',
593 'tomato' => '#FF6347',
594 'turquoise' => '#40E0D0',
595 'violet' => '#EE82EE',
596 'wheat' => '#F5DEB3',
597 'white' => '#FFFFFF',
598 'whitesmoke' => '#F5F5F5',
599 'yellow' => '#FFFF00',
600 'yellowgreen' => '#9ACD32'
601 );
602}
603
604/**
605 * A structure to represent a CSS selector.
606 *
607 * The selector is the classes, id, elements, and psuedo bits that make up a CSS
608 * rule.
609 *
610 * @package moodlecore
611 * @copyright 2011 Sam Hemelryk
612 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
613 */
614class css_selector {
615
616 /**
617 * An array of selector bits
618 * @var array
619 */
620 protected $selectors = array();
621
622 /**
623 * The number of selectors.
624 * @var int
625 */
626 protected $count = 0;
627
628 /**
629 * Initialises a new CSS selector
630 * @return css_selector
631 */
632 public static function init() {
633 return new css_selector();
634 }
635
636 /**
637 * CSS selectors can only be created through the init method above.
638 */
639 protected function __construct() {}
640
641 /**
642 * Adds a selector to the end of the current selector
643 * @param string $selector
644 */
645 public function add($selector) {
646 $selector = trim($selector);
647 $count = 0;
648 $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
649 if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
650 $count ++;
651 }
652 $this->count = $count;
653 $this->selectors[] = $selector;
654 }
655 /**
656 * Returns the number of individual components that make up this selector
657 * @return int
658 */
659 public function get_selector_count() {
660 return $this->count;
661 }
662
663 /**
664 * Returns the selector for use in a CSS rule
665 * @return string
666 */
667 public function out() {
668 return trim(join(' ', $this->selectors));
669 }
670}
671
672/**
673 * A structure to represent a CSS rule.
674 *
675 * @package moodlecore
676 * @copyright 2011 Sam Hemelryk
677 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
678 */
679class css_rule {
680
681 /**
682 * An array of CSS selectors {@see css_selector}
683 * @var array
684 */
685 protected $selectors = array();
686
687 /**
688 * An array of CSS styles {@see css_style}
689 * @var array
690 */
691 protected $styles = array();
692
693 /**
694 * Created a new CSS rule. This is the only way to create a new CSS rule externally.
695 * @return css_rule
696 */
697 public static function init() {
698 return new css_rule();
699 }
700
701 /**
702 * Constructs a new css rule - this can only be called from within the scope of
703 * this class or its descendants.
704 *
705 * @param type $selector
706 * @param array $styles
707 */
708 protected function __construct($selector = null, array $styles = array()) {
709 if ($selector != null) {
710 if (is_array($selector)) {
711 $this->selectors = $selector;
712 } else {
713 $this->selectors = array($selector);
714 }
715 $this->add_styles($styles);
716 }
717 }
718
719 /**
720 * Adds a new CSS selector to this rule
721 *
722 * @param css_selector $selector
723 */
724 public function add_selector(css_selector $selector) {
725 $this->selectors[] = $selector;
726 }
727
728 /**
729 * Adds a new CSS style to this rule.
730 *
731 * @param css_style|string $style
732 */
733 public function add_style($style) {
734 if (is_string($style)) {
735 $style = trim($style);
736 if (empty($style)) {
737 return;
738 }
739 $bits = explode(':', $style, 2);
740 if (count($bits) == 2) {
741 list($name, $value) = array_map('trim', $bits);
742 }
743 if (isset($name) && isset($value) && $name !== '' && $value !== '') {
744 $style = css_style::init($name, $value);
745 }
746 }
747 if ($style instanceof css_style) {
748 $name = $style->get_name();
749 if (array_key_exists($name, $this->styles)) {
750 $this->styles[$name]->set_value($style->get_value());
751 } else {
752 $this->styles[$name] = $style;
753 }
754 }
755 }
756
757 /**
758 * An easy method of adding several styles at once. Just calls add_style.
759 *
760 * @param array $styles
761 */
762 public function add_styles(array $styles) {
763 foreach ($styles as $style) {
764 $this->add_style($style);
765 }
766 }
767
768 /**
769 * Returns all of the styles as a single string that can be used in a CSS
770 * rule.
771 *
772 * @return string
773 */
774 protected function get_style_sting() {
775 $bits = array();
776 foreach ($this->styles as $style) {
777 $bits[] = $style->out();
778 }
779 return join('', $bits);
780 }
781
782 /**
783 * Returns all of the selectors as a single string that can be used in a
784 * CSS rule
785 *
786 * @return string
787 */
788 protected function get_selector_string() {
789 $selectors = array();
790 foreach ($this->selectors as $selector) {
791 $selectors[] = $selector->out();
792 }
793 return join(",\n", $selectors);
794 }
795
796 /**
797 * Returns the array of selectors
798 * @return array
799 */
800 public function get_selectors() {
801 return $this->selectors;
802 }
803
804 /**
805 * Returns the array of styles
806 * @return array
807 */
808 public function get_styles() {
809 return $this->styles;
810 }
811
812 /**
813 * Outputs this rule as a fragment of CSS
814 * @return string
815 */
816 public function out() {
817 $css = $this->get_selector_string();
818 $css .= '{';
819 $css .= $this->get_style_sting();
820 $css .= '}';
821 return $css;
822 }
823
824 /**
825 * Splits this rules into an array of CSS rules. One for each of the selectors
826 * that make up this rule.
827 *
828 * @return array(css_rule)
829 */
830 public function split_by_selector() {
831 $return = array();
832 foreach ($this->selectors as $selector) {
833 $return[] = new css_rule($selector, $this->styles);
834 }
835 return $return;
836 }
837
838 /**
839 * Splits this rule into an array of rules. One for each of the styles that
840 * make up this rule
841 *
842 * @return array(css_rule)
843 */
844 public function split_by_style() {
845 $return = array();
846 foreach ($this->styles as $style) {
847 $return[] = new css_rule($this->selectors, array($style));
848 }
849 return $return;
850 }
851
852 /**
853 * Gets a hash for the styles of this rule
854 * @return string
855 */
856 public function get_style_hash() {
857 $styles = $this->get_style_sting();
858 return md5($styles);
859 }
860
861 /**
862 * Gets a hash for the selectors of this rule
863 * @return string
864 */
865 public function get_selector_hash() {
866 $selector = $this->get_selector_string();
867 return md5($selector);
868 }
869
870 /**
871 * Gets the number of selectors that make up this rule.
872 * @return int
873 */
874 public function get_selector_count() {
875 $count = 0;
876 foreach ($this->selectors as $selector) {
877 $count += $selector->get_selector_count();
878 }
879 return $count;
880 }
881}
882
883/**
884 * A media class to organise rules by the media they apply to.
885 *
886 * @package moodlecore
887 * @copyright 2011 Sam Hemelryk
888 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
889 */
890class css_media {
891
892 /**
893 * An array of the different media types this instance applies to.
894 * @var array
895 */
896 protected $types = array();
897
898 /**
899 * An array of rules within this media instance
900 * @var array
901 */
902 protected $rules = array();
903
904 /**
905 * Initalises a new media instance
906 *
907 * @param type $for
908 */
909 public function __construct($for = 'all') {
910 $types = explode(',', $for);
911 $this->types = array_map('trim', $types);
912 }
913
914 /**
915 * Adds a new CSS rule to this media instance
916 *
917 * @param css_rule $newrule
918 */
919 public function add_rule(css_rule $newrule) {
920 foreach ($newrule->split_by_selector() as $rule) {
921 $hash = $rule->get_selector_hash();
922 if (!array_key_exists($hash, $this->rules)) {
923 $this->rules[$hash] = $rule;
924 } else {
925 $this->rules[$hash]->add_styles($rule->get_styles());
926 }
927 }
928 }
929
930 /**
931 * Returns the rules used by this
932 *
933 * @return array
934 */
935 public function get_rules() {
936 return $this->rules;
937 }
938
939 /**
940 * Organises rules by gropuing selectors based upon the styles and consolidating
941 * those selectors into single rules.
942 *
943 * @return array An array of optimised styles
944 */
945 public function organise_rules_by_selectors() {
946 $optimised = array();
947 $beforecount = count($this->rules);
948 foreach ($this->rules as $rule) {
949 $hash = $rule->get_style_hash();
950 if (!array_key_exists($hash, $optimised)) {
951 $optimised[$hash] = clone($rule);
952 } else {
953 foreach ($rule->get_selectors() as $selector) {
954 $optimised[$hash]->add_selector($selector);
955 }
956 }
957 }
958 $this->rules = $optimised;
959 $aftercount = count($this->rules);
960 return ($beforecount < $aftercount);
961 }
962
963 /**
964 * Returns the total number of rules that exist within this media set
965 *
966 * @return int
967 */
968 public function count_rules() {
969 return count($this->rules);
970 }
971
972 /**
973 * Returns the total number of selectors that exist within this media set
974 *
975 * @return int
976 */
977 public function count_selectors() {
978 $count = 0;
979 foreach ($this->rules as $rule) {
980 $count += $rule->get_selector_count();
981 }
982 return $count;
983 }
984
985 /**
986 * Returns the CSS for this media and all of its rules.
987 *
988 * @return string
989 */
990 public function out() {
991 $output = '';
992 $types = join(',', $this->types);
993 if ($types !== 'all') {
994 $output .= "\n\n/***** New media declaration *****/\n";
995 $output .= "@media {$types} {\n";
996 }
997 foreach ($this->rules as $rule) {
998 $output .= $rule->out()."\n";
999 }
1000 if ($types !== 'all') {
1001 $output .= '}';
1002 $output .= "\n/***** Media declaration end for $types *****/";
1003 }
1004 return $output;
1005 }
1006
1007 /**
1008 * Returns an array of media that this media instance applies to
1009 *
1010 * @return array
1011 */
1012 public function get_types() {
1013 return $this->types;
1014 }
1015}
1016
1017/**
1018 * An absract class to represent CSS styles
1019 *
1020 * @package moodlecore
1021 * @copyright 2011 Sam Hemelryk
1022 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1023 */
1024abstract class css_style {
1025
1026 /**
1027 * The name of the style
1028 * @var string
1029 */
1030 protected $name;
1031
1032 /**
1033 * The value for the style
1034 * @var mixed
1035 */
1036 protected $value;
1037
1038 /**
1039 * If set to true this style was defined with the !important rule.
1040 * @var bool
1041 */
1042 protected $important = false;
1043
1044 /**
1045 * Initialises a new style.
1046 *
1047 * This is the only public way to create a style to ensure they that appropriate
1048 * style class is used if it exists.
1049 *
1050 * @param type $name
1051 * @param type $value
1052 * @return css_style_generic
1053 */
1054 public static function init($name, $value) {
1055 $specificclass = 'css_style_'.preg_replace('#[^a-zA-Z0-9]+#', '', $name);
1056 if (class_exists($specificclass)) {
1057 return $specificclass::init($value);
1058 }
1059 return new css_style_generic($name, $value);
1060 }
1061
1062 /**
1063 * Creates a new style when given its name and value
1064 *
1065 * @param string $name
1066 * @param string $value
1067 */
1068 protected function __construct($name, $value) {
1069 $this->name = $name;
1070 $this->set_value($value);
1071 }
1072
1073 /**
1074 * Sets the value for the style
1075 *
1076 * @param string $value
1077 */
1078 final public function set_value($value) {
1079 $value = trim($value);
1080 $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
1081 if ($important) {
1082 $value = substr($value, 0, -(strlen($matches[1])));
1083 }
1084 if (!$this->important || $important) {
1085 $this->value = $this->clean_value($value);
1086 $this->important = $important;
1087 }
1088 }
1089
1090 /**
1091 * Returns the name for the style
1092 *
1093 * @return string
1094 */
1095 public function get_name() {
1096 return $this->name;
1097 }
1098
1099 /**
1100 * Returns the value for the style
1101 *
1102 * @return string
1103 */
1104 public function get_value() {
1105 $value = $this->value;
1106 if ($this->important) {
1107 $value .= ' !important';
1108 }
1109 return $value;
1110 }
1111
1112 /**
1113 * Returns the style ready for use in CSS
1114 *
1115 * @param string|null $value
1116 * @return string
1117 */
1118 public function out($value = null) {
1119 if ($value === null) {
1120 $value = $this->get_value();
1121 } else if ($this->important && strpos($value, '!important') === false) {
1122 $value .= ' !important';
1123 }
1124 return "{$this->name}:{$value};";
1125 }
1126
1127 /**
1128 * This can be overridden by a specific style allowing it to clean its values
1129 * consistently.
1130 *
1131 * @param mixed $value
1132 * @return mixed
1133 */
1134 protected function clean_value($value) {
1135 return $value;
1136 }
1137}
1138
1139/**
1140 * A generic CSS style class to use when a more specific class does not exist.
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_generic extends css_style {
1147 /**
1148 * Cleans incoming values for typical things that can be optimised.
1149 *
1150 * @param mixed $value
1151 * @return string
1152 */
1153 protected function clean_value($value) {
1154 if (trim($value) == '0px') {
1155 $value = 0;
1156 } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
1157 $value = '#'.strtoupper($matches[1]);
1158 }
1159 return $value;
1160 }
1161}
1162
1163/**
1164 * A colour CSS style
1165 *
1166 * @package moodlecore
1167 * @copyright 2011 Sam Hemelryk
1168 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1169 */
1170class css_style_color extends css_style {
1171 /**
1172 * Creates a new colour style
1173 *
1174 * @param mixed $value
1175 * @return css_style_color
1176 */
1177 public static function init($value) {
1178 return new css_style_color('color', $value);
1179 }
1180
1181 /**
1182 * Cleans the colour unifing it to a 6 char hash colour if possible
1183 * Doing this allows us to associate identical colours being specified in
1184 * different ways. e.g. Red, red, #F00, and #F00000
1185 *
1186 * @param mixed $value
1187 * @return string
1188 */
1189 protected function clean_value($value) {
1190 $value = trim($value);
1191 if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
1192 $value = '#'.strtoupper($matches[1]);
1193 } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
1194 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
1195 $value = '#'.strtoupper($value);
1196 } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
1197 $value = css_optimiser::$htmlcolours[strtolower($value)];
1198 }
1199 return $value;
1200 }
1201
1202 /**
1203 * Returns the colour style for use within CSS.
1204 * Will return an optimised hash colour.
1205 *
1206 * e.g #123456
1207 * #123 instead of #112233
1208 * #F00 instead of red
1209 *
1210 * @param string $overridevalue
1211 * @return string
1212 */
1213 public function out($overridevalue = null) {
1214 if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $this->value, $matches)) {
1215 $overridevalue = '#'.$matches[1].$matches[2].$matches[3];
1216 }
1217 return parent::out($overridevalue);
1218 }
1219}
1220
1221/**
1222 * A background colour style.
1223 *
1224 * Based upon the colour style.
1225 *
1226 * @package moodlecore
1227 * @copyright 2011 Sam Hemelryk
1228 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1229 */
1230class css_style_backgroundcolor extends css_style_color {
1231 /**
1232 * Creates a new background colour style
1233 *
1234 * @param mixed $value
1235 * @return css_style_backgroundcolor
1236 */
1237 public static function init($value) {
1238 return new css_style_backgroundcolor('background-color', $value);
1239 }
1240}
1241
1242/**
1243 * A border colour style
1244 *
1245 * @package moodlecore
1246 * @copyright 2011 Sam Hemelryk
1247 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1248 */
1249class css_style_bordercolor extends css_style_color {
1250 /**
1251 * Creates a new border colour style
1252 *
1253 * Based upon the colour style
1254 *
1255 * @param mixed $value
1256 * @return css_style_bordercolor
1257 */
1258 public static function init($value) {
1259 return new css_style_bordercolor('border-color', $value);
1260 }
1261}
1262
1263/**
1264 * A border style
1265 *
1266 * @package moodlecore
1267 * @copyright 2011 Sam Hemelryk
1268 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1269 */
1270class css_style_border extends css_style {
1271 /**
1272 * Created a new border style
1273 *
1274 * @param mixed $value
1275 * @return css_style_border
1276 */
1277 public static function init($value) {
1278 return new css_style_border('border', $value);
1279 }
1280}