2 // This file is part of Moodle - http://moodle.org/
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.
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.
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/>.
18 * This file contains CSS related class, and function for the CSS optimiser
20 * Please see the {@link css_optimiser} class for greater detail.
24 * @copyright 2012 Sam Hemelryk
25 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
28 // NOTE: do not verify MOODLE_INTERNAL here, this is used from themes too
31 * Stores CSS in a file at the given path.
33 * This function either succeeds or throws an exception.
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.
38 * @param bool $chunk If set to true these files will be chunked to ensure
39 * that no one file contains more than 4095 selectors.
40 * @param string $chunkurl If the CSS is be chunked then we need to know the URL
41 * to use for the chunked files.
43 function css_store_css(theme_config
$theme, $csspath, array $cssfiles, $chunk = false, $chunkurl = null) {
46 // Check if both the CSS optimiser is enabled and the theme supports it.
47 if (!empty($CFG->enablecssoptimiser
) && $theme->supportscssoptimisation
) {
48 // This is an experimental feature introduced in Moodle 2.3
49 // The CSS optimiser organises the CSS in order to reduce the overall number
50 // of rules and styles being sent to the client. It does this by collating
51 // the CSS before it is cached removing excess styles and rules and stripping
52 // out any extraneous content such as comments and empty rules.
53 $optimiser = new css_optimiser
;
55 foreach ($cssfiles as $file) {
56 $css .= file_get_contents($file)."\n";
58 $css = $theme->post_process($css);
59 $css = $optimiser->process($css);
61 // If cssoptimisestats is set then stats from the optimisation are collected
62 // and output at the beginning of the CSS
63 if (!empty($CFG->cssoptimiserstats
)) {
64 $css = $optimiser->output_stats_css().$css;
67 // This is the default behaviour.
68 // The cssoptimise setting was introduced in Moodle 2.3 and will hopefully
69 // in the future be changed from an experimental setting to the default.
70 // The css_minify_css will method will use the Minify library remove
71 // comments, additional whitespace and other minor measures to reduce the
72 // the overall CSS being sent.
73 // However it has the distinct disadvantage of having to minify the CSS
74 // before running the post process functions. Potentially things may break
75 // here if theme designers try to push things with CSS post processing.
76 $css = $theme->post_process(css_minify_css($cssfiles));
80 // Chunk the CSS if requried.
81 $css = css_chunk_by_selector_count($css, $chunkurl);
87 if (!file_exists(dirname($csspath))) {
88 @mkdir
(dirname($csspath), $CFG->directorypermissions
, true);
91 // Prevent serving of incomplete file from concurrent request,
92 // the rename() should be more atomic than fwrite().
93 ignore_user_abort(true);
97 foreach ($css as $content) {
98 if ($files > 1 && ($count+
1) !== $files) {
99 // If there is more than one file and this is not the last file.
100 $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
103 $filename = $csspath;
105 if ($fp = fopen($filename.'.tmp', 'xb')) {
106 fwrite($fp, $content);
108 rename($filename.'.tmp', $filename);
109 @chmod
($filename, $CFG->filepermissions
);
110 @unlink
($filename.'.tmp'); // just in case anything fails
114 ignore_user_abort(false);
115 if (connection_aborted()) {
121 * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
123 * @param string $css The CSS to chunk.
124 * @param string $importurl The URL to use for import statements.
125 * @param int $maxselectors The number of selectors to limit a chunk to.
126 * @param int $buffer The buffer size to use when chunking. You shouldn't need to reduce this
127 * unless you are lowering the maximum selectors.
128 * @return array An array of CSS chunks.
130 function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
131 // Check if we need to chunk this CSS file.
132 $count = substr_count($css, ',') +
substr_count($css, '{');
133 if ($count < $maxselectors) {
134 // The number of selectors is less then the max - we're fine.
139 // Split the CSS by array, making sure to save the delimiter in the process.
140 $parts = preg_split('#([,\}])#', $css, null, PREG_SPLIT_DELIM_CAPTURE + PREG_SPLIT_NO_EMPTY
);
141 // We need to chunk the array. Each delimiter is stored separately so we multiple by 2.
142 // We also subtract 100 to give us a small buffer just in case.
143 $parts = array_chunk($parts, $maxselectors * 2 - $buffer * 2);
145 $partcount = count($parts);
146 foreach ($parts as $key => $chunk) {
147 if (end($chunk) === ',') {
148 // Damn last element was a comma.
149 // Pretty much the only way to deal with this is to take the styles from the end of the
150 // comma separated chain of selectors and apply it to the last selector we have here in place
152 // Unit tests are essential for making sure this works.
155 while ($styles === false && $i < ($partcount - 1)) {
157 $nextpart = $parts[$i];
158 foreach ($nextpart as $style) {
159 if (strpos($style, '{') !== false) {
160 $styles = preg_replace('#^[^\{]+#', '', $style);
165 if ($styles === false) {
166 $styles = '/** Error chunking CSS **/';
171 array_push($chunk, $styles);
173 $css[] = join('', $chunk);
175 // The array $css now contains CSS split into perfect sized chunks.
176 // Import statements can only appear at the very top of a CSS file.
177 // Imported sheets are applied in the the order they are imported and
178 // are followed by the contents of the CSS.
179 // This is terrible for performance.
180 // It means we must put the import statements at the top of the last chunk
181 // to ensure that things are always applied in the correct order.
182 // This way the chunked files are included in the order they were chunked
183 // followed by the contents of the final chunk in the actual sheet.
185 $slashargs = strpos($importurl, '.php?') === false;
186 $parts = count($css);
187 for ($i = 0; $i < $parts - 1; $i++
) {
189 $importcss .= "@import url({$importurl}/chunk{$i});\n";
191 $importcss .= "@import url({$importurl}&chunk={$i});\n";
194 $importcss .= end($css);
195 $css[key($css)] = $importcss;
201 * Sends IE specific CSS
203 * In writing the CSS parser I have a theory that we could optimise the CSS
204 * then split it based upon the number of selectors to ensure we dont' break IE
205 * and that we include only as many sub-stylesheets as we require.
206 * Of course just a theory but may be fun to code.
208 * @param string $themename The name of the theme we are sending CSS for.
209 * @param string $rev The revision to ensure we utilise the cache.
210 * @param string $etag The revision to ensure we utilise the cache.
211 * @param bool $slasharguments
213 function css_send_ie_css($themename, $rev, $etag, $slasharguments) {
216 $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often
218 $relroot = preg_replace('|^http.?://[^/]+|', '', $CFG->wwwroot
);
220 $css = "/** Unfortunately IE6-9 does not support more than 4096 selectors in one CSS file, which means we have to use some ugly hacks :-( **/";
221 if ($slasharguments) {
222 $css .= "\n@import url($relroot/styles.php/$themename/$rev/plugins);";
223 $css .= "\n@import url($relroot/styles.php/$themename/$rev/parents);";
224 $css .= "\n@import url($relroot/styles.php/$themename/$rev/theme);";
226 $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=plugins);";
227 $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=parents);";
228 $css .= "\n@import url($relroot/styles.php?theme=$themename&rev=$rev&type=theme);";
231 header('Etag: "'.$etag.'"');
232 header('Content-Disposition: inline; filename="styles.php"');
233 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
234 header('Expires: '. gmdate('D, d M Y H:i:s', time() +
$lifetime) .' GMT');
236 header('Cache-Control: public, max-age='.$lifetime);
237 header('Accept-Ranges: none');
238 header('Content-Type: text/css; charset=utf-8');
239 header('Content-Length: '.strlen($css));
246 * Sends a cached CSS file
248 * This function sends the cached CSS file. Remember it is generated on the first
249 * request, then optimised/minified, and finally cached for serving.
251 * @param string $csspath The path to the CSS file we want to serve.
252 * @param string $etag The revision to make sure we utilise any caches.
254 function css_send_cached_css($csspath, $etag) {
255 $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often
257 header('Etag: "'.$etag.'"');
258 header('Content-Disposition: inline; filename="styles.php"');
259 header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
260 header('Expires: '. gmdate('D, d M Y H:i:s', time() +
$lifetime) .' GMT');
262 header('Cache-Control: public, max-age='.$lifetime);
263 header('Accept-Ranges: none');
264 header('Content-Type: text/css; charset=utf-8');
265 if (!min_enable_zlib_compression()) {
266 header('Content-Length: '.filesize($csspath));
274 * Sends CSS directly without caching it.
276 * This function takes a raw CSS string, optimises it if required, and then
278 * Turning both themedesignermode and CSS optimiser on at the same time is awful
279 * for performance because of the optimiser running here. However it was done so
280 * that theme designers could utilise the optimised output during development to
281 * help them optimise their CSS... not that they should write lazy CSS.
285 function css_send_uncached_css($css, $themesupportsoptimisation = true) {
288 header('Content-Disposition: inline; filename="styles_debug.php"');
289 header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
290 header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME
) .' GMT');
292 header('Accept-Ranges: none');
293 header('Content-Type: text/css; charset=utf-8');
295 if (is_array($css)) {
296 $css = implode("\n\n", $css);
305 * Send file not modified headers
306 * @param int $lastmodified
307 * @param string $etag
309 function css_send_unmodified($lastmodified, $etag) {
310 $lifetime = 60*60*24*60; // 60 days only - the revision may get incremented quite often
311 header('HTTP/1.1 304 Not Modified');
312 header('Expires: '. gmdate('D, d M Y H:i:s', time() +
$lifetime) .' GMT');
313 header('Cache-Control: public, max-age='.$lifetime);
314 header('Content-Type: text/css; charset=utf-8');
315 header('Etag: "'.$etag.'"');
317 header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
323 * Sends a 404 message about CSS not being found.
325 function css_send_css_not_found() {
326 header('HTTP/1.0 404 not found');
327 die('CSS was not found, sorry.');
331 * Uses the minify library to compress CSS.
333 * This is used if $CFG->enablecssoptimiser has been turned off. This was
334 * the original CSS optimisation library.
335 * It removes whitespace and shrinks things but does no apparent optimisation.
336 * Note the minify library is still being used for JavaScript.
338 * @param array $files An array of files to minify
339 * @return string The minified CSS
341 function css_minify_css($files) {
348 set_include_path($CFG->libdir
. '/minify/lib' . PATH_SEPARATOR
. get_include_path());
349 require_once('Minify.php');
351 if (0 === stripos(PHP_OS
, 'win')) {
352 Minify
::setDocRoot(); // IIS may need help
354 // disable all caching, we do it in moodle
355 Minify
::setCache(null, false);
358 // JSMin is not GNU GPL compatible, use the plus version instead.
359 'minifiers' => array(Minify
::TYPE_JS
=> array('JSMinPlus', 'minify')),
360 'bubbleCssImports' => false,
361 // Don't gzip content we just want text for storage
362 'encodeOutput' => false,
363 // Maximum age to cache, not used but required
364 'maxAge' => (60*60*24*20),
365 // The files to minify
367 // Turn orr URI rewriting
368 'rewriteCssUris' => false,
369 // This returns the CSS rather than echoing it for display
375 $result = Minify
::serve('Files', $options);
376 if ($result['success']) {
377 return $result['content'];
379 } catch (Exception
$e) {
380 $error = $e->getMessage();
381 $error = str_replace("\r", ' ', $error);
382 $error = str_replace("\n", ' ', $error);
385 // minification failed - try to inform the theme developer and include the non-minified version
388 /* Problem detected during theme CSS minimisation, please review the following code */
389 /* ================================================================================ */
393 foreach ($files as $cssfile) {
394 $css .= file_get_contents($cssfile)."\n";
400 * Determines if the given value is a valid CSS colour.
402 * A CSS colour can be one of the following:
403 * - Hex colour: #AA66BB
404 * - RGB colour: rgb(0-255, 0-255, 0-255)
405 * - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1)
406 * - HSL colour: hsl(0-360, 0-100%, 0-100%)
407 * - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1)
409 * Or a recognised browser colour mapping {@link css_optimiser::$htmlcolours}
411 * @param string $value The colour value to check
414 function css_is_colour($value) {
415 $value = trim($value);
417 $hex = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/';
418 $rgb = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i';
419 $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
420 $hsl = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i';
421 $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
423 if (in_array(strtolower($value), array('inherit'))) {
425 } else if (preg_match($hex, $value)) {
427 } else if (in_array(strtolower($value), array_keys(css_optimiser
::$htmlcolours))) {
429 } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
430 // It is an RGB colour
432 } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
433 // It is an RGBA colour
435 } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
436 // It is an HSL colour
438 } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
439 // It is an HSLA colour
442 // Doesn't look like a colour.
447 * Returns true is the passed value looks like a CSS width.
448 * In order to pass this test the value must be purely numerical or end with a
449 * valid CSS unit term.
451 * @param string|int $value
454 function css_is_width($value) {
455 $value = trim($value);
456 if (in_array(strtolower($value), array('auto', 'inherit'))) {
459 if ((string)$value === '0' ||
preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) {
466 * A simple sorting function to sort two array values on the number of items they contain
472 function css_sort_by_count(array $a, array $b) {
478 return ($a > $b) ?
-1 : 1;
482 * A basic CSS optimiser that strips out unwanted things and then processing the
483 * CSS organising styles and moving duplicates and useless CSS.
485 * This CSS optimiser works by reading through a CSS string one character at a
486 * time and building an object structure of the CSS.
487 * As part of that processing styles are expanded out as much as they can be to
488 * ensure we collect all mappings, at the end of the processing those styles are
489 * then combined into an optimised form to keep them as short as possible.
493 * @copyright 2012 Sam Hemelryk
494 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
496 class css_optimiser
{
499 * Used when the processor is about to start processing.
500 * Processing states. Used internally.
502 const PROCESSING_START
= 0;
505 * Used when the processor is currently processing a selector.
506 * Processing states. Used internally.
508 const PROCESSING_SELECTORS
= 0;
511 * Used when the processor is currently processing a style.
512 * Processing states. Used internally.
514 const PROCESSING_STYLES
= 1;
517 * Used when the processor is currently processing a comment.
518 * Processing states. Used internally.
520 const PROCESSING_COMMENT
= 2;
523 * Used when the processor is currently processing an @ rule.
524 * Processing states. Used internally.
526 const PROCESSING_ATRULE
= 3;
529 * The raw string length before optimisation.
530 * Stats variables set during and after processing
533 protected $rawstrlen = 0;
536 * The number of comments that were removed during optimisation.
537 * Stats variables set during and after processing
540 protected $commentsincss = 0;
543 * The number of rules in the CSS before optimisation.
544 * Stats variables set during and after processing
547 protected $rawrules = 0;
550 * The number of selectors using in CSS rules before optimisation.
551 * Stats variables set during and after processing
554 protected $rawselectors = 0;
557 * The string length after optimisation.
558 * Stats variables set during and after processing
561 protected $optimisedstrlen = 0;
564 * The number of rules after optimisation.
565 * Stats variables set during and after processing
568 protected $optimisedrules = 0;
571 * The number of selectors used in rules after optimisation.
572 * Stats variables set during and after processing
575 protected $optimisedselectors = 0;
578 * The start time of the optimisation.
579 * Stats variables set during and after processing
582 protected $timestart = 0;
585 * The end time of the optimisation.
586 * Stats variables set during and after processing
589 protected $timecomplete = 0;
592 * Will be set to any errors that may have occured during processing.
593 * This is updated only at the end of processing NOT during.
597 protected $errors = array();
600 * Processes incoming CSS optimising it and then returning it.
602 * @param string $css The raw CSS to optimise
603 * @return string The optimised CSS
605 public function process($css) {
608 // Easiest win there is
611 $this->reset_stats();
612 $this->timestart
= microtime(true);
613 $this->rawstrlen
= strlen($css);
615 // Don't try to process files with no content... it just doesn't make sense.
616 // But we should produce an error for them, an empty CSS file will lead to a
617 // useless request for those running theme designer mode.
618 if ($this->rawstrlen
=== 0) {
619 $this->errors
[] = 'Skipping file as it has no content.';
623 // First up we need to remove all line breaks - this allows us to instantly
624 // reduce our processing requirements and as we will process everything
625 // into a new structure there's really nothing lost.
626 $css = preg_replace('#\r?\n#', ' ', $css);
628 // Next remove the comments... no need to them in an optimised world and
629 // knowing they're all gone allows us to REALLY make our processing simpler
630 $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss
);
633 'all' => new css_media()
637 // Keyframes are used for CSS animation they will be processed right at the very end.
638 $keyframes = array();
640 $currentprocess = self
::PROCESSING_START
;
641 $currentrule = css_rule
::init();
642 $currentselector = css_selector
::init();
643 $inquotes = false; // ' or "
644 $inbraces = false; // {
645 $inbrackets = false; // [
646 $inparenthesis = false; // (
647 $currentmedia = $medias['all'];
648 $currentatrule = null;
649 $suspectatrule = false;
654 // Next we are going to iterate over every single character in $css.
655 // This is why we removed line breaks and comments!
656 for ($i = 0; $i < $this->rawstrlen
; $i++
) {
658 $char = substr($css, $i, 1);
659 if ($char == '@' && $buffer == '') {
660 $suspectatrule = true;
662 switch ($currentprocess) {
663 // Start processing an @ rule e.g. @media, @page, @keyframes
664 case self
::PROCESSING_ATRULE
:
669 if ($currentatrule == 'import') {
670 $imports[] = $buffer;
671 $currentprocess = self
::PROCESSING_SELECTORS
;
672 } else if ($currentatrule == 'charset') {
674 $currentprocess = self
::PROCESSING_SELECTORS
;
677 if ($currentatrule !== 'media') {
679 $currentatrule = false;
681 // continue 1: The switch processing chars
682 // continue 2: The switch processing the state
683 // continue 3: The for loop
686 if ($currentatrule == 'media' && preg_match('#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#', $buffer, $matches)) {
687 // Basic media declaration
688 $mediatypes = str_replace(' ', '', $matches[1]);
689 if (!array_key_exists($mediatypes, $medias)) {
690 $medias[$mediatypes] = new css_media($mediatypes);
692 $currentmedia = $medias[$mediatypes];
693 $currentprocess = self
::PROCESSING_SELECTORS
;
695 } else if ($currentatrule == 'media' && preg_match('#\s*@media\s*([^{]+)#', $buffer, $matches)) {
696 // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/
697 $mediatypes = $matches[1];
698 $hash = md5($mediatypes);
699 $medias[$hash] = new css_media($mediatypes);
700 $currentmedia = $medias[$hash];
701 $currentprocess = self
::PROCESSING_SELECTORS
;
703 } else if ($currentatrule == 'keyframes' && preg_match('#@((\-moz\-|\-webkit\-)?keyframes)\s*([^\s]+)#', $buffer, $matches)) {
704 // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow
705 // them to be overridden to ensure we don't mess anything up. (means we keep everything in order)
706 $keyframefor = $matches[1];
707 $keyframename = $matches[3];
708 $keyframe = new css_keyframe($keyframefor, $keyframename);
709 $keyframes[] = $keyframe;
710 $currentmedia = $keyframe;
711 $currentprocess = self
::PROCESSING_SELECTORS
;
714 // continue 1: The switch processing chars
715 // continue 2: The switch processing the state
716 // continue 3: The for loop
720 // Start processing selectors
721 case self
::PROCESSING_START
:
722 case self
::PROCESSING_SELECTORS
:
727 // continue 1: The switch processing chars
728 // continue 2: The switch processing the state
729 // continue 3: The for loop
734 // continue 1: The switch processing chars
735 // continue 2: The switch processing the state
736 // continue 3: The for loop
740 // continue 1: The switch processing chars
741 // continue 2: The switch processing the state
742 // continue 3: The for loop
745 if (!empty($buffer)) {
746 // Check for known @ rules
747 if ($suspectatrule && preg_match('#@(media|import|charset|(\-moz\-|\-webkit\-)?(keyframes))\s*#', $buffer, $matches)) {
748 $currentatrule = (!empty($matches[3]))?
$matches[3]:$matches[1];
749 $currentprocess = self
::PROCESSING_ATRULE
;
752 $currentselector->add($buffer);
756 $suspectatrule = false;
757 // continue 1: The switch processing chars
758 // continue 2: The switch processing the state
759 // continue 3: The for loop
763 // continue 1: The switch processing chars
764 // continue 2: The switch processing the state
765 // continue 3: The for loop
768 if ($buffer !== '') {
769 $currentselector->add($buffer);
771 $currentrule->add_selector($currentselector);
772 $currentselector = css_selector
::init();
773 $currentprocess = self
::PROCESSING_STYLES
;
776 // continue 1: The switch processing chars
777 // continue 2: The switch processing the state
778 // continue 3: The for loop
782 // continue 1: The switch processing chars
783 // continue 2: The switch processing the state
784 // continue 3: The for loop
787 if ($currentatrule == 'media') {
788 $currentmedia = $medias['all'];
789 $currentatrule = false;
791 } else if (strpos($currentatrule, 'keyframes') !== false) {
792 $currentmedia = $medias['all'];
793 $currentatrule = false;
796 // continue 1: The switch processing chars
797 // continue 2: The switch processing the state
798 // continue 3: The for loop
802 // continue 1: The switch processing chars
803 // continue 2: The switch processing the state
804 // continue 3: The for loop
807 $currentselector->add($buffer);
808 $currentrule->add_selector($currentselector);
809 $currentselector = css_selector
::init();
811 // continue 1: The switch processing chars
812 // continue 2: The switch processing the state
813 // continue 3: The for loop
817 // Start processing styles
818 case self
::PROCESSING_STYLES
:
819 if ($char == '"' ||
$char == "'") {
820 if ($inquotes === false) {
823 if ($inquotes === $char && $lastchar !== '\\') {
833 if ($inparenthesis) {
835 // continue 1: The switch processing chars
836 // continue 2: The switch processing the state
837 // continue 3: The for loop
840 $currentrule->add_style($buffer);
843 // continue 1: The switch processing chars
844 // continue 2: The switch processing the state
845 // continue 3: The for loop
848 $currentrule->add_style($buffer);
849 $this->rawselectors +
= $currentrule->get_selector_count();
851 $currentmedia->add_rule($currentrule);
853 $currentrule = css_rule
::init();
854 $currentprocess = self
::PROCESSING_SELECTORS
;
858 $inparenthesis = false;
859 // continue 1: The switch processing chars
860 // continue 2: The switch processing the state
861 // continue 3: The for loop
864 $inparenthesis = true;
866 // continue 1: The switch processing chars
867 // continue 2: The switch processing the state
868 // continue 3: The for loop
871 $inparenthesis = false;
873 // continue 1: The switch processing chars
874 // continue 2: The switch processing the state
875 // continue 3: The for loop
883 foreach ($medias as $media) {
884 $this->optimise($media);
886 $css = $this->produce_css($charset, $imports, $medias, $keyframes);
888 $this->timecomplete
= microtime(true);
893 * Produces CSS for the given charset, imports, media, and keyframes
894 * @param string $charset
895 * @param array $imports
896 * @param array $medias
897 * @param array $keyframes
900 protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
902 if (!empty($charset)) {
903 $imports[] = $charset;
905 if (!empty($imports)) {
906 $css .= implode("\n", $imports);
911 $cssstandard = array();
912 $csskeyframes = array();
914 // Process each media declaration individually
915 foreach ($medias as $media) {
916 // If this declaration applies to all media types
917 if (in_array('all', $media->get_types())) {
918 // Collect all rules that represet reset rules and remove them from the media object at the same time.
919 // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they
920 // can't end up out of order because of optimisation.
921 $resetrules = $media->get_reset_rules(true);
922 if (!empty($resetrules)) {
923 $cssreset[] = css_writer
::media('all', $resetrules);
926 // Get the standard cSS
927 $cssstandard[] = $media->out();
930 // Finally if there are any keyframe declarations process them now.
931 if (count($keyframes) > 0) {
932 foreach ($keyframes as $keyframe) {
933 $this->optimisedrules +
= $keyframe->count_rules();
934 $this->optimisedselectors +
= $keyframe->count_selectors();
935 if ($keyframe->has_errors()) {
936 $this->errors +
= $keyframe->get_errors();
938 $csskeyframes[] = $keyframe->out();
942 // Join it all together
943 $css .= join('', $cssreset);
944 $css .= join('', $cssstandard);
945 $css .= join('', $csskeyframes);
947 // Record the strlenght of the now optimised CSS.
948 $this->optimisedstrlen
= strlen($css);
950 // Return the now produced CSS
955 * Optimises the CSS rules within a rule collection of one form or another
957 * @param css_rule_collection $media
958 * @return void This function acts in reference
960 protected function optimise(css_rule_collection
$media) {
961 $media->organise_rules_by_selectors();
962 $this->optimisedrules +
= $media->count_rules();
963 $this->optimisedselectors +
= $media->count_selectors();
964 if ($media->has_errors()) {
965 $this->errors +
= $media->get_errors();
970 * Returns an array of stats from the last processing run
973 public function get_stats() {
975 'timestart' => $this->timestart
,
976 'timecomplete' => $this->timecomplete
,
977 'timetaken' => round($this->timecomplete
- $this->timestart
, 4),
978 'commentsincss' => $this->commentsincss
,
979 'rawstrlen' => $this->rawstrlen
,
980 'rawselectors' => $this->rawselectors
,
981 'rawrules' => $this->rawrules
,
982 'optimisedstrlen' => $this->optimisedstrlen
,
983 'optimisedrules' => $this->optimisedrules
,
984 'optimisedselectors' => $this->optimisedselectors
,
985 'improvementstrlen' => '-',
986 'improvementrules' => '-',
987 'improvementselectors' => '-',
989 // Avoid division by 0 errors by checking we have valid raw values
990 if ($this->rawstrlen
> 0) {
991 $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen
/ $this->rawstrlen
) * 100, 1).'%';
993 if ($this->rawrules
> 0) {
994 $stats['improvementrules'] = round(100 - ($this->optimisedrules
/ $this->rawrules
) * 100, 1).'%';
996 if ($this->rawselectors
> 0) {
997 $stats['improvementselectors'] = round(100 - ($this->optimisedselectors
/ $this->rawselectors
) * 100, 1).'%';
1003 * Returns true if any errors have occured during processing
1007 public function has_errors() {
1008 return !empty($this->errors
);
1012 * Returns an array of errors that have occured
1014 * @param bool $clear If set to true the errors will be cleared after being returned.
1017 public function get_errors($clear = false) {
1018 $errors = $this->errors
;
1020 // Reset the error array
1021 $this->errors
= array();
1027 * Returns any errors as a string that can be included in CSS.
1031 public function output_errors_css() {
1032 $computedcss = "/****************************************\n";
1033 $computedcss .= " *--- Errors found during processing ----\n";
1034 foreach ($this->errors
as $error) {
1035 $computedcss .= preg_replace('#^#m', '* ', $error);
1037 $computedcss .= " ****************************************/\n\n";
1038 return $computedcss;
1042 * Returns a string to display stats about the last generation within CSS output
1046 public function output_stats_css() {
1048 $computedcss = "/****************************************\n";
1049 $computedcss .= " *------- CSS Optimisation stats --------\n";
1051 if ($this->rawstrlen
=== 0) {
1052 $computedcss .= " File not processed as it has no content /\n\n";
1053 $computedcss .= " ****************************************/\n\n";
1054 return $computedcss;
1055 } else if ($this->rawrules
=== 0) {
1056 $computedcss .= " File contained no rules to be processed /\n\n";
1057 $computedcss .= " ****************************************/\n\n";
1058 return $computedcss;
1061 $stats = $this->get_stats();
1063 $computedcss .= " * ".date('r')."\n";
1064 $computedcss .= " * {$stats['commentsincss']} \t comments removed\n";
1065 $computedcss .= " * Optimisation took {$stats['timetaken']} seconds\n";
1066 $computedcss .= " *--------------- before ----------------\n";
1067 $computedcss .= " * {$stats['rawstrlen']} \t chars read in\n";
1068 $computedcss .= " * {$stats['rawrules']} \t rules read in\n";
1069 $computedcss .= " * {$stats['rawselectors']} \t total selectors\n";
1070 $computedcss .= " *---------------- after ----------------\n";
1071 $computedcss .= " * {$stats['optimisedstrlen']} \t chars once optimized\n";
1072 $computedcss .= " * {$stats['optimisedrules']} \t optimized rules\n";
1073 $computedcss .= " * {$stats['optimisedselectors']} \t total selectors once optimized\n";
1074 $computedcss .= " *---------------- stats ----------------\n";
1075 $computedcss .= " * {$stats['improvementstrlen']} \t reduction in chars\n";
1076 $computedcss .= " * {$stats['improvementrules']} \t reduction in rules\n";
1077 $computedcss .= " * {$stats['improvementselectors']} \t reduction in selectors\n";
1078 $computedcss .= " ****************************************/\n\n";
1080 return $computedcss;
1084 * Resets the stats ready for another fresh processing
1086 public function reset_stats() {
1087 $this->commentsincss
= 0;
1088 $this->optimisedrules
= 0;
1089 $this->optimisedselectors
= 0;
1090 $this->optimisedstrlen
= 0;
1091 $this->rawrules
= 0;
1092 $this->rawselectors
= 0;
1093 $this->rawstrlen
= 0;
1094 $this->timecomplete
= 0;
1095 $this->timestart
= 0;
1099 * An array of the common HTML colours that are supported by most browsers.
1101 * This reference table is used to allow us to unify colours, and will aid
1102 * us in identifying buggy CSS using unsupported colours.
1107 public static $htmlcolours = array(
1108 'aliceblue' => '#F0F8FF',
1109 'antiquewhite' => '#FAEBD7',
1110 'aqua' => '#00FFFF',
1111 'aquamarine' => '#7FFFD4',
1112 'azure' => '#F0FFFF',
1113 'beige' => '#F5F5DC',
1114 'bisque' => '#FFE4C4',
1115 'black' => '#000000',
1116 'blanchedalmond' => '#FFEBCD',
1117 'blue' => '#0000FF',
1118 'blueviolet' => '#8A2BE2',
1119 'brown' => '#A52A2A',
1120 'burlywood' => '#DEB887',
1121 'cadetblue' => '#5F9EA0',
1122 'chartreuse' => '#7FFF00',
1123 'chocolate' => '#D2691E',
1124 'coral' => '#FF7F50',
1125 'cornflowerblue' => '#6495ED',
1126 'cornsilk' => '#FFF8DC',
1127 'crimson' => '#DC143C',
1128 'cyan' => '#00FFFF',
1129 'darkblue' => '#00008B',
1130 'darkcyan' => '#008B8B',
1131 'darkgoldenrod' => '#B8860B',
1132 'darkgray' => '#A9A9A9',
1133 'darkgrey' => '#A9A9A9',
1134 'darkgreen' => '#006400',
1135 'darkKhaki' => '#BDB76B',
1136 'darkmagenta' => '#8B008B',
1137 'darkolivegreen' => '#556B2F',
1138 'arkorange' => '#FF8C00',
1139 'darkorchid' => '#9932CC',
1140 'darkred' => '#8B0000',
1141 'darksalmon' => '#E9967A',
1142 'darkseagreen' => '#8FBC8F',
1143 'darkslateblue' => '#483D8B',
1144 'darkslategray' => '#2F4F4F',
1145 'darkslategrey' => '#2F4F4F',
1146 'darkturquoise' => '#00CED1',
1147 'darkviolet' => '#9400D3',
1148 'deeppink' => '#FF1493',
1149 'deepskyblue' => '#00BFFF',
1150 'dimgray' => '#696969',
1151 'dimgrey' => '#696969',
1152 'dodgerblue' => '#1E90FF',
1153 'firebrick' => '#B22222',
1154 'floralwhite' => '#FFFAF0',
1155 'forestgreen' => '#228B22',
1156 'fuchsia' => '#FF00FF',
1157 'gainsboro' => '#DCDCDC',
1158 'ghostwhite' => '#F8F8FF',
1159 'gold' => '#FFD700',
1160 'goldenrod' => '#DAA520',
1161 'gray' => '#808080',
1162 'grey' => '#808080',
1163 'green' => '#008000',
1164 'greenyellow' => '#ADFF2F',
1165 'honeydew' => '#F0FFF0',
1166 'hotpink' => '#FF69B4',
1167 'indianred ' => '#CD5C5C',
1168 'indigo ' => '#4B0082',
1169 'ivory' => '#FFFFF0',
1170 'khaki' => '#F0E68C',
1171 'lavender' => '#E6E6FA',
1172 'lavenderblush' => '#FFF0F5',
1173 'lawngreen' => '#7CFC00',
1174 'lemonchiffon' => '#FFFACD',
1175 'lightblue' => '#ADD8E6',
1176 'lightcoral' => '#F08080',
1177 'lightcyan' => '#E0FFFF',
1178 'lightgoldenrodyellow' => '#FAFAD2',
1179 'lightgray' => '#D3D3D3',
1180 'lightgrey' => '#D3D3D3',
1181 'lightgreen' => '#90EE90',
1182 'lightpink' => '#FFB6C1',
1183 'lightsalmon' => '#FFA07A',
1184 'lightseagreen' => '#20B2AA',
1185 'lightskyblue' => '#87CEFA',
1186 'lightslategray' => '#778899',
1187 'lightslategrey' => '#778899',
1188 'lightsteelblue' => '#B0C4DE',
1189 'lightyellow' => '#FFFFE0',
1190 'lime' => '#00FF00',
1191 'limegreen' => '#32CD32',
1192 'linen' => '#FAF0E6',
1193 'magenta' => '#FF00FF',
1194 'maroon' => '#800000',
1195 'mediumaquamarine' => '#66CDAA',
1196 'mediumblue' => '#0000CD',
1197 'mediumorchid' => '#BA55D3',
1198 'mediumpurple' => '#9370D8',
1199 'mediumseagreen' => '#3CB371',
1200 'mediumslateblue' => '#7B68EE',
1201 'mediumspringgreen' => '#00FA9A',
1202 'mediumturquoise' => '#48D1CC',
1203 'mediumvioletred' => '#C71585',
1204 'midnightblue' => '#191970',
1205 'mintcream' => '#F5FFFA',
1206 'mistyrose' => '#FFE4E1',
1207 'moccasin' => '#FFE4B5',
1208 'navajowhite' => '#FFDEAD',
1209 'navy' => '#000080',
1210 'oldlace' => '#FDF5E6',
1211 'olive' => '#808000',
1212 'olivedrab' => '#6B8E23',
1213 'orange' => '#FFA500',
1214 'orangered' => '#FF4500',
1215 'orchid' => '#DA70D6',
1216 'palegoldenrod' => '#EEE8AA',
1217 'palegreen' => '#98FB98',
1218 'paleturquoise' => '#AFEEEE',
1219 'palevioletred' => '#D87093',
1220 'papayawhip' => '#FFEFD5',
1221 'peachpuff' => '#FFDAB9',
1222 'peru' => '#CD853F',
1223 'pink' => '#FFC0CB',
1224 'plum' => '#DDA0DD',
1225 'powderblue' => '#B0E0E6',
1226 'purple' => '#800080',
1228 'rosybrown' => '#BC8F8F',
1229 'royalblue' => '#4169E1',
1230 'saddlebrown' => '#8B4513',
1231 'salmon' => '#FA8072',
1232 'sandybrown' => '#F4A460',
1233 'seagreen' => '#2E8B57',
1234 'seashell' => '#FFF5EE',
1235 'sienna' => '#A0522D',
1236 'silver' => '#C0C0C0',
1237 'skyblue' => '#87CEEB',
1238 'slateblue' => '#6A5ACD',
1239 'slategray' => '#708090',
1240 'slategrey' => '#708090',
1241 'snow' => '#FFFAFA',
1242 'springgreen' => '#00FF7F',
1243 'steelblue' => '#4682B4',
1245 'teal' => '#008080',
1246 'thistle' => '#D8BFD8',
1247 'tomato' => '#FF6347',
1248 'transparent' => 'transparent',
1249 'turquoise' => '#40E0D0',
1250 'violet' => '#EE82EE',
1251 'wheat' => '#F5DEB3',
1252 'white' => '#FFFFFF',
1253 'whitesmoke' => '#F5F5F5',
1254 'yellow' => '#FFFF00',
1255 'yellowgreen' => '#9ACD32'
1260 * Used to prepare CSS strings
1264 * @copyright 2012 Sam Hemelryk
1265 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1267 abstract class css_writer
{
1270 * The current indent level
1273 protected static $indent = 0;
1276 * Returns true if the output should still maintain minimum formatting.
1279 protected static function is_pretty() {
1281 return (!empty($CFG->cssoptimiserpretty
));
1285 * Returns the indenting char to use for indenting things nicely.
1288 protected static function get_indent() {
1289 if (self
::is_pretty()) {
1290 return str_repeat(" ", self
::$indent);
1296 * Increases the current indent
1298 protected static function increase_indent() {
1303 * Decreases the current indent
1305 protected static function decrease_indent() {
1310 * Returns the string to use as a separator
1313 protected static function get_separator() {
1314 return (self
::is_pretty())?
"\n":' ';
1318 * Returns CSS for media
1320 * @param string $typestring
1321 * @param array $rules An array of css_rule objects
1324 public static function media($typestring, array &$rules) {
1325 $nl = self
::get_separator();
1328 if ($typestring !== 'all') {
1329 $output .= "\n@media {$typestring} {".$nl;
1330 self
::increase_indent();
1332 foreach ($rules as $rule) {
1333 $output .= $rule->out().$nl;
1335 if ($typestring !== 'all') {
1336 self
::decrease_indent();
1343 * Returns CSS for a keyframe
1345 * @param string $for The desired declaration. e.g. keyframes, -moz-keyframes, -webkit-keyframes
1346 * @param string $name The name for the keyframe
1347 * @param array $rules An array of rules belonging to the keyframe
1350 public static function keyframe($for, $name, array &$rules) {
1351 $nl = self
::get_separator();
1353 $output = "\n@{$for} {$name} {";
1354 foreach ($rules as $rule) {
1355 $output .= $rule->out();
1362 * Returns CSS for a rule
1364 * @param string $selector
1365 * @param string $styles
1368 public static function rule($selector, $styles) {
1369 $css = self
::get_indent()."{$selector}{{$styles}}";
1374 * Returns CSS for the selectors of a rule
1376 * @param array $selectors Array of css_selector objects
1379 public static function selectors(array $selectors) {
1380 $nl = self
::get_separator();
1381 $selectorstrings = array();
1382 foreach ($selectors as $selector) {
1383 $selectorstrings[] = $selector->out();
1385 return join(','.$nl, $selectorstrings);
1389 * Returns a selector given the components that make it up.
1391 * @param array $components
1394 public static function selector(array $components) {
1395 return trim(join(' ', $components));
1399 * Returns a CSS string for the provided styles
1401 * @param array $styles Array of css_style objects
1404 public static function styles(array $styles) {
1406 foreach ($styles as $style) {
1407 // Check if the style is an array. If it is then we are outputing an advanced style.
1408 // An advanced style is a style with one or more values, and can occur in situations like background-image
1409 // where browse specific values are being used.
1410 if (is_array($style)) {
1411 foreach ($style as $advstyle) {
1412 $bits[] = $advstyle->out();
1416 $bits[] = $style->out();
1418 return join('', $bits);
1422 * Returns a style CSS
1424 * @param string $name
1425 * @param string $value
1426 * @param bool $important
1429 public static function style($name, $value, $important = false) {
1430 $value = trim($value);
1431 if ($important && strpos($value, '!important') === false) {
1432 $value .= ' !important';
1434 return "{$name}:{$value};";
1439 * A structure to represent a CSS selector.
1441 * The selector is the classes, id, elements, and psuedo bits that make up a CSS
1446 * @copyright 2012 Sam Hemelryk
1447 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1449 class css_selector
{
1452 * An array of selector bits
1455 protected $selectors = array();
1458 * The number of selectors.
1461 protected $count = 0;
1464 * Is null if there are no selectors, true if all selectors are basic and false otherwise.
1465 * A basic selector is one that consists of just the element type. e.g. div, span, td, a
1468 protected $isbasic = null;
1471 * Initialises a new CSS selector
1472 * @return css_selector
1474 public static function init() {
1475 return new css_selector();
1479 * CSS selectors can only be created through the init method above.
1481 protected function __construct() {}
1484 * Adds a selector to the end of the current selector
1485 * @param string $selector
1487 public function add($selector) {
1488 $selector = trim($selector);
1490 $count +
= preg_match_all('/(\.|#)/', $selector, $matchesarray);
1491 if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
1494 // If its already false then no need to continue, its not basic
1495 if ($this->isbasic
!== false) {
1496 // If theres more than one part making up this selector its not basic
1498 $this->isbasic
= false;
1500 // Check whether it is a basic element (a-z+) with possible psuedo selector
1501 $this->isbasic
= (bool)preg_match('#^[a-z]+(:[a-zA-Z]+)?$#', $selector);
1504 $this->count
= $count;
1505 $this->selectors
[] = $selector;
1508 * Returns the number of individual components that make up this selector
1511 public function get_selector_count() {
1512 return $this->count
;
1516 * Returns the selector for use in a CSS rule
1519 public function out() {
1520 return css_writer
::selector($this->selectors
);
1524 * Returns true is all of the selectors act only upon basic elements (no classes/ids)
1527 public function is_basic() {
1528 return ($this->isbasic
=== true);
1533 * A structure to represent a CSS rule.
1537 * @copyright 2012 Sam Hemelryk
1538 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1543 * An array of CSS selectors {@link css_selector}
1546 protected $selectors = array();
1549 * An array of CSS styles {@link css_style}
1552 protected $styles = array();
1555 * Created a new CSS rule. This is the only way to create a new CSS rule externally.
1558 public static function init() {
1559 return new css_rule();
1563 * Constructs a new css rule.
1565 * @param string $selector The selector or array of selectors that make up this rule.
1566 * @param array $styles An array of styles that belong to this rule.
1568 protected function __construct($selector = null, array $styles = array()) {
1569 if ($selector != null) {
1570 if (is_array($selector)) {
1571 $this->selectors
= $selector;
1573 $this->selectors
= array($selector);
1575 $this->add_styles($styles);
1580 * Adds a new CSS selector to this rule
1582 * e.g. $rule->add_selector('.one #two.two');
1584 * @param css_selector $selector Adds a CSS selector to this rule.
1586 public function add_selector(css_selector
$selector) {
1587 $this->selectors
[] = $selector;
1591 * Adds a new CSS style to this rule.
1593 * @param css_style|string $style Adds a new style to this rule
1595 public function add_style($style) {
1596 if (is_string($style)) {
1597 $style = trim($style);
1598 if (empty($style)) {
1601 $bits = explode(':', $style, 2);
1602 if (count($bits) == 2) {
1603 list($name, $value) = array_map('trim', $bits);
1605 if (isset($name) && isset($value) && $name !== '' && $value !== '') {
1606 $style = css_style
::init_automatic($name, $value);
1608 } else if ($style instanceof css_style
) {
1609 // Clone the style as it may be coming from another rule and we don't
1610 // want references as it will likely be overwritten by proceeding
1612 $style = clone($style);
1614 if ($style instanceof css_style
) {
1615 $name = $style->get_name();
1616 $exists = array_key_exists($name, $this->styles
);
1617 // We need to find out if the current style support multiple values, or whether the style
1618 // is already set up to record multiple values. This can happen with background images which can have single
1619 // and multiple values.
1620 if ($style->allows_multiple_values() ||
($exists && is_array($this->styles
[$name]))) {
1622 $this->styles
[$name] = array();
1623 } else if ($this->styles
[$name] instanceof css_style
) {
1624 $this->styles
[$name] = array($this->styles
[$name]);
1626 $this->styles
[$name][] = $style;
1627 } else if ($exists) {
1628 $this->styles
[$name]->set_value($style->get_value());
1630 $this->styles
[$name] = $style;
1632 } else if (is_array($style)) {
1633 // We probably shouldn't worry about processing styles here but to
1634 // be truthful it doesn't hurt.
1635 foreach ($style as $astyle) {
1636 $this->add_style($astyle);
1642 * An easy method of adding several styles at once. Just calls add_style.
1644 * This method simply iterates over the array and calls {@link css_rule::add_style()}
1647 * @param array $styles Adds an array of styles
1649 public function add_styles(array $styles) {
1650 foreach ($styles as $style) {
1651 $this->add_style($style);
1656 * Returns the array of selectors
1660 public function get_selectors() {
1661 return $this->selectors
;
1665 * Returns the array of styles
1669 public function get_styles() {
1670 return $this->styles
;
1674 * Outputs this rule as a fragment of CSS
1678 public function out() {
1679 $selectors = css_writer
::selectors($this->selectors
);
1680 $styles = css_writer
::styles($this->get_consolidated_styles());
1681 return css_writer
::rule($selectors, $styles);
1685 * Consolidates all styles associated with this rule
1687 * @return array An array of consolidated styles
1689 public function get_consolidated_styles() {
1690 $organisedstyles = array();
1691 $finalstyles = array();
1692 $consolidate = array();
1693 $advancedstyles = array();
1694 foreach ($this->styles
as $style) {
1695 // If the style is an array then we are processing an advanced style. An advanced style is a style that can have
1696 // one or more values. Background-image is one such example as it can have browser specific styles.
1697 if (is_array($style)) {
1700 foreach ($style as $advstyle) {
1702 $advancedstyles[$key] = $advstyle;
1703 if (!$advstyle->allows_multiple_values()) {
1704 if (!is_null($single)) {
1705 unset($advancedstyles[$single]);
1710 if (!is_null($single)) {
1711 $style = $advancedstyles[$single];
1713 $consolidatetoclass = $style->consolidate_to();
1714 if (($style->is_valid() ||
$style->is_special_empty_value()) && !empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) {
1715 $class = 'css_style_'.$consolidatetoclass;
1716 if (!array_key_exists($class, $consolidate)) {
1717 $consolidate[$class] = array();
1718 $organisedstyles[$class] = true;
1720 $consolidate[$class][] = $style;
1721 unset($advancedstyles[$single]);
1727 $consolidatetoclass = $style->consolidate_to();
1728 if (($style->is_valid() ||
$style->is_special_empty_value()) && !empty($consolidatetoclass) && class_exists('css_style_'.$consolidatetoclass)) {
1729 $class = 'css_style_'.$consolidatetoclass;
1730 if (!array_key_exists($class, $consolidate)) {
1731 $consolidate[$class] = array();
1732 $organisedstyles[$class] = true;
1734 $consolidate[$class][] = $style;
1736 $organisedstyles[$style->get_name()] = $style;
1740 foreach ($consolidate as $class => $styles) {
1741 $organisedstyles[$class] = $class::consolidate($styles);
1744 foreach ($organisedstyles as $style) {
1745 if (is_array($style)) {
1746 foreach ($style as $s) {
1747 $finalstyles[] = $s;
1750 $finalstyles[] = $style;
1753 $finalstyles = array_merge($finalstyles, $advancedstyles);
1754 return $finalstyles;
1758 * Splits this rules into an array of CSS rules. One for each of the selectors
1759 * that make up this rule.
1761 * @return array(css_rule)
1763 public function split_by_selector() {
1765 foreach ($this->selectors
as $selector) {
1766 $return[] = new css_rule($selector, $this->styles
);
1772 * Splits this rule into an array of rules. One for each of the styles that
1775 * @return array Array of css_rule objects
1777 public function split_by_style() {
1779 foreach ($this->styles
as $style) {
1780 if (is_array($style)) {
1781 $return[] = new css_rule($this->selectors
, $style);
1784 $return[] = new css_rule($this->selectors
, array($style));
1790 * Gets a hash for the styles of this rule
1794 public function get_style_hash() {
1795 return md5(css_writer
::styles($this->styles
));
1799 * Gets a hash for the selectors of this rule
1803 public function get_selector_hash() {
1804 return md5(css_writer
::selectors($this->selectors
));
1808 * Gets the number of selectors that make up this rule.
1812 public function get_selector_count() {
1814 foreach ($this->selectors
as $selector) {
1815 $count +
= $selector->get_selector_count();
1821 * Returns true if there are any errors with this rule.
1825 public function has_errors() {
1826 foreach ($this->styles
as $style) {
1827 if (is_array($style)) {
1828 foreach ($style as $advstyle) {
1829 if ($advstyle->has_error()) {
1835 if ($style->has_error()) {
1843 * Returns the error strings that were recorded when processing this rule.
1845 * Before calling this function you should first call {@link css_rule::has_errors()}
1846 * to make sure there are errors (hopefully there arn't).
1850 public function get_error_string() {
1851 $css = $this->out();
1853 foreach ($this->styles
as $style) {
1854 if (is_array($style)) {
1855 foreach ($style as $s) {
1856 if ($style instanceof css_style
&& $style->has_error()) {
1857 $errors[] = " * ".$style->get_last_error();
1860 } else if ($style instanceof css_style
&& $style->has_error()) {
1861 $errors[] = " * ".$style->get_last_error();
1864 return $css." has the following errors:\n".join("\n", $errors);
1868 * Returns true if this rule could be considered a reset rule.
1870 * A reset rule is a rule that acts upon an HTML element and does not include any other parts to its selector.
1874 public function is_reset_rule() {
1875 foreach ($this->selectors
as $selector) {
1876 if (!$selector->is_basic()) {
1885 * An abstract CSS rule collection class.
1887 * This class is extended by things such as media and keyframe declaration. They are declarations that
1888 * group rules together for a purpose.
1889 * When no declaration is specified rules accumulate into @media all.
1893 * @copyright 2012 Sam Hemelryk
1894 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1896 abstract class css_rule_collection
{
1898 * An array of rules within this collection instance
1901 protected $rules = array();
1904 * The collection must be able to print itself.
1906 abstract public function out();
1909 * Adds a new CSS rule to this collection instance
1911 * @param css_rule $newrule
1913 public function add_rule(css_rule
$newrule) {
1914 foreach ($newrule->split_by_selector() as $rule) {
1915 $hash = $rule->get_selector_hash();
1916 if (!array_key_exists($hash, $this->rules
)) {
1917 $this->rules
[$hash] = $rule;
1919 $this->rules
[$hash]->add_styles($rule->get_styles());
1925 * Returns the rules used by this collection
1929 public function get_rules() {
1930 return $this->rules
;
1934 * Organises rules by gropuing selectors based upon the styles and consolidating
1935 * those selectors into single rules.
1937 * @return bool True if the CSS was optimised by this method
1939 public function organise_rules_by_selectors() {
1940 $optimised = array();
1941 $beforecount = count($this->rules
);
1944 foreach ($this->rules
as $rule) {
1945 $hash = $rule->get_style_hash();
1946 if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) {
1947 foreach ($rule->get_selectors() as $selector) {
1948 $lastrule->add_selector($selector);
1952 $lastrule = clone($rule);
1954 $optimised[] = $lastrule;
1956 $this->rules
= array();
1957 foreach ($optimised as $optimised) {
1958 $this->rules
[$optimised->get_selector_hash()] = $optimised;
1960 $aftercount = count($this->rules
);
1961 return ($beforecount < $aftercount);
1965 * Returns the total number of rules that exist within this collection
1969 public function count_rules() {
1970 return count($this->rules
);
1974 * Returns the total number of selectors that exist within this collection
1978 public function count_selectors() {
1980 foreach ($this->rules
as $rule) {
1981 $count +
= $rule->get_selector_count();
1987 * Returns true if the collection has any rules that have errors
1991 public function has_errors() {
1992 foreach ($this->rules
as $rule) {
1993 if ($rule->has_errors()) {
2001 * Returns any errors that have happened within rules in this collection.
2005 public function get_errors() {
2007 foreach ($this->rules
as $rule) {
2008 if ($rule->has_errors()) {
2009 $errors[] = $rule->get_error_string();
2017 * A media class to organise rules by the media they apply to.
2021 * @copyright 2012 Sam Hemelryk
2022 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2024 class css_media
extends css_rule_collection
{
2027 * An array of the different media types this instance applies to.
2030 protected $types = array();
2033 * Initalises a new media instance
2035 * @param string $for The media that the contained rules are destined for.
2037 public function __construct($for = 'all') {
2038 $types = explode(',', $for);
2039 $this->types
= array_map('trim', $types);
2043 * Returns the CSS for this media and all of its rules.
2047 public function out() {
2048 return css_writer
::media(join(',', $this->types
), $this->rules
);
2052 * Returns an array of media that this media instance applies to
2056 public function get_types() {
2057 return $this->types
;
2061 * Returns all of the reset rules known by this media set.
2062 * @param bool $remove If set to true reset rules will be removed before being returned.
2065 public function get_reset_rules($remove = false) {
2066 $resetrules = array();
2067 foreach ($this->rules
as $key => $rule) {
2068 if ($rule->is_reset_rule()) {
2069 $resetrules[] = clone $rule;
2071 unset($this->rules
[$key]);
2080 * A media class to organise rules by the media they apply to.
2084 * @copyright 2012 Sam Hemelryk
2085 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2087 class css_keyframe
extends css_rule_collection
{
2089 /** @var string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes */
2092 /** @var string $name The name for the keyframes */
2095 * Constructs a new keyframe
2097 * @param string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2098 * @param string $name The name for the keyframes
2100 public function __construct($for, $name) {
2102 $this->name
= $name;
2105 * Returns the directive of this keyframe
2107 * e.g. keyframes, -moz-keyframes, -webkit-keyframes
2110 public function get_for() {
2114 * Returns the name of this keyframe
2117 public function get_name() {
2121 * Returns the CSS for this collection of keyframes and all of its rules.
2125 public function out() {
2126 return css_writer
::keyframe($this->for, $this->name
, $this->rules
);
2131 * An absract class to represent CSS styles
2135 * @copyright 2012 Sam Hemelryk
2136 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2138 abstract class css_style
{
2140 /** Constant used for recongise a special empty value in a CSS style */
2141 const NULL_VALUE
= '@@$NULL$@@';
2144 * The name of the style
2150 * The value for the style
2156 * If set to true this style was defined with the !important rule.
2157 * Only trolls use !important.
2158 * Don't hide under bridges.. its not good for your skin. Do the proper thing
2159 * and fix the issue don't just force a fix that will undoubtedly one day
2160 * lead to further frustration.
2163 protected $important = false;
2166 * Gets set to true if this style has an error
2169 protected $error = false;
2172 * The last error message that occured
2175 protected $errormessage = null;
2178 * Initialises a new style.
2180 * This is the only public way to create a style to ensure they that appropriate
2181 * style class is used if it exists.
2183 * @param string $name The name of the style.
2184 * @param string $value The value of the style.
2185 * @return css_style_generic
2187 public static function init_automatic($name, $value) {
2188 $specificclass = 'css_style_'.preg_replace('#[^a-zA-Z0-9]+#', '', $name);
2189 if (class_exists($specificclass)) {
2190 return $specificclass::init($value);
2192 return new css_style_generic($name, $value);
2196 * Creates a new style when given its name and value
2198 * @param string $name The name of the style.
2199 * @param string $value The value of the style.
2201 protected function __construct($name, $value) {
2202 $this->name
= $name;
2203 $this->set_value($value);
2207 * Sets the value for the style
2209 * @param string $value
2211 final public function set_value($value) {
2212 $value = trim($value);
2213 $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
2215 $value = substr($value, 0, -(strlen($matches[1])));
2216 $value = rtrim($value);
2218 if (!$this->important ||
$important) {
2219 $this->value
= $this->clean_value($value);
2220 $this->important
= $important;
2222 if (!$this->is_valid()) {
2223 $this->set_error('Invalid value for '.$this->name
);
2228 * Returns true if the value associated with this style is valid
2232 public function is_valid() {
2237 * Returns the name for the style
2241 public function get_name() {
2246 * Returns the value for the style
2248 * @param bool $includeimportant If set to true and the rule is important !important postfix will be used.
2251 public function get_value($includeimportant = true) {
2252 $value = $this->value
;
2253 if ($includeimportant && $this->important
) {
2254 $value .= ' !important';
2260 * Returns the style ready for use in CSS
2262 * @param string|null $value A value to use to override the value for this style.
2265 public function out($value = null) {
2266 if (is_null($value)) {
2267 $value = $this->get_value();
2269 return css_writer
::style($this->name
, $value, $this->important
);
2273 * This can be overridden by a specific style allowing it to clean its values
2276 * @param mixed $value
2279 protected function clean_value($value) {
2284 * If this particular style can be consolidated into another style this function
2285 * should return the style that it can be consolidated into.
2287 * @return string|null
2289 public function consolidate_to() {
2294 * Sets the last error message.
2296 * @param string $message
2298 protected function set_error($message) {
2299 $this->error
= true;
2300 $this->errormessage
= $message;
2304 * Returns true if an error has occured
2308 public function has_error() {
2309 return $this->error
;
2313 * Returns the last error that occured or null if no errors have happened.
2317 public function get_last_error() {
2318 return $this->errormessage
;
2322 * Returns true if the value for this style is the special null value.
2324 * This should only be overriden in circumstances where a shorthand style can lead
2325 * to move explicit styles being overwritten. Not a common place occurenace.
2328 * This occurs if the shorthand background property was used but no proper value
2329 * was specified for this style.
2330 * This leads to a null value being used unless otherwise overridden.
2334 public function is_special_empty_value() {
2339 * Returns true if this style permits multiple values.
2341 * This occurs for styles such as background image that can have browser specific values that need to be maintained because
2342 * of course we don't know what browser the user is using, and optimisation occurs before caching.
2343 * Thus we must always server all values we encounter in the order we encounter them for when this is set to true.
2345 * @return boolean False by default, true if the style supports muliple values.
2347 public function allows_multiple_values() {
2352 * Returns true if this style was marked important.
2355 public function is_important() {
2356 return !empty($this->important
);
2360 * Sets the important flag for this style and its current value.
2361 * @param bool $important
2363 public function set_important($important = true) {
2364 $this->important
= (bool) $important;
2369 * A generic CSS style class to use when a more specific class does not exist.
2373 * @copyright 2012 Sam Hemelryk
2374 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2376 class css_style_generic
extends css_style
{
2379 * Cleans incoming values for typical things that can be optimised.
2381 * @param mixed $value Cleans the provided value optimising it if possible
2384 protected function clean_value($value) {
2385 if (trim($value) == '0px') {
2387 } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
2388 $value = '#'.strtoupper($matches[1]);
2395 * A colour CSS style
2399 * @copyright 2012 Sam Hemelryk
2400 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2402 class css_style_color
extends css_style
{
2405 * Creates a new colour style
2407 * @param mixed $value Initialises a new colour style
2408 * @return css_style_color
2410 public static function init($value) {
2411 return new css_style_color('color', $value);
2415 * Cleans the colour unifing it to a 6 char hash colour if possible
2416 * Doing this allows us to associate identical colours being specified in
2417 * different ways. e.g. Red, red, #F00, and #F00000
2419 * @param mixed $value Cleans the provided value optimising it if possible
2422 protected function clean_value($value) {
2423 $value = trim($value);
2424 if (css_is_colour($value)) {
2425 if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
2426 $value = '#'.strtoupper($matches[1]);
2427 } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
2428 $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
2429 $value = '#'.strtoupper($value);
2430 } else if (array_key_exists(strtolower($value), css_optimiser
::$htmlcolours)) {
2431 $value = css_optimiser
::$htmlcolours[strtolower($value)];
2438 * Returns the colour style for use within CSS.
2439 * Will return an optimised hash colour.
2442 * #123 instead of #112233
2443 * #F00 instead of red
2445 * @param string $overridevalue If provided then this value will be used instead
2446 * of the styles current value.
2449 public function out($overridevalue = null) {
2450 if ($overridevalue === null) {
2451 $overridevalue = $this->value
;
2453 return parent
::out(self
::shrink_value($overridevalue));
2457 * Shrinks the colour value is possible.
2459 * @param string $value Shrinks the current value to an optimial form if possible
2462 public static function shrink_value($value) {
2463 if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) {
2464 return '#'.$matches[1].$matches[2].$matches[3];
2470 * Returns true if the value is a valid colour.
2474 public function is_valid() {
2475 return css_is_colour($this->value
);
2484 * @copyright 2012 Sam Hemelryk
2485 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2487 class css_style_width
extends css_style
{
2490 * Checks if the width is valid
2493 public function is_valid() {
2494 return css_is_width($this->value
);
2498 * Cleans the provided value
2500 * @param mixed $value Cleans the provided value optimising it if possible
2503 protected function clean_value($value) {
2504 if (!css_is_width($value)) {
2505 // Note we don't actually change the value to something valid. That
2506 // would be bad for futureproofing.
2507 $this->set_error('Invalid width specified for '.$this->name
);
2508 } else if (preg_match('#^0\D+$#', $value)) {
2511 return trim($value);
2515 * Initialises a new width style
2517 * @param mixed $value The value this style has
2518 * @return css_style_width
2520 public static function init($value) {
2521 return new css_style_width('width', $value);
2530 * @copyright 2012 Sam Hemelryk
2531 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2533 class css_style_margin
extends css_style_width
{
2536 * Initialises a margin style.
2538 * In this case we split the margin into several other margin styles so that
2539 * we can properly condense overrides and then reconsolidate them later into
2542 * @param string $value The value the style has
2543 * @return array An array of margin values that can later be consolidated
2545 public static function init($value) {
2547 if (strpos($value, '!important') !== false) {
2548 $important = ' !important';
2549 $value = str_replace('!important', '', $value);
2552 $value = preg_replace('#\s+#', ' ', trim($value));
2553 $bits = explode(' ', $value, 4);
2555 $top = $right = $bottom = $left = null;
2556 if (count($bits) > 0) {
2557 $top = $right = $bottom = $left = array_shift($bits);
2559 if (count($bits) > 0) {
2560 $right = $left = array_shift($bits);
2562 if (count($bits) > 0) {
2563 $bottom = array_shift($bits);
2565 if (count($bits) > 0) {
2566 $left = array_shift($bits);
2569 new css_style_margintop('margin-top', $top.$important),
2570 new css_style_marginright('margin-right', $right.$important),
2571 new css_style_marginbottom('margin-bottom', $bottom.$important),
2572 new css_style_marginleft('margin-left', $left.$important)
2577 * Consolidates individual margin styles into a single margin style
2579 * @param array $styles
2580 * @return array An array of consolidated styles
2582 public static function consolidate(array $styles) {
2583 if (count($styles) != 4) {
2587 $someimportant = false;
2588 $allimportant = null;
2589 $notimportantequal = null;
2591 foreach ($styles as $style) {
2592 if ($style->is_important()) {
2593 $someimportant = true;
2594 if ($allimportant === null) {
2595 $allimportant = true;
2598 if ($allimportant === true) {
2599 $allimportant = false;
2601 if ($firstvalue == null) {
2602 $firstvalue = $style->get_value(false);
2603 $notimportantequal = true;
2604 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
2605 $notimportantequal = false;
2610 if ($someimportant && !$allimportant && !$notimportantequal) {
2614 if ($someimportant && !$allimportant && $notimportantequal) {
2616 new css_style_margin('margin', $firstvalue)
2618 foreach ($styles as $style) {
2619 if ($style->is_important()) {
2629 foreach ($styles as $style) {
2630 switch ($style->get_name()) {
2632 $top = $style->get_value(false);
2634 case 'margin-right' :
2635 $right = $style->get_value(false);
2637 case 'margin-bottom' :
2638 $bottom = $style->get_value(false);
2640 case 'margin-left' :
2641 $left = $style->get_value(false);
2645 if ($top == $bottom && $left == $right) {
2646 if ($top == $left) {
2647 $returnstyle = new css_style_margin('margin', $top);
2649 $returnstyle = new css_style_margin('margin', "{$top} {$left}");
2651 } else if ($left == $right) {
2652 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom}");
2654 $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}");
2656 if ($allimportant) {
2657 $returnstyle->set_important();
2659 return array($returnstyle);
2665 * A margin top style
2669 * @copyright 2012 Sam Hemelryk
2670 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2672 class css_style_margintop
extends css_style_margin
{
2675 * A simple init, just a single style
2677 * @param string $value The value the style has
2678 * @return css_style_margintop
2680 public static function init($value) {
2681 return new css_style_margintop('margin-top', $value);
2685 * This style can be consolidated into a single margin style
2689 public function consolidate_to() {
2695 * A margin right style
2699 * @copyright 2012 Sam Hemelryk
2700 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2702 class css_style_marginright
extends css_style_margin
{
2705 * A simple init, just a single style
2707 * @param string $value The value the style has
2708 * @return css_style_margintop
2710 public static function init($value) {
2711 return new css_style_marginright('margin-right', $value);
2715 * This style can be consolidated into a single margin style
2719 public function consolidate_to() {
2725 * A margin bottom style
2729 * @copyright 2012 Sam Hemelryk
2730 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2732 class css_style_marginbottom
extends css_style_margin
{
2735 * A simple init, just a single style
2737 * @param string $value The value the style has
2738 * @return css_style_margintop
2740 public static function init($value) {
2741 return new css_style_marginbottom('margin-bottom', $value);
2745 * This style can be consolidated into a single margin style
2749 public function consolidate_to() {
2755 * A margin left style
2759 * @copyright 2012 Sam Hemelryk
2760 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2762 class css_style_marginleft
extends css_style_margin
{
2765 * A simple init, just a single style
2767 * @param string $value The value the style has
2768 * @return css_style_margintop
2770 public static function init($value) {
2771 return new css_style_marginleft('margin-left', $value);
2775 * This style can be consolidated into a single margin style
2779 public function consolidate_to() {
2789 * @copyright 2012 Sam Hemelryk
2790 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2792 class css_style_border
extends css_style
{
2795 * Initalises the border style into an array of individual style compontents
2797 * @param string $value The value the style has
2798 * @return css_style_bordercolor
2800 public static function init($value) {
2801 $value = preg_replace('#\s+#', ' ', $value);
2802 $bits = explode(' ', $value, 3);
2805 if (count($bits) > 0) {
2806 $width = array_shift($bits);
2807 if (!css_style_borderwidth
::is_border_width($width)) {
2810 $return[] = new css_style_borderwidth('border-top-width', $width);
2811 $return[] = new css_style_borderwidth('border-right-width', $width);
2812 $return[] = new css_style_borderwidth('border-bottom-width', $width);
2813 $return[] = new css_style_borderwidth('border-left-width', $width);
2815 if (count($bits) > 0) {
2816 $style = array_shift($bits);
2817 $return[] = new css_style_borderstyle('border-top-style', $style);
2818 $return[] = new css_style_borderstyle('border-right-style', $style);
2819 $return[] = new css_style_borderstyle('border-bottom-style', $style);
2820 $return[] = new css_style_borderstyle('border-left-style', $style);
2822 if (count($bits) > 0) {
2823 $colour = array_shift($bits);
2824 $return[] = new css_style_bordercolor('border-top-color', $colour);
2825 $return[] = new css_style_bordercolor('border-right-color', $colour);
2826 $return[] = new css_style_bordercolor('border-bottom-color', $colour);
2827 $return[] = new css_style_bordercolor('border-left-color', $colour);
2833 * Consolidates all border styles into a single style
2835 * @param array $styles An array of border styles
2836 * @return array An optimised array of border styles
2838 public static function consolidate(array $styles) {
2840 $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2841 $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2842 $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2844 foreach ($styles as $style) {
2845 switch ($style->get_name()) {
2846 case 'border-top-width':
2847 $borderwidths['top'] = $style->get_value();
2849 case 'border-right-width':
2850 $borderwidths['right'] = $style->get_value();
2852 case 'border-bottom-width':
2853 $borderwidths['bottom'] = $style->get_value();
2855 case 'border-left-width':
2856 $borderwidths['left'] = $style->get_value();
2859 case 'border-top-style':
2860 $borderstyles['top'] = $style->get_value();
2862 case 'border-right-style':
2863 $borderstyles['right'] = $style->get_value();
2865 case 'border-bottom-style':
2866 $borderstyles['bottom'] = $style->get_value();
2868 case 'border-left-style':
2869 $borderstyles['left'] = $style->get_value();
2872 case 'border-top-color':
2873 $bordercolors['top'] = css_style_color
::shrink_value($style->get_value());
2875 case 'border-right-color':
2876 $bordercolors['right'] = css_style_color
::shrink_value($style->get_value());
2878 case 'border-bottom-color':
2879 $bordercolors['bottom'] = css_style_color
::shrink_value($style->get_value());
2881 case 'border-left-color':
2882 $bordercolors['left'] = css_style_color
::shrink_value($style->get_value());
2887 $uniquewidths = count(array_unique($borderwidths));
2888 $uniquestyles = count(array_unique($borderstyles));
2889 $uniquecolors = count(array_unique($bordercolors));
2891 $nullwidths = in_array(null, $borderwidths, true);
2892 $nullstyles = in_array(null, $borderstyles, true);
2893 $nullcolors = in_array(null, $bordercolors, true);
2895 $allwidthsthesame = ($uniquewidths === 1)?
1:0;
2896 $allstylesthesame = ($uniquestyles === 1)?
1:0;
2897 $allcolorsthesame = ($uniquecolors === 1)?
1:0;
2899 $allwidthsnull = $allwidthsthesame && $nullwidths;
2900 $allstylesnull = $allstylesthesame && $nullstyles;
2901 $allcolorsnull = $allcolorsthesame && $nullcolors;
2904 if ($allwidthsnull && $allstylesnull && $allcolorsnull) {
2905 // Everything is null still... boo
2906 return array(new css_style_border('border', ''));
2908 } else if ($allwidthsnull && $allstylesnull) {
2910 self
::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2913 } else if ($allwidthsnull && $allcolorsnull) {
2915 self
::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2918 } else if ($allcolorsnull && $allstylesnull) {
2920 self
::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2925 if ($allwidthsthesame +
$allstylesthesame +
$allcolorsthesame == 3) {
2927 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']);
2929 } else if ($allwidthsthesame +
$allstylesthesame +
$allcolorsthesame == 2) {
2931 if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) {
2933 $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']);
2934 self
::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2936 } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) {
2938 $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']);
2939 self
::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2941 } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) {
2943 $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']);
2944 self
::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2947 self
::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2948 self
::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2949 self
::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2952 } else if (!$nullwidths && !$nullcolors && !$nullstyles && max(array_count_values($borderwidths)) == 3 && max(array_count_values($borderstyles)) == 3 && max(array_count_values($bordercolors)) == 3) {
2953 $widthkeys = array();
2954 $stylekeys = array();
2955 $colorkeys = array();
2957 foreach ($borderwidths as $key => $value) {
2958 if (!array_key_exists($value, $widthkeys)) {
2959 $widthkeys[$value] = array();
2961 $widthkeys[$value][] = $key;
2963 usort($widthkeys, 'css_sort_by_count');
2964 $widthkeys = array_values($widthkeys);
2966 foreach ($borderstyles as $key => $value) {
2967 if (!array_key_exists($value, $stylekeys)) {
2968 $stylekeys[$value] = array();
2970 $stylekeys[$value][] = $key;
2972 usort($stylekeys, 'css_sort_by_count');
2973 $stylekeys = array_values($stylekeys);
2975 foreach ($bordercolors as $key => $value) {
2976 if (!array_key_exists($value, $colorkeys)) {
2977 $colorkeys[$value] = array();
2979 $colorkeys[$value][] = $key;
2981 usort($colorkeys, 'css_sort_by_count');
2982 $colorkeys = array_values($colorkeys);
2984 if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) {
2985 $key = $widthkeys[0][0];
2986 self
::build_style_string($return, 'css_style_border', 'border', $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2987 $key = $widthkeys[1][0];
2988 self
::build_style_string($return, 'css_style_border'.$key, 'border-'.$key, $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
2990 self
::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2991 self
::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2992 self
::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2993 self
::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
2996 self
::build_style_string($return, 'css_style_bordertop', 'border-top', $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
2997 self
::build_style_string($return, 'css_style_borderright', 'border-right', $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
2998 self
::build_style_string($return, 'css_style_borderbottom', 'border-bottom', $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
2999 self
::build_style_string($return, 'css_style_borderleft', 'border-left', $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
3001 foreach ($return as $key => $style) {
3002 if ($style->get_value() == '') {
3003 unset($return[$key]);
3010 * Border styles get consolidated to a single border style.
3014 public function consolidate_to() {
3019 * Consolidates a series of border styles into an optimised array of border
3020 * styles by looking at the direction of the border and prioritising that
3021 * during the optimisation.
3023 * @param array $array An array to add styles into during consolidation. Passed by reference.
3024 * @param string $class The class type to initalise
3025 * @param string $style The style to create
3026 * @param string|array $top The top value
3027 * @param string $right The right value
3028 * @param string $bottom The bottom value
3029 * @param string $left The left value
3032 public static function consolidate_styles_by_direction(&$array, $class, $style, $top, $right = null, $bottom = null, $left = null) {
3033 if (is_array($top)) {
3034 $right = $top['right'];
3035 $bottom = $top['bottom'];
3036 $left = $top['left'];
3040 if ($top == $bottom && $left == $right && $top == $left) {
3041 if (is_null($top)) {
3042 $array[] = new $class($style, '');
3044 $array[] = new $class($style, $top);
3046 } else if ($top == null ||
$right == null ||
$bottom == null ||
$left == null) {
3047 if ($top !== null) {
3048 $array[] = new $class(str_replace('border-', 'border-top-', $style), $top);
3050 if ($right !== null) {
3051 $array[] = new $class(str_replace('border-', 'border-right-', $style), $right);
3053 if ($bottom !== null) {
3054 $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom);
3056 if ($left !== null) {
3057 $array[] = new $class(str_replace('border-', 'border-left-', $style), $left);
3059 } else if ($top == $bottom && $left == $right) {
3060 $array[] = new $class($style, $top.' '.$right);
3061 } else if ($left == $right) {
3062 $array[] = new $class($style, $top.' '.$right.' '.$bottom);
3064 $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left);
3070 * Builds a border style for a set of width, style, and colour values
3072 * @param array $array An array into which the generated style is added
3073 * @param string $class The class type to initialise
3074 * @param string $cssstyle The style to use
3075 * @param string $width The width of the border
3076 * @param string $style The style of the border
3077 * @param string $color The colour of the border
3080 public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) {
3081 if (!is_null($width) && !is_null($style) && !is_null($color)) {
3082 $array[] = new $class($cssstyle, $width.' '.$style.' '.$color);
3083 } else if (!is_null($width) && !is_null($style) && is_null($color)) {
3084 $array[] = new $class($cssstyle, $width.' '.$style);
3085 } else if (!is_null($width) && is_null($style) && is_null($color)) {
3086 $array[] = new $class($cssstyle, $width);
3088 if (!is_null($width)) {
3089 $array[] = new $class($cssstyle, $width);
3091 if (!is_null($style)) {
3092 $array[] = new $class($cssstyle, $style);
3094 if (!is_null($color)) {
3095 $array[] = new $class($cssstyle, $color);
3103 * A border colour style
3107 * @copyright 2012 Sam Hemelryk
3108 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3110 class css_style_bordercolor
extends css_style_color
{
3113 * Creates a new border colour style
3115 * Based upon the colour style
3117 * @param mixed $value
3118 * @return Array of css_style_bordercolor
3120 public static function init($value) {
3121 $value = preg_replace('#\s+#', ' ', $value);
3122 $bits = explode(' ', $value, 4);
3124 $top = $right = $bottom = $left = null;
3125 if (count($bits) > 0) {
3126 $top = $right = $bottom = $left = array_shift($bits);
3128 if (count($bits) > 0) {
3129 $right = $left = array_shift($bits);
3131 if (count($bits) > 0) {
3132 $bottom = array_shift($bits);
3134 if (count($bits) > 0) {
3135 $left = array_shift($bits);
3138 css_style_bordertopcolor
::init($top),
3139 css_style_borderrightcolor
::init($right),
3140 css_style_borderbottomcolor
::init($bottom),
3141 css_style_borderleftcolor
::init($left)
3146 * Consolidate this to a single border style
3150 public function consolidate_to() {
3157 * @param string $value Cleans the provided value optimising it if possible
3160 protected function clean_value($value) {
3161 $values = explode(' ', $value);
3162 $values = array_map('parent::clean_value', $values);
3163 return join (' ', $values);
3167 * Outputs this style
3169 * @param string $overridevalue
3172 public function out($overridevalue = null) {
3173 if ($overridevalue === null) {
3174 $overridevalue = $this->value
;
3176 $values = explode(' ', $overridevalue);
3177 $values = array_map('css_style_color::shrink_value', $values);
3178 return parent
::out(join (' ', $values));
3183 * A border left style
3187 * @copyright 2012 Sam Hemelryk
3188 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3190 class css_style_borderleft
extends css_style_generic
{
3193 * Initialises the border left style into individual components
3195 * @param string $value
3196 * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor
3198 public static function init($value) {
3199 $value = preg_replace('#\s+#', ' ', $value);
3200 $bits = explode(' ', $value, 3);
3203 if (count($bits) > 0) {
3204 $return[] = css_style_borderleftwidth
::init(array_shift($bits));
3206 if (count($bits) > 0) {
3207 $return[] = css_style_borderleftstyle
::init(array_shift($bits));
3209 if (count($bits) > 0) {
3210 $return[] = css_style_borderleftcolor
::init(array_shift($bits));
3216 * Consolidate this to a single border style
3220 public function consolidate_to() {
3226 * A border right style
3230 * @copyright 2012 Sam Hemelryk
3231 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3233 class css_style_borderright
extends css_style_generic
{
3236 * Initialises the border right style into individual components
3238 * @param string $value The value of the style
3239 * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor
3241 public static function init($value) {
3242 $value = preg_replace('#\s+#', ' ', $value);
3243 $bits = explode(' ', $value, 3);
3246 if (count($bits) > 0) {
3247 $return[] = css_style_borderrightwidth
::init(array_shift($bits));
3249 if (count($bits) > 0) {
3250 $return[] = css_style_borderrightstyle
::init(array_shift($bits));
3252 if (count($bits) > 0) {
3253 $return[] = css_style_borderrightcolor
::init(array_shift($bits));
3259 * Consolidate this to a single border style
3263 public function consolidate_to() {
3269 * A border top style
3273 * @copyright 2012 Sam Hemelryk
3274 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3276 class css_style_bordertop
extends css_style_generic
{
3279 * Initialises the border top style into individual components
3281 * @param string $value The value of the style
3282 * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor
3284 public static function init($value) {
3285 $value = preg_replace('#\s+#', ' ', $value);
3286 $bits = explode(' ', $value, 3);
3289 if (count($bits) > 0) {
3290 $return[] = css_style_bordertopwidth
::init(array_shift($bits));
3292 if (count($bits) > 0) {
3293 $return[] = css_style_bordertopstyle
::init(array_shift($bits));
3295 if (count($bits) > 0) {
3296 $return[] = css_style_bordertopcolor
::init(array_shift($bits));
3302 * Consolidate this to a single border style
3306 public function consolidate_to() {
3312 * A border bottom style
3316 * @copyright 2012 Sam Hemelryk
3317 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3319 class css_style_borderbottom
extends css_style_generic
{
3322 * Initialises the border bottom style into individual components
3324 * @param string $value The value of the style
3325 * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor
3327 public static function init($value) {
3328 $value = preg_replace('#\s+#', ' ', $value);
3329 $bits = explode(' ', $value, 3);
3332 if (count($bits) > 0) {
3333 $return[] = css_style_borderbottomwidth
::init(array_shift($bits));
3335 if (count($bits) > 0) {
3336 $return[] = css_style_borderbottomstyle
::init(array_shift($bits));
3338 if (count($bits) > 0) {
3339 $return[] = css_style_borderbottomcolor
::init(array_shift($bits));
3345 * Consolidate this to a single border style
3349 public function consolidate_to() {
3355 * A border width style
3359 * @copyright 2012 Sam Hemelryk
3360 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3362 class css_style_borderwidth
extends css_style_width
{
3365 * Creates a new border colour style
3367 * Based upon the colour style
3369 * @param string $value The value of the style
3370 * @return array Array of css_style_border*width
3372 public static function init($value) {
3373 $value = preg_replace('#\s+#', ' ', $value);
3374 $bits = explode(' ', $value, 4);
3376 $top = $right = $bottom = $left = null;
3377 if (count($bits) > 0) {
3378 $top = $right = $bottom = $left = array_shift($bits);
3380 if (count($bits) > 0) {
3381 $right = $left = array_shift($bits);
3383 if (count($bits) > 0) {
3384 $bottom = array_shift($bits);
3386 if (count($bits) > 0) {
3387 $left = array_shift($bits);
3390 css_style_bordertopwidth
::init($top),
3391 css_style_borderrightwidth
::init($right),
3392 css_style_borderbottomwidth
::init($bottom),
3393 css_style_borderleftwidth
::init($left)
3398 * Consolidate this to a single border style
3402 public function consolidate_to() {
3407 * Checks if the width is valid
3410 public function is_valid() {
3411 return self
::is_border_width($this->value
);
3415 * Cleans the provided value
3417 * @param mixed $value Cleans the provided value optimising it if possible
3420 protected function clean_value($value) {
3421 $isvalid = self
::is_border_width($value);
3423 $this->set_error('Invalid width specified for '.$this->name
);
3424 } else if (preg_match('#^0\D+$#', $value)) {
3427 return trim($value);
3431 * Returns true if the provided value is a permitted border width
3432 * @param string $value The value to check
3435 public static function is_border_width($value) {
3436 $altwidthvalues = array('thin', 'medium', 'thick');
3437 return css_is_width($value) ||
in_array($value, $altwidthvalues);
3442 * A border style style
3446 * @copyright 2012 Sam Hemelryk
3447 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3449 class css_style_borderstyle
extends css_style_generic
{
3452 * Creates a new border colour style
3454 * Based upon the colour style
3456 * @param string $value The value of the style
3457 * @return array Array of css_style_border*style
3459 public static function init($value) {
3460 $value = preg_replace('#\s+#', ' ', $value);
3461 $bits = explode(' ', $value, 4);
3463 $top = $right = $bottom = $left = null;
3464 if (count($bits) > 0) {
3465 $top = $right = $bottom = $left = array_shift($bits);
3467 if (count($bits) > 0) {
3468 $right = $left = array_shift($bits);
3470 if (count($bits) > 0) {
3471 $bottom = array_shift($bits);
3473 if (count($bits) > 0) {
3474 $left = array_shift($bits);
3477 css_style_bordertopstyle
::init($top),
3478 css_style_borderrightstyle
::init($right),
3479 css_style_borderbottomstyle
::init($bottom),
3480 css_style_borderleftstyle
::init($left)
3485 * Consolidate this to a single border style
3489 public function consolidate_to() {
3495 * A border top colour style
3499 * @copyright 2012 Sam Hemelryk
3500 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3502 class css_style_bordertopcolor
extends css_style_bordercolor
{
3505 * Initialises this style object
3507 * @param string $value The value of the style
3508 * @return css_style_bordertopcolor
3510 public static function init($value) {
3511 return new css_style_bordertopcolor('border-top-color', $value);
3515 * Consolidate this to a single border style
3519 public function consolidate_to() {
3525 * A border left colour style
3529 * @copyright 2012 Sam Hemelryk
3530 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3532 class css_style_borderleftcolor
extends css_style_bordercolor
{
3535 * Initialises this style object
3537 * @param string $value The value of the style
3538 * @return css_style_borderleftcolor
3540 public static function init($value) {
3541 return new css_style_borderleftcolor('border-left-color', $value);
3545 * Consolidate this to a single border style
3549 public function consolidate_to() {
3555 * A border right colour style
3559 * @copyright 2012 Sam Hemelryk
3560 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3562 class css_style_borderrightcolor
extends css_style_bordercolor
{
3565 * Initialises this style object
3567 * @param string $value The value of the style
3568 * @return css_style_borderrightcolor
3570 public static function init($value) {
3571 return new css_style_borderrightcolor('border-right-color', $value);
3575 * Consolidate this to a single border style
3579 public function consolidate_to() {
3585 * A border bottom colour style
3589 * @copyright 2012 Sam Hemelryk
3590 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3592 class css_style_borderbottomcolor
extends css_style_bordercolor
{
3595 * Initialises this style object
3597 * @param string $value The value of the style
3598 * @return css_style_borderbottomcolor
3600 public static function init($value) {
3601 return new css_style_borderbottomcolor('border-bottom-color', $value);
3605 * Consolidate this to a single border style
3609 public function consolidate_to() {
3615 * A border width top style
3619 * @copyright 2012 Sam Hemelryk
3620 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3622 class css_style_bordertopwidth
extends css_style_borderwidth
{
3625 * Initialises this style object
3627 * @param string $value The value of the style
3628 * @return css_style_bordertopwidth
3630 public static function init($value) {
3631 return new css_style_bordertopwidth('border-top-width', $value);
3635 * Consolidate this to a single border style
3639 public function consolidate_to() {
3645 * A border width left style
3649 * @copyright 2012 Sam Hemelryk
3650 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3652 class css_style_borderleftwidth
extends css_style_borderwidth
{
3655 * Initialises this style object
3657 * @param string $value The value of the style
3658 * @return css_style_borderleftwidth
3660 public static function init($value) {
3661 return new css_style_borderleftwidth('border-left-width', $value);
3665 * Consolidate this to a single border style
3669 public function consolidate_to() {
3675 * A border width right style
3679 * @copyright 2012 Sam Hemelryk
3680 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3682 class css_style_borderrightwidth
extends css_style_borderwidth
{
3685 * Initialises this style object
3687 * @param string $value The value of the style
3688 * @return css_style_borderrightwidth
3690 public static function init($value) {
3691 return new css_style_borderrightwidth('border-right-width', $value);
3695 * Consolidate this to a single border style
3699 public function consolidate_to() {
3705 * A border width bottom style
3709 * @copyright 2012 Sam Hemelryk
3710 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3712 class css_style_borderbottomwidth
extends css_style_borderwidth
{
3715 * Initialises this style object
3717 * @param string $value The value of the style
3718 * @return css_style_borderbottomwidth
3720 public static function init($value) {
3721 return new css_style_borderbottomwidth('border-bottom-width', $value);
3725 * Consolidate this to a single border style
3729 public function consolidate_to() {
3735 * A border top style
3739 * @copyright 2012 Sam Hemelryk
3740 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3742 class css_style_bordertopstyle
extends css_style_borderstyle
{
3745 * Initialises this style object
3747 * @param string $value The value of the style
3748 * @return css_style_bordertopstyle
3750 public static function init($value) {
3751 return new css_style_bordertopstyle('border-top-style', $value);
3755 * Consolidate this to a single border style
3759 public function consolidate_to() {
3765 * A border left style
3769 * @copyright 2012 Sam Hemelryk
3770 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3772 class css_style_borderleftstyle
extends css_style_borderstyle
{
3775 * Initialises this style object
3777 * @param string $value The value of the style
3778 * @return css_style_borderleftstyle
3780 public static function init($value) {
3781 return new css_style_borderleftstyle('border-left-style', $value);
3785 * Consolidate this to a single border style
3789 public function consolidate_to() {
3795 * A border right style
3799 * @copyright 2012 Sam Hemelryk
3800 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3802 class css_style_borderrightstyle
extends css_style_borderstyle
{
3805 * Initialises this style object
3807 * @param string $value The value of the style
3808 * @return css_style_borderrightstyle
3810 public static function init($value) {
3811 return new css_style_borderrightstyle('border-right-style', $value);
3815 * Consolidate this to a single border style
3819 public function consolidate_to() {
3825 * A border bottom style
3829 * @copyright 2012 Sam Hemelryk
3830 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3832 class css_style_borderbottomstyle
extends css_style_borderstyle
{
3835 * Initialises this style object
3837 * @param string $value The value for the style
3838 * @return css_style_borderbottomstyle
3840 public static function init($value) {
3841 return new css_style_borderbottomstyle('border-bottom-style', $value);
3845 * Consolidate this to a single border style
3849 public function consolidate_to() {
3855 * A background style
3859 * @copyright 2012 Sam Hemelryk
3860 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3862 class css_style_background
extends css_style
{
3865 * Initialises a background style
3867 * @param string $value The value of the style
3868 * @return array An array of background component.
3870 public static function init($value) {
3871 // colour - image - repeat - attachment - position
3874 if (preg_match('#url\(([^\)]+)\)#', $value, $matches)) {
3875 $imageurl = trim($matches[1]);
3876 $value = str_replace($matches[1], '', $value);
3879 // Switch out the brackets so that they don't get messed up when we explode
3880 $brackets = array();
3882 while (preg_match('#\([^\)\(]+\)#', $value, $matches)) {
3883 $key = "##BRACKET-{$bracketcount}##";
3885 $brackets[$key] = $matches[0];
3886 $value = str_replace($matches[0], $key, $value);
3889 $important = (stripos($value, '!important') !== false);
3891 // Great some genius put !important in the background shorthand property
3892 $value = str_replace('!important', '', $value);
3895 $value = preg_replace('#\s+#', ' ', $value);
3896 $bits = explode(' ', $value);
3898 foreach ($bits as $key => $bit) {
3899 $bits[$key] = self
::replace_bracket_placeholders($bit, $brackets);
3901 unset($bracketcount);
3904 $repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit');
3905 $attachments = array('scroll' , 'fixed', 'inherit');
3906 $positions = array('top', 'left', 'bottom', 'right', 'center');
3909 $unknownbits = array();
3911 $color = self
::NULL_VALUE
;
3912 if (count($bits) > 0 && css_is_colour(reset($bits))) {
3913 $color = array_shift($bits);
3916 $image = self
::NULL_VALUE
;
3917 if (count($bits) > 0 && preg_match('#^\s*(none|inherit|url\(\))\s*$#', reset($bits))) {
3918 $image = array_shift($bits);
3919 if ($image == 'url()') {
3920 $image = "url({$imageurl})";
3924 $repeat = self
::NULL_VALUE
;
3925 if (count($bits) > 0 && in_array(reset($bits), $repeats)) {
3926 $repeat = array_shift($bits);
3929 $attachment = self
::NULL_VALUE
;
3930 if (count($bits) > 0 && in_array(reset($bits), $attachments)) {
3931 // scroll , fixed, inherit
3932 $attachment = array_shift($bits);
3935 $position = self
::NULL_VALUE
;
3936 if (count($bits) > 0) {
3937 $widthbits = array();
3938 foreach ($bits as $bit) {
3939 if (in_array($bit, $positions) ||
css_is_width($bit)) {
3940 $widthbits[] = $bit;
3942 $unknownbits[] = $bit;
3945 if (count($widthbits)) {
3946 $position = join(' ', $widthbits);
3950 if (count($unknownbits)) {
3951 foreach ($unknownbits as $bit) {
3953 if ($color === self
::NULL_VALUE
&& css_is_colour($bit)) {
3955 } else if ($repeat === self
::NULL_VALUE
&& in_array($bit, $repeats)) {
3957 } else if ($attachment === self
::NULL_VALUE
&& in_array($bit, $attachments)) {
3959 } else if ($bit !== '') {
3960 $advanced = css_style_background_advanced
::init($bit);
3962 $advanced->set_important();
3964 $return[] = $advanced;
3969 if ($color === self
::NULL_VALUE
&& $image === self
::NULL_VALUE
&& $repeat === self
::NULL_VALUE
&& $attachment === self
::NULL_VALUE
&& $position === self
::NULL_VALUE
) {
3970 // All primaries are null, return without doing anything else. There may be advanced madness there.
3974 $return[] = new css_style_backgroundcolor('background-color', $color);
3975 $return[] = new css_style_backgroundimage('background-image', $image);
3976 $return[] = new css_style_backgroundrepeat('background-repeat', $repeat);
3977 $return[] = new css_style_backgroundattachment('background-attachment', $attachment);
3978 $return[] = new css_style_backgroundposition('background-position', $position);
3981 foreach ($return as $style) {
3982 $style->set_important();
3990 * Static helper method to switch in bracket replacements
3992 * @param string $value
3993 * @param array $placeholders
3996 protected static function replace_bracket_placeholders($value, array $placeholders) {
3997 while (preg_match('/##BRACKET-\d+##/', $value, $matches)) {
3998 $value = str_replace($matches[0], $placeholders[$matches[0]], $value);
4004 * Consolidates background styles into a single background style
4006 * @param array $styles Consolidates the provided array of background styles
4007 * @return array Consolidated optimised background styles
4009 public static function consolidate(array $styles) {
4011 if (empty($styles)) {
4024 $someimportant = false;
4025 $allimportant = null;
4026 foreach ($styles as $style) {
4027 if ($style instanceof css_style_backgroundimage_advanced
) {
4030 if ($style->is_important()) {
4031 $someimportant = true;
4032 if ($allimportant === null) {
4033 $allimportant = true;
4035 } else if ($allimportant === true) {
4036 $allimportant = false;
4040 $organisedstyles = array();
4041 $advancedstyles = array();
4042 $importantstyles = array();
4043 foreach ($styles as $style) {
4044 if ($style instanceof css_style_backgroundimage_advanced
) {
4045 $advancedstyles[] = $style;
4048 if ($someimportant && !$allimportant && $style->is_important()) {
4049 $importantstyles[] = $style;
4052 $organisedstyles[$style->get_name()] = $style;
4053 switch ($style->get_name()) {
4054 case 'background-color' :
4055 $color = css_style_color
::shrink_value($style->get_value(false));
4057 case 'background-image' :
4058 $image = $style->get_value(false);
4060 case 'background-repeat' :
4061 $repeat = $style->get_value(false);
4063 case 'background-attachment' :
4064 $attachment = $style->get_value(false);
4066 case 'background-position' :
4067 $position = $style->get_value(false);
4069 case 'background-clip' :
4070 $clip = $style->get_value();
4072 case 'background-origin' :
4073 $origin = $style->get_value();
4075 case 'background-size' :
4076 $size = $style->get_value();
4081 $consolidatetosingle = array();
4082 if (!is_null($color) && !is_null($image) && !is_null($repeat) && !is_null($attachment) && !is_null($position)) {
4083 // We can use the shorthand background-style!
4084 if (!$organisedstyles['background-color']->is_special_empty_value()) {
4085 $consolidatetosingle[] = $color;
4087 if (!$organisedstyles['background-image']->is_special_empty_value()) {
4088 $consolidatetosingle[] = $image;
4090 if (!$organisedstyles['background-repeat']->is_special_empty_value()) {
4091 $consolidatetosingle[] = $repeat;
4093 if (!$organisedstyles['background-attachment']->is_special_empty_value()) {
4094 $consolidatetosingle[] = $attachment;
4096 if (!$organisedstyles['background-position']->is_special_empty_value()) {
4097 $consolidatetosingle[] = $position;
4099 // Reset them all to null so we don't use them again.
4108 // Single background style needs to come first;
4109 if (count($consolidatetosingle) > 0) {
4110 $returnstyle = new css_style_background('background', join(' ', $consolidatetosingle));
4111 if ($allimportant) {
4112 $returnstyle->set_important();
4114 $return[] = $returnstyle;
4116 foreach ($styles as $style) {
4118 switch ($style->get_name()) {
4119 case 'background-color' : $value = $color; break;
4120 case 'background-image' : $value = $image; break;
4121 case 'background-repeat' : $value = $repeat; break;
4122 case 'background-attachment' : $value = $attachment; break;
4123 case 'background-position' : $value = $position; break;
4124 case 'background-clip' : $value = $clip; break;
4125 case 'background-origin' : $value = $origin; break;
4126 case 'background-size' : $value = $size; break;
4128 if (!is_null($value)) {
4132 $return = array_merge($return, $importantstyles, $advancedstyles);
4138 * A advanced background style that allows multiple values to preserve unknown entities
4142 * @copyright 2012 Sam Hemelryk
4143 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4145 class css_style_background_advanced
extends css_style_generic
{
4147 * Creates a new background colour style
4149 * @param string $value The value of the style
4150 * @return css_style_backgroundimage
4152 public static function init($value) {
4153 $value = preg_replace('#\s+#', ' ', $value);
4154 return new css_style_background_advanced('background', $value);
4158 * Returns true because the advanced background image supports multiple values.
4159 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4163 public function allows_multiple_values() {
4169 * A background colour style.
4171 * Based upon the colour style.
4175 * @copyright 2012 Sam Hemelryk
4176 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4178 class css_style_backgroundcolor
extends css_style_color
{
4181 * Creates a new background colour style
4183 * @param string $value The value of the style
4184 * @return css_style_backgroundcolor
4186 public static function init($value) {
4187 return new css_style_backgroundcolor('background-color', $value);
4191 * css_style_backgroundcolor consolidates to css_style_background
4195 public function consolidate_to() {
4196 return 'background';
4200 * Returns true if the value for this style is the special null value.
4202 * This occurs if the shorthand background property was used but no proper value
4203 * was specified for this style.
4204 * This leads to a null value being used unless otherwise overridden.
4208 public function is_special_empty_value() {
4209 return ($this->value
=== self
::NULL_VALUE
);
4213 * Returns true if the value for this style is valid
4216 public function is_valid() {
4217 return $this->is_special_empty_value() || parent
::is_valid();
4222 * A background image style.
4226 * @copyright 2012 Sam Hemelryk
4227 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4229 class css_style_backgroundimage
extends css_style_generic
{
4232 * Creates a new background image style
4234 * @param string $value The value of the style
4235 * @return css_style_backgroundimage
4237 public static function init($value) {
4238 if (!preg_match('#^\s*(none|inherit|url\()#i', $value)) {
4239 return css_style_backgroundimage_advanced
::init($value);
4241 return new css_style_backgroundimage('background-image', $value);
4245 * Consolidates this style into a single background style
4249 public function consolidate_to() {
4250 return 'background';
4254 * Returns true if the value for this style is the special null value.
4256 * This occurs if the shorthand background property was used but no proper value
4257 * was specified for this style.
4258 * This leads to a null value being used unless otherwise overridden.
4262 public function is_special_empty_value() {
4263 return ($this->value
=== self
::NULL_VALUE
);
4267 * Returns true if the value for this style is valid
4270 public function is_valid() {
4271 return $this->is_special_empty_value() || parent
::is_valid();
4276 * A background image style that supports mulitple values and masquerades as a background-image
4280 * @copyright 2012 Sam Hemelryk
4281 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4283 class css_style_backgroundimage_advanced
extends css_style_generic
{
4285 * Creates a new background colour style
4287 * @param string $value The value of the style
4288 * @return css_style_backgroundimage
4290 public static function init($value) {
4291 $value = preg_replace('#\s+#', ' ', $value);
4292 return new css_style_backgroundimage_advanced('background-image', $value);
4296 * Returns true because the advanced background image supports multiple values.
4297 * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4301 public function allows_multiple_values() {
4307 * A background repeat style.
4311 * @copyright 2012 Sam Hemelryk
4312 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4314 class css_style_backgroundrepeat
extends css_style_generic
{
4317 * Creates a new background colour style
4319 * @param string $value The value of the style
4320 * @return css_style_backgroundrepeat
4322 public static function init($value) {
4323 return new css_style_backgroundrepeat('background-repeat', $value);
4327 * Consolidates this style into a single background style
4331 public function consolidate_to() {
4332 return 'background';
4336 * Returns true if the value for this style is the special null value.
4338 * This occurs if the shorthand background property was used but no proper value
4339 * was specified for this style.
4340 * This leads to a null value being used unless otherwise overridden.
4344 public function is_special_empty_value() {
4345 return ($this->value
=== self
::NULL_VALUE
);
4349 * Returns true if the value for this style is valid
4352 public function is_valid() {
4353 return $this->is_special_empty_value() || parent
::is_valid();
4358 * A background attachment style.
4362 * @copyright 2012 Sam Hemelryk
4363 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4365 class css_style_backgroundattachment
extends css_style_generic
{
4368 * Creates a new background colour style
4370 * @param string $value The value of the style
4371 * @return css_style_backgroundattachment
4373 public static function init($value) {
4374 return new css_style_backgroundattachment('background-attachment', $value);
4378 * Consolidates this style into a single background style
4382 public function consolidate_to() {
4383 return 'background';
4387 * Returns true if the value for this style is the special null value.
4389 * This occurs if the shorthand background property was used but no proper value
4390 * was specified for this style.
4391 * This leads to a null value being used unless otherwise overridden.
4395 public function is_special_empty_value() {
4396 return ($this->value
=== self
::NULL_VALUE
);
4400 * Returns true if the value for this style is valid
4403 public function is_valid() {
4404 return $this->is_special_empty_value() || parent
::is_valid();
4409 * A background position style.
4413 * @copyright 2012 Sam Hemelryk
4414 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4416 class css_style_backgroundposition
extends css_style_generic
{
4419 * Creates a new background colour style
4421 * @param string $value The value of the style
4422 * @return css_style_backgroundposition
4424 public static function init($value) {
4425 return new css_style_backgroundposition('background-position', $value);
4429 * Consolidates this style into a single background style
4433 public function consolidate_to() {
4434 return 'background';
4438 * Returns true if the value for this style is the special null value.
4440 * This occurs if the shorthand background property was used but no proper value
4441 * was specified for this style.
4442 * This leads to a null value being used unless otherwise overridden.
4446 public function is_special_empty_value() {
4447 return ($this->value
=== self
::NULL_VALUE
);
4451 * Returns true if the value for this style is valid
4454 public function is_valid() {
4455 return $this->is_special_empty_value() || parent
::is_valid();
4460 * A background size style.
4464 * @copyright 2012 Sam Hemelryk
4465 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4467 class css_style_backgroundsize
extends css_style_generic
{
4470 * Creates a new background size style
4472 * @param string $value The value of the style
4473 * @return css_style_backgroundposition
4475 public static function init($value) {
4476 return new css_style_backgroundsize('background-size', $value);
4480 * Consolidates this style into a single background style
4484 public function consolidate_to() {
4485 return 'background';
4490 * A background clip style.
4494 * @copyright 2012 Sam Hemelryk
4495 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4497 class css_style_backgroundclip
extends css_style_generic
{
4500 * Creates a new background clip style
4502 * @param string $value The value of the style
4503 * @return css_style_backgroundposition
4505 public static function init($value) {
4506 return new css_style_backgroundclip('background-clip', $value);
4510 * Consolidates this style into a single background style
4514 public function consolidate_to() {
4515 return 'background';
4520 * A background origin style.
4524 * @copyright 2012 Sam Hemelryk
4525 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4527 class css_style_backgroundorigin
extends css_style_generic
{
4530 * Creates a new background origin style
4532 * @param string $value The value of the style
4533 * @return css_style_backgroundposition
4535 public static function init($value) {
4536 return new css_style_backgroundorigin('background-origin', $value);
4540 * Consolidates this style into a single background style
4544 public function consolidate_to() {
4545 return 'background';
4554 * @copyright 2012 Sam Hemelryk
4555 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4557 class css_style_padding
extends css_style_width
{
4560 * Initialises this padding style into several individual padding styles
4562 * @param string $value The value fo the style
4563 * @return array An array of padding styles
4565 public static function init($value) {
4567 if (strpos($value, '!important') !== false) {
4568 $important = ' !important';
4569 $value = str_replace('!important', '', $value);
4572 $value = preg_replace('#\s+#', ' ', trim($value));
4573 $bits = explode(' ', $value, 4);
4575 $top = $right = $bottom = $left = null;
4576 if (count($bits) > 0) {
4577 $top = $right = $bottom = $left = array_shift($bits);
4579 if (count($bits) > 0) {
4580 $right = $left = array_shift($bits);
4582 if (count($bits) > 0) {
4583 $bottom = array_shift($bits);
4585 if (count($bits) > 0) {
4586 $left = array_shift($bits);
4589 new css_style_paddingtop('padding-top', $top.$important),
4590 new css_style_paddingright('padding-right', $right.$important),
4591 new css_style_paddingbottom('padding-bottom', $bottom.$important),
4592 new css_style_paddingleft('padding-left', $left.$important)
4597 * Consolidates several padding styles into a single style.
4599 * @param array $styles Array of padding styles
4600 * @return array Optimised+consolidated array of padding styles
4602 public static function consolidate(array $styles) {
4603 if (count($styles) != 4) {
4607 $someimportant = false;
4608 $allimportant = null;
4609 $notimportantequal = null;
4611 foreach ($styles as $style) {
4612 if ($style->is_important()) {
4613 $someimportant = true;
4614 if ($allimportant === null) {
4615 $allimportant = true;
4618 if ($allimportant === true) {
4619 $allimportant = false;
4621 if ($firstvalue == null) {
4622 $firstvalue = $style->get_value(false);
4623 $notimportantequal = true;
4624 } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
4625 $notimportantequal = false;
4630 if ($someimportant && !$allimportant && !$notimportantequal) {
4634 if ($someimportant && !$allimportant && $notimportantequal) {
4636 new css_style_padding('padding', $firstvalue)
4638 foreach ($styles as $style) {
4639 if ($style->is_important()) {
4649 foreach ($styles as $style) {
4650 switch ($style->get_name()) {
4651 case 'padding-top' : $top = $style->get_value(false);break;
4652 case 'padding-right' : $right = $style->get_value(false);break;
4653 case 'padding-bottom' : $bottom = $style->get_value(false);break;
4654 case 'padding-left' : $left = $style->get_value(false);break;
4657 if ($top == $bottom && $left == $right) {
4658 if ($top == $left) {
4659 $returnstyle = new css_style_padding('padding', $top);
4661 $returnstyle = new css_style_padding('padding', "{$top} {$left}");
4663 } else if ($left == $right) {
4664 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom}");
4666 $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom} {$left}");
4668 if ($allimportant) {
4669 $returnstyle->set_important();
4671 return array($returnstyle);
4677 * A padding top style.
4681 * @copyright 2012 Sam Hemelryk
4682 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4684 class css_style_paddingtop
extends css_style_padding
{
4687 * Initialises this style
4689 * @param string $value The value of the style
4690 * @return css_style_paddingtop
4692 public static function init($value) {
4693 return new css_style_paddingtop('padding-top', $value);
4697 * Consolidates this style into a single padding style
4701 public function consolidate_to() {
4707 * A padding right style.
4711 * @copyright 2012 Sam Hemelryk
4712 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4714 class css_style_paddingright
extends css_style_padding
{
4717 * Initialises this style
4719 * @param string $value The value of the style
4720 * @return css_style_paddingright
4722 public static function init($value) {
4723 return new css_style_paddingright('padding-right', $value);
4727 * Consolidates this style into a single padding style
4731 public function consolidate_to() {
4737 * A padding bottom style.
4741 * @copyright 2012 Sam Hemelryk
4742 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4744 class css_style_paddingbottom
extends css_style_padding
{
4747 * Initialises this style
4749 * @param string $value The value of the style
4750 * @return css_style_paddingbottom
4752 public static function init($value) {
4753 return new css_style_paddingbottom('padding-bottom', $value);
4757 * Consolidates this style into a single padding style
4761 public function consolidate_to() {
4767 * A padding left style.
4771 * @copyright 2012 Sam Hemelryk
4772 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4774 class css_style_paddingleft
extends css_style_padding
{
4777 * Initialises this style
4779 * @param string $value The value of the style
4780 * @return css_style_paddingleft
4782 public static function init($value) {
4783 return new css_style_paddingleft('padding-left', $value);
4787 * Consolidates this style into a single padding style
4791 public function consolidate_to() {
4801 * @copyright 2012 Sam Hemelryk
4802 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4804 class css_style_cursor
extends css_style_generic
{
4806 * Initialises a new cursor style
4807 * @param string $value
4808 * @return css_style_cursor
4810 public static function init($value) {
4811 return new css_style_cursor('cursor', $value);
4814 * Cleans the given value and returns it.
4816 * @param string $value
4819 protected function clean_value($value) {
4820 // Allowed values for the cursor style
4821 $allowed = array('auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize',
4822 'pointer', 'progress', 's-resize', 'se-resize', 'sw-resize', 'text', 'w-resize', 'wait', 'inherit');
4823 // Has to be one of the allowed values of an image to use. Loosely match the image... doesn't need to be thorough
4824 if (!in_array($value, $allowed) && !preg_match('#\.[a-zA-Z0-9_\-]{1,5}$#', $value)) {
4825 $this->set_error('Invalid or unexpected cursor value specified: '.$value);
4827 return trim($value);
4832 * A vertical alignment style.
4836 * @copyright 2012 Sam Hemelryk
4837 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4839 class css_style_verticalalign
extends css_style_generic
{
4841 * Initialises a new vertical alignment style
4842 * @param string $value
4843 * @return css_style_verticalalign
4845 public static function init($value) {
4846 return new css_style_verticalalign('vertical-align', $value);
4849 * Cleans the given value and returns it.
4851 * @param string $value
4854 protected function clean_value($value) {
4855 $allowed = array('baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'inherit');
4856 if (!css_is_width($value) && !in_array($value, $allowed)) {
4857 $this->set_error('Invalid vertical-align value specified: '.$value);
4859 return trim($value);
4868 * @copyright 2012 Sam Hemelryk
4869 * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4871 class css_style_float
extends css_style_generic
{
4873 * Initialises a new float style
4874 * @param string $value
4875 * @return css_style_float
4877 public static function init($value) {
4878 return new css_style_float('float', $value);
4881 * Cleans the given value and returns it.
4883 * @param string $value
4886 protected function clean_value($value) {
4887 $allowed = array('left', 'right', 'none', 'inherit');
4888 if (!css_is_width($value) && !in_array($value, $allowed)) {
4889 $this->set_error('Invalid float value specified: '.$value);
4891 return trim($value);