[ Index ]

PHP Cross Reference of Unnamed Project

title

Body

[close]

/lib/ -> csslib.php (source)

   1  <?php
   2  // This file is part of Moodle - http://moodle.org/
   3  //
   4  // Moodle is free software: you can redistribute it and/or modify
   5  // it under the terms of the GNU General Public License as published by
   6  // the Free Software Foundation, either version 3 of the License, or
   7  // (at your option) any later version.
   8  //
   9  // Moodle is distributed in the hope that it will be useful,
  10  // but WITHOUT ANY WARRANTY; without even the implied warranty of
  11  // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
  12  // GNU General Public License for more details.
  13  //
  14  // You should have received a copy of the GNU General Public License
  15  // along with Moodle.  If not, see <http://www.gnu.org/licenses/>.
  16  
  17  /**
  18   * This file contains CSS related class, and function for the CSS optimiser.
  19   *
  20   * Please see the {@link css_optimiser} class for greater detail.
  21   *
  22   * NOTE: these functions are not expected to be used from any addons.
  23   *
  24   * @package core
  25   * @subpackage cssoptimiser
  26   * @copyright 2012 Sam Hemelryk
  27   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
  28   */
  29  
  30  defined('MOODLE_INTERNAL') || die();
  31  
  32  if (!defined('THEME_DESIGNER_CACHE_LIFETIME')) {
  33      // This can be also set in config.php file,
  34      // it needs to be higher than the time it takes to generate all CSS content.
  35      define('THEME_DESIGNER_CACHE_LIFETIME', 10);
  36  }
  37  
  38  /**
  39   * Stores CSS in a file at the given path.
  40   *
  41   * This function either succeeds or throws an exception.
  42   *
  43   * @param theme_config $theme The theme that the CSS belongs to.
  44   * @param string $csspath The path to store the CSS at.
  45   * @param string $csscontent the complete CSS in one string
  46   * @param bool $chunk If set to true these files will be chunked to ensure
  47   *      that no one file contains more than 4095 selectors.
  48   * @param string $chunkurl If the CSS is be chunked then we need to know the URL
  49   *      to use for the chunked files.
  50   */
  51  function css_store_css(theme_config $theme, $csspath, $csscontent, $chunk = false, $chunkurl = null) {
  52      global $CFG;
  53  
  54      clearstatcache();
  55      if (!file_exists(dirname($csspath))) {
  56          @mkdir(dirname($csspath), $CFG->directorypermissions, true);
  57      }
  58  
  59      // Prevent serving of incomplete file from concurrent request,
  60      // the rename() should be more atomic than fwrite().
  61      ignore_user_abort(true);
  62  
  63      // First up write out the single file for all those using decent browsers.
  64      css_write_file($csspath, $csscontent);
  65  
  66      if ($chunk) {
  67          // If we need to chunk the CSS for browsers that are sub-par.
  68          $css = css_chunk_by_selector_count($csscontent, $chunkurl);
  69          $files = count($css);
  70          $count = 1;
  71          foreach ($css as $content) {
  72              if ($count === $files) {
  73                  // If there is more than one file and this IS the last file.
  74                  $filename = preg_replace('#\.css$#', '.0.css', $csspath);
  75              } else {
  76                  // If there is more than one file and this is not the last file.
  77                  $filename = preg_replace('#\.css$#', '.'.$count.'.css', $csspath);
  78              }
  79              $count++;
  80              css_write_file($filename, $content);
  81          }
  82      }
  83  
  84      ignore_user_abort(false);
  85      if (connection_aborted()) {
  86          die;
  87      }
  88  }
  89  
  90  /**
  91   * Writes a CSS file.
  92   *
  93   * @param string $filename
  94   * @param string $content
  95   */
  96  function css_write_file($filename, $content) {
  97      global $CFG;
  98      if ($fp = fopen($filename.'.tmp', 'xb')) {
  99          fwrite($fp, $content);
 100          fclose($fp);
 101          rename($filename.'.tmp', $filename);
 102          @chmod($filename, $CFG->filepermissions);
 103          @unlink($filename.'.tmp'); // Just in case anything fails.
 104      }
 105  }
 106  
 107  /**
 108   * Takes CSS and chunks it if the number of selectors within it exceeds $maxselectors.
 109   *
 110   * The chunking will not split a group of selectors, or a media query. That means that
 111   * if n > $maxselectors and there are n selectors grouped together,
 112   * they will not be chunked and you could end up with more selectors than desired.
 113   * The same applies for a media query that has more than n selectors.
 114   *
 115   * Also, as we do not split group of selectors or media queries, the chunking might
 116   * not be as optimal as it could be, having files with less selectors than it could
 117   * potentially contain.
 118   *
 119   * String functions used here are not compliant with unicode characters. But that is
 120   * not an issue as the syntax of CSS is using ASCII codes. Even if we have unicode
 121   * characters in comments, or in the property 'content: ""', it will behave correcly.
 122   *
 123   * Please note that this strips out the comments if chunking happens.
 124   *
 125   * @param string $css The CSS to chunk.
 126   * @param string $importurl The URL to use for import statements.
 127   * @param int $maxselectors The number of selectors to limit a chunk to.
 128   * @param int $buffer Not used any more.
 129   * @return array An array of CSS chunks.
 130   */
 131  function css_chunk_by_selector_count($css, $importurl, $maxselectors = 4095, $buffer = 50) {
 132  
 133      // Check if we need to chunk this CSS file.
 134      $count = substr_count($css, ',') + substr_count($css, '{');
 135      if ($count < $maxselectors) {
 136          // The number of selectors is less then the max - we're fine.
 137          return array($css);
 138      }
 139  
 140      $chunks = array();                  // The final chunks.
 141      $offsets = array();                 // The indexes to chunk at.
 142      $offset = 0;                        // The current offset.
 143      $selectorcount = 0;                 // The number of selectors since the last split.
 144      $lastvalidoffset = 0;               // The last valid index to split at.
 145      $lastvalidoffsetselectorcount = 0;  // The number of selectors used at the time were could split.
 146      $inrule = 0;                        // The number of rules we are in, should not be greater than 1.
 147      $inmedia = false;                   // Whether or not we are in a media query.
 148      $mediacoming = false;               // Whether or not we are expeting a media query.
 149      $currentoffseterror = null;         // Not null when we have recorded an error for the current split.
 150      $offseterrors = array();            // The offsets where we found errors.
 151  
 152      // Remove the comments. Because it's easier, safer and probably a lot of other good reasons.
 153      $css = preg_replace('#/\*(.*?)\*/#s', '', $css);
 154      $strlen = strlen($css);
 155  
 156      // Walk through the CSS content character by character.
 157      for ($i = 1; $i <= $strlen; $i++) {
 158          $char = $css[$i - 1];
 159          $offset = $i;
 160  
 161          // Is that a media query that I see coming towards us?
 162          if ($char === '@') {
 163              if (!$inmedia && substr($css, $offset, 5) === 'media') {
 164                  $mediacoming = true;
 165              }
 166          }
 167  
 168          // So we are entering a rule or a media query...
 169          if ($char === '{') {
 170              if ($mediacoming) {
 171                  $inmedia = true;
 172                  $mediacoming = false;
 173              } else {
 174                  $inrule++;
 175                  $selectorcount++;
 176              }
 177          }
 178  
 179          // Let's count the number of selectors, but only if we are not in a rule, or in
 180          // the definition of a media query, as they can contain commas too.
 181          if (!$mediacoming && !$inrule && $char === ',') {
 182              $selectorcount++;
 183          }
 184  
 185          // We reached the end of something.
 186          if ($char === '}') {
 187              // Oh, we are in a media query.
 188              if ($inmedia) {
 189                  if (!$inrule) {
 190                      // This is the end of the media query.
 191                      $inmedia = false;
 192                  } else {
 193                      // We were in a rule, in the media query.
 194                      $inrule--;
 195                  }
 196              } else {
 197                  $inrule--;
 198                  // Handle stupid broken CSS where there are too many } brackets,
 199                  // as this can cause it to break (with chunking) where it would
 200                  // coincidentally have worked otherwise.
 201                  if ($inrule < 0) {
 202                      $inrule = 0;
 203                  }
 204              }
 205  
 206              // We are not in a media query, and there is no pending rule, it is safe to split here.
 207              if (!$inmedia && !$inrule) {
 208                  $lastvalidoffset = $offset;
 209                  $lastvalidoffsetselectorcount = $selectorcount;
 210              }
 211          }
 212  
 213          // Alright, this is splitting time...
 214          if ($selectorcount > $maxselectors) {
 215              if (!$lastvalidoffset) {
 216                  // We must have reached more selectors into one set than we were allowed. That means that either
 217                  // the chunk size value is too small, or that we have a gigantic group of selectors, or that a media
 218                  // query contains more selectors than the chunk size. We have to ignore this because we do not
 219                  // support split inside a group of selectors or media query.
 220                  if ($currentoffseterror === null) {
 221                      $currentoffseterror = $offset;
 222                      $offseterrors[] = $currentoffseterror;
 223                  }
 224              } else {
 225                  // We identify the offset to split at and reset the number of selectors found from there.
 226                  $offsets[] = $lastvalidoffset;
 227                  $selectorcount = $selectorcount - $lastvalidoffsetselectorcount;
 228                  $lastvalidoffset = 0;
 229                  $currentoffseterror = null;
 230              }
 231          }
 232      }
 233  
 234      // Report offset errors.
 235      if (!empty($offseterrors)) {
 236          debugging('Could not find a safe place to split at offset(s): ' . implode(', ', $offseterrors) . '. Those were ignored.',
 237              DEBUG_DEVELOPER);
 238      }
 239  
 240      // Now that we have got the offets, we can chunk the CSS.
 241      $offsetcount = count($offsets);
 242      foreach ($offsets as $key => $index) {
 243          $start = 0;
 244          if ($key > 0) {
 245              $start = $offsets[$key - 1];
 246          }
 247          // From somewhere up to the offset.
 248          $chunks[] = substr($css, $start, $index - $start);
 249      }
 250      // Add the last chunk (if there is one), from the last offset to the end of the string.
 251      if (end($offsets) != $strlen) {
 252          $chunks[] = substr($css, end($offsets));
 253      }
 254  
 255      // The array $chunks now contains CSS split into perfect sized chunks.
 256      // Import statements can only appear at the very top of a CSS file.
 257      // Imported sheets are applied in the the order they are imported and
 258      // are followed by the contents of the CSS.
 259      // This is terrible for performance.
 260      // It means we must put the import statements at the top of the last chunk
 261      // to ensure that things are always applied in the correct order.
 262      // This way the chunked files are included in the order they were chunked
 263      // followed by the contents of the final chunk in the actual sheet.
 264      $importcss = '';
 265      $slashargs = strpos($importurl, '.php?') === false;
 266      $parts = count($chunks);
 267      for ($i = 1; $i < $parts; $i++) {
 268          if ($slashargs) {
 269              $importcss .= "@import url({$importurl}/chunk{$i});\n";
 270          } else {
 271              $importcss .= "@import url({$importurl}&chunk={$i});\n";
 272          }
 273      }
 274      $importcss .= end($chunks);
 275      $chunks[key($chunks)] = $importcss;
 276  
 277      return $chunks;
 278  }
 279  
 280  /**
 281   * Sends a cached CSS file
 282   *
 283   * This function sends the cached CSS file. Remember it is generated on the first
 284   * request, then optimised/minified, and finally cached for serving.
 285   *
 286   * @param string $csspath The path to the CSS file we want to serve.
 287   * @param string $etag The revision to make sure we utilise any caches.
 288   */
 289  function css_send_cached_css($csspath, $etag) {
 290      // 60 days only - the revision may get incremented quite often.
 291      $lifetime = 60*60*24*60;
 292  
 293      header('Etag: "'.$etag.'"');
 294      header('Content-Disposition: inline; filename="styles.php"');
 295      header('Last-Modified: '. gmdate('D, d M Y H:i:s', filemtime($csspath)) .' GMT');
 296      header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
 297      header('Pragma: ');
 298      header('Cache-Control: public, max-age='.$lifetime);
 299      header('Accept-Ranges: none');
 300      header('Content-Type: text/css; charset=utf-8');
 301      if (!min_enable_zlib_compression()) {
 302          header('Content-Length: '.filesize($csspath));
 303      }
 304  
 305      readfile($csspath);
 306      die;
 307  }
 308  
 309  /**
 310   * Sends a cached CSS content
 311   *
 312   * @param string $csscontent The actual CSS markup.
 313   * @param string $etag The revision to make sure we utilise any caches.
 314   */
 315  function css_send_cached_css_content($csscontent, $etag) {
 316      // 60 days only - the revision may get incremented quite often.
 317      $lifetime = 60*60*24*60;
 318  
 319      header('Etag: "'.$etag.'"');
 320      header('Content-Disposition: inline; filename="styles.php"');
 321      header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
 322      header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
 323      header('Pragma: ');
 324      header('Cache-Control: public, max-age='.$lifetime);
 325      header('Accept-Ranges: none');
 326      header('Content-Type: text/css; charset=utf-8');
 327      if (!min_enable_zlib_compression()) {
 328          header('Content-Length: '.strlen($csscontent));
 329      }
 330  
 331      echo($csscontent);
 332      die;
 333  }
 334  
 335  /**
 336   * Sends CSS directly without caching it.
 337   *
 338   * This function takes a raw CSS string, optimises it if required, and then
 339   * serves it.
 340   * Turning both themedesignermode and CSS optimiser on at the same time is awful
 341   * for performance because of the optimiser running here. However it was done so
 342   * that theme designers could utilise the optimised output during development to
 343   * help them optimise their CSS... not that they should write lazy CSS.
 344   *
 345   * @param string $css
 346   */
 347  function css_send_uncached_css($css) {
 348      header('Content-Disposition: inline; filename="styles_debug.php"');
 349      header('Last-Modified: '. gmdate('D, d M Y H:i:s', time()) .' GMT');
 350      header('Expires: '. gmdate('D, d M Y H:i:s', time() + THEME_DESIGNER_CACHE_LIFETIME) .' GMT');
 351      header('Pragma: ');
 352      header('Accept-Ranges: none');
 353      header('Content-Type: text/css; charset=utf-8');
 354  
 355      if (is_array($css)) {
 356          $css = implode("\n\n", $css);
 357      }
 358      echo $css;
 359      die;
 360  }
 361  
 362  /**
 363   * Send file not modified headers
 364   *
 365   * @param int $lastmodified
 366   * @param string $etag
 367   */
 368  function css_send_unmodified($lastmodified, $etag) {
 369      // 60 days only - the revision may get incremented quite often.
 370      $lifetime = 60*60*24*60;
 371      header('HTTP/1.1 304 Not Modified');
 372      header('Expires: '. gmdate('D, d M Y H:i:s', time() + $lifetime) .' GMT');
 373      header('Cache-Control: public, max-age='.$lifetime);
 374      header('Content-Type: text/css; charset=utf-8');
 375      header('Etag: "'.$etag.'"');
 376      if ($lastmodified) {
 377          header('Last-Modified: '. gmdate('D, d M Y H:i:s', $lastmodified) .' GMT');
 378      }
 379      die;
 380  }
 381  
 382  /**
 383   * Sends a 404 message about CSS not being found.
 384   */
 385  function css_send_css_not_found() {
 386      header('HTTP/1.0 404 not found');
 387      die('CSS was not found, sorry.');
 388  }
 389  
 390  /**
 391   * Determines if the given value is a valid CSS colour.
 392   *
 393   * A CSS colour can be one of the following:
 394   *    - Hex colour:  #AA66BB
 395   *    - RGB colour:  rgb(0-255, 0-255, 0-255)
 396   *    - RGBA colour: rgba(0-255, 0-255, 0-255, 0-1)
 397   *    - HSL colour:  hsl(0-360, 0-100%, 0-100%)
 398   *    - HSLA colour: hsla(0-360, 0-100%, 0-100%, 0-1)
 399   *
 400   * Or a recognised browser colour mapping {@link css_optimiser::$htmlcolours}
 401   *
 402   * @param string $value The colour value to check
 403   * @return bool
 404   */
 405  function css_is_colour($value) {
 406      $value = trim($value);
 407  
 408      $hex  = '/^#([a-fA-F0-9]{1,3}|[a-fA-F0-9]{6})$/';
 409      $rgb  = '#^rgb\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*\)$#i';
 410      $rgba = '#^rgba\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1,3})\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
 411      $hsl  = '#^hsl\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*\)$#i';
 412      $hsla = '#^hsla\s*\(\s*(\d{1,3})\s*,\s*(\d{1,3})\%\s*,\s*(\d{1,3})\%\s*,\s*(\d{1}(\.\d+)?)\s*\)$#i';
 413  
 414      if (in_array(strtolower($value), array('inherit'))) {
 415          return true;
 416      } else if (preg_match($hex, $value)) {
 417          return true;
 418      } else if (in_array(strtolower($value), array_keys(css_optimiser::$htmlcolours))) {
 419          return true;
 420      } else if (preg_match($rgb, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
 421          // It is an RGB colour.
 422          return true;
 423      } else if (preg_match($rgba, $value, $m) && $m[1] < 256 && $m[2] < 256 && $m[3] < 256) {
 424          // It is an RGBA colour.
 425          return true;
 426      } else if (preg_match($hsl, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
 427          // It is an HSL colour.
 428          return true;
 429      } else if (preg_match($hsla, $value, $m) && $m[1] <= 360 && $m[2] <= 100 && $m[3] <= 100) {
 430          // It is an HSLA colour.
 431          return true;
 432      }
 433      // Doesn't look like a colour.
 434      return false;
 435  }
 436  
 437  /**
 438   * Returns true is the passed value looks like a CSS width.
 439   * In order to pass this test the value must be purely numerical or end with a
 440   * valid CSS unit term.
 441   *
 442   * @param string|int $value
 443   * @return boolean
 444   */
 445  function css_is_width($value) {
 446      $value = trim($value);
 447      if (in_array(strtolower($value), array('auto', 'inherit'))) {
 448          return true;
 449      }
 450      if ((string)$value === '0' || preg_match('#^(\-\s*)?(\d*\.)?(\d+)\s*(em|px|pt|\%|in|cm|mm|ex|pc)$#i', $value)) {
 451          return true;
 452      }
 453      return false;
 454  }
 455  
 456  /**
 457   * A simple sorting function to sort two array values on the number of items they contain
 458   *
 459   * @param array $a
 460   * @param array $b
 461   * @return int
 462   */
 463  function css_sort_by_count(array $a, array $b) {
 464      $a = count($a);
 465      $b = count($b);
 466      if ($a == $b) {
 467          return 0;
 468      }
 469      return ($a > $b) ? -1 : 1;
 470  }
 471  
 472  /**
 473   * A basic CSS optimiser that strips out unwanted things and then processes CSS organising and cleaning styles.
 474   *
 475   * This CSS optimiser works by reading through a CSS string one character at a
 476   * time and building an object structure of the CSS.
 477   * As part of that processing styles are expanded out as much as they can be to
 478   * ensure we collect all mappings, at the end of the processing those styles are
 479   * then combined into an optimised form to keep them as short as possible.
 480   *
 481   * @package core
 482   * @subpackage cssoptimiser
 483   * @copyright 2012 Sam Hemelryk
 484   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
 485   */
 486  class css_optimiser {
 487  
 488      /**
 489       * Used when the processor is about to start processing.
 490       * Processing states. Used internally.
 491       */
 492      const PROCESSING_START = 0;
 493  
 494      /**
 495       * Used when the processor is currently processing a selector.
 496       * Processing states. Used internally.
 497       */
 498      const PROCESSING_SELECTORS = 0;
 499  
 500      /**
 501       * Used when the processor is currently processing a style.
 502       * Processing states. Used internally.
 503       */
 504      const PROCESSING_STYLES = 1;
 505  
 506      /**
 507       * Used when the processor is currently processing a comment.
 508       * Processing states. Used internally.
 509       */
 510      const PROCESSING_COMMENT = 2;
 511  
 512      /**
 513       * Used when the processor is currently processing an @ rule.
 514       * Processing states. Used internally.
 515       */
 516      const PROCESSING_ATRULE = 3;
 517  
 518      /**
 519       * The raw string length before optimisation.
 520       * Stats variables set during and after processing
 521       * @var int
 522       */
 523      protected $rawstrlen = 0;
 524  
 525      /**
 526       * The number of comments that were removed during optimisation.
 527       * Stats variables set during and after processing
 528       * @var int
 529       */
 530      protected $commentsincss = 0;
 531  
 532      /**
 533       * The number of rules in the CSS before optimisation.
 534       * Stats variables set during and after processing
 535       * @var int
 536       */
 537      protected $rawrules = 0;
 538  
 539      /**
 540       * The number of selectors using in CSS rules before optimisation.
 541       * Stats variables set during and after processing
 542       * @var int
 543       */
 544      protected $rawselectors = 0;
 545  
 546      /**
 547       * The string length after optimisation.
 548       * Stats variables set during and after processing
 549       * @var int
 550       */
 551      protected $optimisedstrlen = 0;
 552  
 553      /**
 554       * The number of rules after optimisation.
 555       * Stats variables set during and after processing
 556       * @var int
 557       */
 558      protected $optimisedrules = 0;
 559  
 560      /**
 561       * The number of selectors used in rules after optimisation.
 562       * Stats variables set during and after processing
 563       * @var int
 564       */
 565      protected $optimisedselectors = 0;
 566  
 567      /**
 568       * The start time of the optimisation.
 569       * Stats variables set during and after processing
 570       * @var int
 571       */
 572      protected $timestart = 0;
 573  
 574      /**
 575       * The end time of the optimisation.
 576       * Stats variables set during and after processing
 577       * @var int
 578       */
 579      protected $timecomplete = 0;
 580  
 581      /**
 582       * Will be set to any errors that may have occured during processing.
 583       * This is updated only at the end of processing NOT during.
 584       *
 585       * @var array
 586       */
 587      protected $errors = array();
 588  
 589      /**
 590       * Processes incoming CSS optimising it and then returning it.
 591       *
 592       * @param string $css The raw CSS to optimise
 593       * @return string The optimised CSS
 594       */
 595      public function process($css) {
 596          // Easiest win there is.
 597          $css = trim($css);
 598  
 599          $this->reset_stats();
 600          $this->timestart = microtime(true);
 601          $this->rawstrlen = strlen($css);
 602  
 603          // Don't try to process files with no content... it just doesn't make sense.
 604          // But we should produce an error for them, an empty CSS file will lead to a
 605          // useless request for those running theme designer mode.
 606          if ($this->rawstrlen === 0) {
 607              $this->errors[] = 'Skipping file as it has no content.';
 608              return '';
 609          }
 610  
 611          // First up we need to remove all line breaks - this allows us to instantly
 612          // reduce our processing requirements and as we will process everything
 613          // into a new structure there's really nothing lost.
 614          $css = preg_replace('#\r?\n#', ' ', $css);
 615  
 616          // Next remove the comments... no need to them in an optimised world and
 617          // knowing they're all gone allows us to REALLY make our processing simpler.
 618          $css = preg_replace('#/\*(.*?)\*/#m', '', $css, -1, $this->commentsincss);
 619  
 620          $medias = array(
 621              'all' => new css_media()
 622          );
 623          $imports = array();
 624          $charset = false;
 625          // Keyframes are used for CSS animation they will be processed right at the very end.
 626          $keyframes = array();
 627  
 628          $currentprocess = self::PROCESSING_START;
 629          $currentrule = css_rule::init();
 630          $currentselector = css_selector::init();
 631          $inquotes = false;      // ' or "
 632          $inbraces = false;      // {
 633          $inbrackets = false;    // [
 634          $inparenthesis = false; // (
 635          /* @var css_media $currentmedia */
 636          $currentmedia = $medias['all'];
 637          $currentatrule = null;
 638          $suspectatrule = false;
 639  
 640          $buffer = '';
 641          $char = null;
 642  
 643          // Next we are going to iterate over every single character in $css.
 644          // This is why we removed line breaks and comments!
 645          for ($i = 0; $i < $this->rawstrlen; $i++) {
 646              $lastchar = $char;
 647              $char = substr($css, $i, 1);
 648              if ($char == '@' && $buffer == '') {
 649                  $suspectatrule = true;
 650              }
 651              switch ($currentprocess) {
 652                  // Start processing an @ rule e.g. @media, @page, @keyframes.
 653                  case self::PROCESSING_ATRULE:
 654                      switch ($char) {
 655                          case ';':
 656                              if (!$inbraces) {
 657                                  $buffer .= $char;
 658                                  if ($currentatrule == 'import') {
 659                                      $imports[] = $buffer;
 660                                      $currentprocess = self::PROCESSING_SELECTORS;
 661                                  } else if ($currentatrule == 'charset') {
 662                                      $charset = $buffer;
 663                                      $currentprocess = self::PROCESSING_SELECTORS;
 664                                  }
 665                              }
 666                              if ($currentatrule !== 'media') {
 667                                  $buffer = '';
 668                                  $currentatrule = false;
 669                              }
 670                              // Continue 1: The switch processing chars
 671                              // Continue 2: The switch processing the state
 672                              // Continue 3: The for loop.
 673                              continue 3;
 674                          case '{':
 675                              $regexmediabasic = '#\s*@media\s*([a-zA-Z0-9]+(\s*,\s*[a-zA-Z0-9]+)*)\s*{#';
 676                              $regexadvmedia = '#\s*@media\s*([^{]+)#';
 677                              $regexkeyframes = '#@((\-moz\-|\-webkit\-|\-ms\-|\-o\-)?keyframes)\s*([^\s]+)#';
 678  
 679                              if ($currentatrule == 'media' && preg_match($regexmediabasic, $buffer, $matches)) {
 680                                  // Basic media declaration.
 681                                  $mediatypes = str_replace(' ', '', $matches[1]);
 682                                  if (!array_key_exists($mediatypes, $medias)) {
 683                                      $medias[$mediatypes] = new css_media($mediatypes);
 684                                  }
 685                                  $currentmedia = $medias[$mediatypes];
 686                                  $currentprocess = self::PROCESSING_SELECTORS;
 687                                  $buffer = '';
 688                              } else if ($currentatrule == 'media' && preg_match($regexadvmedia, $buffer, $matches)) {
 689                                  // Advanced media query declaration http://www.w3.org/TR/css3-mediaqueries/.
 690                                  $mediatypes = $matches[1];
 691                                  $hash = md5($mediatypes);
 692                                  $medias[$hash] = new css_media($mediatypes);
 693                                  $currentmedia = $medias[$hash];
 694                                  $currentprocess = self::PROCESSING_SELECTORS;
 695                                  $buffer = '';
 696                              } else if ($currentatrule == 'keyframes' && preg_match($regexkeyframes, $buffer, $matches)) {
 697                                  // Keyframes declaration, we treat it exactly like a @media declaration except we don't allow
 698                                  // them to be overridden to ensure we don't mess anything up. (means we keep everything in order).
 699                                  $keyframefor = $matches[1];
 700                                  $keyframename = $matches[3];
 701                                  $keyframe = new css_keyframe($keyframefor, $keyframename);
 702                                  $keyframes[] = $keyframe;
 703                                  $currentmedia = $keyframe;
 704                                  $currentprocess = self::PROCESSING_SELECTORS;
 705                                  $buffer = '';
 706                              }
 707                              // Continue 1: The switch processing chars
 708                              // Continue 2: The switch processing the state
 709                              // Continue 3: The for loop.
 710                              continue 3;
 711                      }
 712                      break;
 713                  // Start processing selectors.
 714                  case self::PROCESSING_START:
 715                  case self::PROCESSING_SELECTORS:
 716                      $regexatrule = '#@(media|import|charset|(\-moz\-|\-webkit\-|\-ms\-|\-o\-)?(keyframes))\s*#';
 717                      switch ($char) {
 718                          case '[':
 719                              $inbrackets ++;
 720                              $buffer .= $char;
 721                              // Continue 1: The switch processing chars
 722                              // Continue 2: The switch processing the state
 723                              // Continue 3: The for loop.
 724                              continue 3;
 725                          case ']':
 726                              $inbrackets --;
 727                              $buffer .= $char;
 728                              // Continue 1: The switch processing chars
 729                              // Continue 2: The switch processing the state
 730                              // Continue 3: The for loop.
 731                              continue 3;
 732                          case ' ':
 733                              if ($inbrackets) {
 734                                  // Continue 1: The switch processing chars
 735                                  // Continue 2: The switch processing the state
 736                                  // Continue 3: The for loop.
 737                                  continue 3;
 738                              }
 739                              if (!empty($buffer)) {
 740                                  // Check for known @ rules.
 741                                  if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
 742                                      $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
 743                                      $currentprocess = self::PROCESSING_ATRULE;
 744                                      $buffer .= $char;
 745                                  } else {
 746                                      $currentselector->add($buffer);
 747                                      $buffer = '';
 748                                  }
 749                              }
 750                              $suspectatrule = false;
 751                              // Continue 1: The switch processing chars
 752                              // Continue 2: The switch processing the state
 753                              // Continue 3: The for loop.
 754                              continue 3;
 755                          case '{':
 756                              if ($inbrackets) {
 757                                  // Continue 1: The switch processing chars
 758                                  // Continue 2: The switch processing the state
 759                                  // Continue 3: The for loop.
 760                                  continue 3;
 761                              }
 762                              // Check for known @ rules.
 763                              if ($suspectatrule && preg_match($regexatrule, $buffer, $matches)) {
 764                                  // Ahh we've been in an @rule, lets rewind one and have the @rule case process this.
 765                                  $currentatrule = (!empty($matches[3]))?$matches[3]:$matches[1];
 766                                  $currentprocess = self::PROCESSING_ATRULE;
 767                                  $i--;
 768                                  $suspectatrule = false;
 769                                  // Continue 1: The switch processing chars
 770                                  // Continue 2: The switch processing the state
 771                                  // Continue 3: The for loop.
 772                                  continue 3;
 773                              }
 774                              if ($buffer !== '') {
 775                                  $currentselector->add($buffer);
 776                              }
 777                              $currentrule->add_selector($currentselector);
 778                              $currentselector = css_selector::init();
 779                              $currentprocess = self::PROCESSING_STYLES;
 780  
 781                              $buffer = '';
 782                              // Continue 1: The switch processing chars
 783                              // Continue 2: The switch processing the state
 784                              // Continue 3: The for loop.
 785                              continue 3;
 786                          case '}':
 787                              if ($inbrackets) {
 788                                  // Continue 1: The switch processing chars
 789                                  // Continue 2: The switch processing the state
 790                                  // Continue 3: The for loop.
 791                                  continue 3;
 792                              }
 793                              if ($currentatrule == 'media') {
 794                                  $currentmedia = $medias['all'];
 795                                  $currentatrule = false;
 796                                  $buffer = '';
 797                              } else if (strpos($currentatrule, 'keyframes') !== false) {
 798                                  $currentmedia = $medias['all'];
 799                                  $currentatrule = false;
 800                                  $buffer = '';
 801                              }
 802                              // Continue 1: The switch processing chars
 803                              // Continue 2: The switch processing the state
 804                              // Continue 3: The for loop.
 805                              continue 3;
 806                          case ',':
 807                              if ($inbrackets) {
 808                                  // Continue 1: The switch processing chars
 809                                  // Continue 2: The switch processing the state
 810                                  // Continue 3: The for loop.
 811                                  continue 3;
 812                              }
 813                              $currentselector->add($buffer);
 814                              $currentrule->add_selector($currentselector);
 815                              $currentselector = css_selector::init();
 816                              $buffer = '';
 817                              // Continue 1: The switch processing chars
 818                              // Continue 2: The switch processing the state
 819                              // Continue 3: The for loop.
 820                              continue 3;
 821                      }
 822                      break;
 823                  // Start processing styles.
 824                  case self::PROCESSING_STYLES:
 825                      if ($char == '"' || $char == "'") {
 826                          if ($inquotes === false) {
 827                              $inquotes = $char;
 828                          }
 829                          if ($inquotes === $char && $lastchar !== '\\') {
 830                              $inquotes = false;
 831                          }
 832                      }
 833                      if ($inquotes) {
 834                          $buffer .= $char;
 835                          continue 2;
 836                      }
 837                      switch ($char) {
 838                          case ';':
 839                              if ($inparenthesis) {
 840                                  $buffer .= $char;
 841                                  // Continue 1: The switch processing chars
 842                                  // Continue 2: The switch processing the state
 843                                  // Continue 3: The for loop.
 844                                  continue 3;
 845                              }
 846                              $currentrule->add_style($buffer);
 847                              $buffer = '';
 848                              $inquotes = false;
 849                              // Continue 1: The switch processing chars
 850                              // Continue 2: The switch processing the state
 851                              // Continue 3: The for loop.
 852                              continue 3;
 853                          case '}':
 854                              $currentrule->add_style($buffer);
 855                              $this->rawselectors += $currentrule->get_selector_count();
 856  
 857                              $currentmedia->add_rule($currentrule);
 858  
 859                              $currentrule = css_rule::init();
 860                              $currentprocess = self::PROCESSING_SELECTORS;
 861                              $this->rawrules++;
 862                              $buffer = '';
 863                              $inquotes = false;
 864                              $inparenthesis = false;
 865                              // Continue 1: The switch processing chars
 866                              // Continue 2: The switch processing the state
 867                              // Continue 3: The for loop.
 868                              continue 3;
 869                          case '(':
 870                              $inparenthesis = true;
 871                              $buffer .= $char;
 872                              // Continue 1: The switch processing chars
 873                              // Continue 2: The switch processing the state
 874                              // Continue 3: The for loop.
 875                              continue 3;
 876                          case ')':
 877                              $inparenthesis = false;
 878                              $buffer .= $char;
 879                              // Continue 1: The switch processing chars
 880                              // Continue 2: The switch processing the state
 881                              // Continue 3: The for loop.
 882                              continue 3;
 883                      }
 884                      break;
 885              }
 886              $buffer .= $char;
 887          }
 888  
 889          foreach ($medias as $media) {
 890              $this->optimise($media);
 891          }
 892          $css = $this->produce_css($charset, $imports, $medias, $keyframes);
 893  
 894          $this->timecomplete = microtime(true);
 895          return trim($css);
 896      }
 897  
 898      /**
 899       * Produces CSS for the given charset, imports, media, and keyframes
 900       * @param string $charset
 901       * @param array $imports
 902       * @param css_media[] $medias
 903       * @param css_keyframe[] $keyframes
 904       * @return string
 905       */
 906      protected function produce_css($charset, array $imports, array $medias, array $keyframes) {
 907          $css = '';
 908          if (!empty($charset)) {
 909              $imports[] = $charset;
 910          }
 911          if (!empty($imports)) {
 912              $css .= implode("\n", $imports);
 913              $css .= "\n\n";
 914          }
 915  
 916          $cssreset = array();
 917          $cssstandard = array();
 918          $csskeyframes = array();
 919  
 920          // Process each media declaration individually.
 921          foreach ($medias as $media) {
 922              // If this declaration applies to all media types.
 923              if (in_array('all', $media->get_types())) {
 924                  // Collect all rules that represet reset rules and remove them from the media object at the same time.
 925                  // We do this because we prioritise reset rules to the top of a CSS output. This ensures that they
 926                  // can't end up out of order because of optimisation.
 927                  $resetrules = $media->get_reset_rules(true);
 928                  if (!empty($resetrules)) {
 929                      $cssreset[] = css_writer::media('all', $resetrules);
 930                  }
 931              }
 932              // Get the standard cSS.
 933              $cssstandard[] = $media->out();
 934          }
 935  
 936          // Finally if there are any keyframe declarations process them now.
 937          if (count($keyframes) > 0) {
 938              foreach ($keyframes as $keyframe) {
 939                  $this->optimisedrules += $keyframe->count_rules();
 940                  $this->optimisedselectors +=  $keyframe->count_selectors();
 941                  if ($keyframe->has_errors()) {
 942                      $this->errors += $keyframe->get_errors();
 943                  }
 944                  $csskeyframes[] = $keyframe->out();
 945              }
 946          }
 947  
 948          // Join it all together.
 949          $css .= join('', $cssreset);
 950          $css .= join('', $cssstandard);
 951          $css .= join('', $csskeyframes);
 952  
 953          // Record the strlenght of the now optimised CSS.
 954          $this->optimisedstrlen = strlen($css);
 955  
 956          // Return the now produced CSS.
 957          return $css;
 958      }
 959  
 960      /**
 961       * Optimises the CSS rules within a rule collection of one form or another
 962       *
 963       * @param css_rule_collection $media
 964       * @return void This function acts in reference
 965       */
 966      protected function optimise(css_rule_collection $media) {
 967          $media->organise_rules_by_selectors();
 968          $this->optimisedrules += $media->count_rules();
 969          $this->optimisedselectors +=  $media->count_selectors();
 970          if ($media->has_errors()) {
 971              $this->errors += $media->get_errors();
 972          }
 973      }
 974  
 975      /**
 976       * Returns an array of stats from the last processing run
 977       * @return string
 978       */
 979      public function get_stats() {
 980          $stats = array(
 981              'timestart'             => $this->timestart,
 982              'timecomplete'          => $this->timecomplete,
 983              'timetaken'             => round($this->timecomplete - $this->timestart, 4),
 984              'commentsincss'         => $this->commentsincss,
 985              'rawstrlen'             => $this->rawstrlen,
 986              'rawselectors'          => $this->rawselectors,
 987              'rawrules'              => $this->rawrules,
 988              'optimisedstrlen'       => $this->optimisedstrlen,
 989              'optimisedrules'        => $this->optimisedrules,
 990              'optimisedselectors'    => $this->optimisedselectors,
 991              'improvementstrlen'     => '-',
 992              'improvementrules'     => '-',
 993              'improvementselectors'     => '-',
 994          );
 995          // Avoid division by 0 errors by checking we have valid raw values.
 996          if ($this->rawstrlen > 0) {
 997              $stats['improvementstrlen'] = round(100 - ($this->optimisedstrlen / $this->rawstrlen) * 100, 1).'%';
 998          }
 999          if ($this->rawrules > 0) {
1000              $stats['improvementrules'] = round(100 - ($this->optimisedrules / $this->rawrules) * 100, 1).'%';
1001          }
1002          if ($this->rawselectors > 0) {
1003              $stats['improvementselectors'] = round(100 - ($this->optimisedselectors / $this->rawselectors) * 100, 1).'%';
1004          }
1005          return $stats;
1006      }
1007  
1008      /**
1009       * Returns true if any errors have occured during processing
1010       *
1011       * @return bool
1012       */
1013      public function has_errors() {
1014          return !empty($this->errors);
1015      }
1016  
1017      /**
1018       * Returns an array of errors that have occured
1019       *
1020       * @param bool $clear If set to true the errors will be cleared after being returned.
1021       * @return array
1022       */
1023      public function get_errors($clear = false) {
1024          $errors = $this->errors;
1025          if ($clear) {
1026              // Reset the error array.
1027              $this->errors = array();
1028          }
1029          return $errors;
1030      }
1031  
1032      /**
1033       * Returns any errors as a string that can be included in CSS.
1034       *
1035       * @return string
1036       */
1037      public function output_errors_css() {
1038          $computedcss  = "/****************************************\n";
1039          $computedcss .= " *--- Errors found during processing ----\n";
1040          foreach ($this->errors as $error) {
1041              $computedcss .= preg_replace('#^#m', '* ', $error);
1042          }
1043          $computedcss .= " ****************************************/\n\n";
1044          return $computedcss;
1045      }
1046  
1047      /**
1048       * Returns a string to display stats about the last generation within CSS output
1049       *
1050       * @return string
1051       */
1052      public function output_stats_css() {
1053  
1054          $computedcss  = "/****************************************\n";
1055          $computedcss .= " *------- CSS Optimisation stats --------\n";
1056  
1057          if ($this->rawstrlen === 0) {
1058              $computedcss .= " File not processed as it has no content /\n\n";
1059              $computedcss .= " ****************************************/\n\n";
1060              return $computedcss;
1061          } else if ($this->rawrules === 0) {
1062              $computedcss .= " File contained no rules to be processed /\n\n";
1063              $computedcss .= " ****************************************/\n\n";
1064              return $computedcss;
1065          }
1066  
1067          $stats = $this->get_stats();
1068  
1069          $computedcss .= " *  ".date('r')."\n";
1070          $computedcss .= " *  {$stats['commentsincss']}  \t comments removed\n";
1071          $computedcss .= " *  Optimisation took {$stats['timetaken']} seconds\n";
1072          $computedcss .= " *--------------- before ----------------\n";
1073          $computedcss .= " *  {$stats['rawstrlen']}  \t chars read in\n";
1074          $computedcss .= " *  {$stats['rawrules']}  \t rules read in\n";
1075          $computedcss .= " *  {$stats['rawselectors']}  \t total selectors\n";
1076          $computedcss .= " *---------------- after ----------------\n";
1077          $computedcss .= " *  {$stats['optimisedstrlen']}  \t chars once optimized\n";
1078          $computedcss .= " *  {$stats['optimisedrules']}  \t optimized rules\n";
1079          $computedcss .= " *  {$stats['optimisedselectors']}  \t total selectors once optimized\n";
1080          $computedcss .= " *---------------- stats ----------------\n";
1081          $computedcss .= " *  {$stats['improvementstrlen']}  \t reduction in chars\n";
1082          $computedcss .= " *  {$stats['improvementrules']}  \t reduction in rules\n";
1083          $computedcss .= " *  {$stats['improvementselectors']}  \t reduction in selectors\n";
1084          $computedcss .= " ****************************************/\n\n";
1085  
1086          return $computedcss;
1087      }
1088  
1089      /**
1090       * Resets the stats ready for another fresh processing
1091       */
1092      public function reset_stats() {
1093          $this->commentsincss = 0;
1094          $this->optimisedrules = 0;
1095          $this->optimisedselectors = 0;
1096          $this->optimisedstrlen = 0;
1097          $this->rawrules = 0;
1098          $this->rawselectors = 0;
1099          $this->rawstrlen = 0;
1100          $this->timecomplete = 0;
1101          $this->timestart = 0;
1102      }
1103  
1104      /**
1105       * An array of the common HTML colours that are supported by most browsers.
1106       *
1107       * This reference table is used to allow us to unify colours, and will aid
1108       * us in identifying buggy CSS using unsupported colours.
1109       *
1110       * @var string[]
1111       */
1112      public static $htmlcolours = array(
1113          'aliceblue' => '#F0F8FF',
1114          'antiquewhite' => '#FAEBD7',
1115          'aqua' => '#00FFFF',
1116          'aquamarine' => '#7FFFD4',
1117          'azure' => '#F0FFFF',
1118          'beige' => '#F5F5DC',
1119          'bisque' => '#FFE4C4',
1120          'black' => '#000000',
1121          'blanchedalmond' => '#FFEBCD',
1122          'blue' => '#0000FF',
1123          'blueviolet' => '#8A2BE2',
1124          'brown' => '#A52A2A',
1125          'burlywood' => '#DEB887',
1126          'cadetblue' => '#5F9EA0',
1127          'chartreuse' => '#7FFF00',
1128          'chocolate' => '#D2691E',
1129          'coral' => '#FF7F50',
1130          'cornflowerblue' => '#6495ED',
1131          'cornsilk' => '#FFF8DC',
1132          'crimson' => '#DC143C',
1133          'cyan' => '#00FFFF',
1134          'darkblue' => '#00008B',
1135          'darkcyan' => '#008B8B',
1136          'darkgoldenrod' => '#B8860B',
1137          'darkgray' => '#A9A9A9',
1138          'darkgrey' => '#A9A9A9',
1139          'darkgreen' => '#006400',
1140          'darkKhaki' => '#BDB76B',
1141          'darkmagenta' => '#8B008B',
1142          'darkolivegreen' => '#556B2F',
1143          'arkorange' => '#FF8C00',
1144          'darkorchid' => '#9932CC',
1145          'darkred' => '#8B0000',
1146          'darksalmon' => '#E9967A',
1147          'darkseagreen' => '#8FBC8F',
1148          'darkslateblue' => '#483D8B',
1149          'darkslategray' => '#2F4F4F',
1150          'darkslategrey' => '#2F4F4F',
1151          'darkturquoise' => '#00CED1',
1152          'darkviolet' => '#9400D3',
1153          'deeppink' => '#FF1493',
1154          'deepskyblue' => '#00BFFF',
1155          'dimgray' => '#696969',
1156          'dimgrey' => '#696969',
1157          'dodgerblue' => '#1E90FF',
1158          'firebrick' => '#B22222',
1159          'floralwhite' => '#FFFAF0',
1160          'forestgreen' => '#228B22',
1161          'fuchsia' => '#FF00FF',
1162          'gainsboro' => '#DCDCDC',
1163          'ghostwhite' => '#F8F8FF',
1164          'gold' => '#FFD700',
1165          'goldenrod' => '#DAA520',
1166          'gray' => '#808080',
1167          'grey' => '#808080',
1168          'green' => '#008000',
1169          'greenyellow' => '#ADFF2F',
1170          'honeydew' => '#F0FFF0',
1171          'hotpink' => '#FF69B4',
1172          'indianred ' => '#CD5C5C',
1173          'indigo ' => '#4B0082',
1174          'ivory' => '#FFFFF0',
1175          'khaki' => '#F0E68C',
1176          'lavender' => '#E6E6FA',
1177          'lavenderblush' => '#FFF0F5',
1178          'lawngreen' => '#7CFC00',
1179          'lemonchiffon' => '#FFFACD',
1180          'lightblue' => '#ADD8E6',
1181          'lightcoral' => '#F08080',
1182          'lightcyan' => '#E0FFFF',
1183          'lightgoldenrodyellow' => '#FAFAD2',
1184          'lightgray' => '#D3D3D3',
1185          'lightgrey' => '#D3D3D3',
1186          'lightgreen' => '#90EE90',
1187          'lightpink' => '#FFB6C1',
1188          'lightsalmon' => '#FFA07A',
1189          'lightseagreen' => '#20B2AA',
1190          'lightskyblue' => '#87CEFA',
1191          'lightslategray' => '#778899',
1192          'lightslategrey' => '#778899',
1193          'lightsteelblue' => '#B0C4DE',
1194          'lightyellow' => '#FFFFE0',
1195          'lime' => '#00FF00',
1196          'limegreen' => '#32CD32',
1197          'linen' => '#FAF0E6',
1198          'magenta' => '#FF00FF',
1199          'maroon' => '#800000',
1200          'mediumaquamarine' => '#66CDAA',
1201          'mediumblue' => '#0000CD',
1202          'mediumorchid' => '#BA55D3',
1203          'mediumpurple' => '#9370D8',
1204          'mediumseagreen' => '#3CB371',
1205          'mediumslateblue' => '#7B68EE',
1206          'mediumspringgreen' => '#00FA9A',
1207          'mediumturquoise' => '#48D1CC',
1208          'mediumvioletred' => '#C71585',
1209          'midnightblue' => '#191970',
1210          'mintcream' => '#F5FFFA',
1211          'mistyrose' => '#FFE4E1',
1212          'moccasin' => '#FFE4B5',
1213          'navajowhite' => '#FFDEAD',
1214          'navy' => '#000080',
1215          'oldlace' => '#FDF5E6',
1216          'olive' => '#808000',
1217          'olivedrab' => '#6B8E23',
1218          'orange' => '#FFA500',
1219          'orangered' => '#FF4500',
1220          'orchid' => '#DA70D6',
1221          'palegoldenrod' => '#EEE8AA',
1222          'palegreen' => '#98FB98',
1223          'paleturquoise' => '#AFEEEE',
1224          'palevioletred' => '#D87093',
1225          'papayawhip' => '#FFEFD5',
1226          'peachpuff' => '#FFDAB9',
1227          'peru' => '#CD853F',
1228          'pink' => '#FFC0CB',
1229          'plum' => '#DDA0DD',
1230          'powderblue' => '#B0E0E6',
1231          'purple' => '#800080',
1232          'red' => '#FF0000',
1233          'rosybrown' => '#BC8F8F',
1234          'royalblue' => '#4169E1',
1235          'saddlebrown' => '#8B4513',
1236          'salmon' => '#FA8072',
1237          'sandybrown' => '#F4A460',
1238          'seagreen' => '#2E8B57',
1239          'seashell' => '#FFF5EE',
1240          'sienna' => '#A0522D',
1241          'silver' => '#C0C0C0',
1242          'skyblue' => '#87CEEB',
1243          'slateblue' => '#6A5ACD',
1244          'slategray' => '#708090',
1245          'slategrey' => '#708090',
1246          'snow' => '#FFFAFA',
1247          'springgreen' => '#00FF7F',
1248          'steelblue' => '#4682B4',
1249          'tan' => '#D2B48C',
1250          'teal' => '#008080',
1251          'thistle' => '#D8BFD8',
1252          'tomato' => '#FF6347',
1253          'transparent' => 'transparent',
1254          'turquoise' => '#40E0D0',
1255          'violet' => '#EE82EE',
1256          'wheat' => '#F5DEB3',
1257          'white' => '#FFFFFF',
1258          'whitesmoke' => '#F5F5F5',
1259          'yellow' => '#FFFF00',
1260          'yellowgreen' => '#9ACD32'
1261      );
1262  }
1263  
1264  /**
1265   * Used to prepare CSS strings
1266   *
1267   * @package core
1268   * @subpackage cssoptimiser
1269   * @copyright 2012 Sam Hemelryk
1270   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1271   */
1272  abstract class css_writer {
1273  
1274      /**
1275       * The current indent level
1276       * @var int
1277       */
1278      protected static $indent = 0;
1279  
1280      /**
1281       * Returns true if the output should still maintain minimum formatting.
1282       * @return bool
1283       */
1284      protected static function is_pretty() {
1285          global $CFG;
1286          return (!empty($CFG->cssoptimiserpretty));
1287      }
1288  
1289      /**
1290       * Returns the indenting char to use for indenting things nicely.
1291       * @return string
1292       */
1293      protected static function get_indent() {
1294          if (self::is_pretty()) {
1295              return str_repeat("  ", self::$indent);
1296          }
1297          return '';
1298      }
1299  
1300      /**
1301       * Increases the current indent
1302       */
1303      protected static function increase_indent() {
1304          self::$indent++;
1305      }
1306  
1307      /**
1308       * Decreases the current indent
1309       */
1310      protected static function decrease_indent() {
1311          self::$indent--;
1312      }
1313  
1314      /**
1315       * Returns the string to use as a separator
1316       * @return string
1317       */
1318      protected static function get_separator() {
1319          return (self::is_pretty())?"\n":' ';
1320      }
1321  
1322      /**
1323       * Returns CSS for media
1324       *
1325       * @param string $typestring
1326       * @param css_rule[] $rules An array of css_rule objects
1327       * @return string
1328       */
1329      public static function media($typestring, array &$rules) {
1330          $nl = self::get_separator();
1331  
1332          $output = '';
1333          if ($typestring !== 'all') {
1334              $output .= "\n@media {$typestring} {".$nl;
1335              self::increase_indent();
1336          }
1337          foreach ($rules as $rule) {
1338              $output .= $rule->out().$nl;
1339          }
1340          if ($typestring !== 'all') {
1341              self::decrease_indent();
1342              $output .= '}';
1343          }
1344          return $output;
1345      }
1346  
1347      /**
1348       * Returns CSS for a keyframe
1349       *
1350       * @param string $for The desired declaration. e.g. keyframes, -moz-keyframes, -webkit-keyframes
1351       * @param string $name The name for the keyframe
1352       * @param css_rule[] $rules An array of rules belonging to the keyframe
1353       * @return string
1354       */
1355      public static function keyframe($for, $name, array &$rules) {
1356          $output = "\n@{$for} {$name} {";
1357          foreach ($rules as $rule) {
1358              $output .= $rule->out();
1359          }
1360          $output .= '}';
1361          return $output;
1362      }
1363  
1364      /**
1365       * Returns CSS for a rule
1366       *
1367       * @param string $selector
1368       * @param string $styles
1369       * @return string
1370       */
1371      public static function rule($selector, $styles) {
1372          $css = self::get_indent()."{$selector}{{$styles}}";
1373          return $css;
1374      }
1375  
1376      /**
1377       * Returns CSS for the selectors of a rule
1378       *
1379       * @param css_selector[] $selectors Array of css_selector objects
1380       * @return string
1381       */
1382      public static function selectors(array $selectors) {
1383          $nl = self::get_separator();
1384          $selectorstrings = array();
1385          foreach ($selectors as $selector) {
1386              $selectorstrings[] = $selector->out();
1387          }
1388          return join(','.$nl, $selectorstrings);
1389      }
1390  
1391      /**
1392       * Returns a selector given the components that make it up.
1393       *
1394       * @param array $components
1395       * @return string
1396       */
1397      public static function selector(array $components) {
1398          return trim(join(' ', $components));
1399      }
1400  
1401      /**
1402       * Returns a CSS string for the provided styles
1403       *
1404       * @param css_style[] $styles Array of css_style objects
1405       * @return string
1406       */
1407      public static function styles(array $styles) {
1408          $bits = array();
1409          foreach ($styles as $style) {
1410              // Check if the style is an array. If it is then we are outputing an advanced style.
1411              // An advanced style is a style with one or more values, and can occur in situations like background-image
1412              // where browse specific values are being used.
1413              if (is_array($style)) {
1414                  /* @var css_style[] $style */
1415                  foreach ($style as $advstyle) {
1416                      $bits[] = $advstyle->out();
1417                  }
1418                  continue;
1419              }
1420              $bits[] = $style->out();
1421          }
1422          return join('', $bits);
1423      }
1424  
1425      /**
1426       * Returns a style CSS
1427       *
1428       * @param string $name
1429       * @param string $value
1430       * @param bool $important
1431       * @return string
1432       */
1433      public static function style($name, $value, $important = false) {
1434          $value = trim($value);
1435          if ($important && strpos($value, '!important') === false) {
1436              $value .= ' !important';
1437          }
1438          return "{$name}:{$value};";
1439      }
1440  }
1441  
1442  /**
1443   * A consolidatable style interface.
1444   *
1445   * Class that implement this have a short-hand notation for specifying multiple styles.
1446   *
1447   * @package core
1448   * @subpackage cssoptimiser
1449   * @copyright 2012 Sam Hemelryk
1450   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1451   */
1452  interface core_css_consolidatable_style {
1453      /**
1454       * Used to consolidate several styles into a single "short-hand" style.
1455       * @param array $styles
1456       * @return mixed
1457       */
1458      public static function consolidate(array $styles);
1459  }
1460  
1461  /**
1462   * A structure to represent a CSS selector.
1463   *
1464   * The selector is the classes, id, elements, and psuedo bits that make up a CSS
1465   * rule.
1466   *
1467   * @package core
1468   * @subpackage cssoptimiser
1469   * @copyright 2012 Sam Hemelryk
1470   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1471   */
1472  class css_selector {
1473  
1474      /**
1475       * An array of selector bits
1476       * @var array
1477       */
1478      protected $selectors = array();
1479  
1480      /**
1481       * The number of selectors.
1482       * @var int
1483       */
1484      protected $count = 0;
1485  
1486      /**
1487       * Is null if there are no selectors, true if all selectors are basic and false otherwise.
1488       * A basic selector is one that consists of just the element type. e.g. div, span, td, a
1489       * @var bool|null
1490       */
1491      protected $isbasic = null;
1492  
1493      /**
1494       * Initialises a new CSS selector
1495       * @return css_selector
1496       */
1497      public static function init() {
1498          return new css_selector();
1499      }
1500  
1501      /**
1502       * CSS selectors can only be created through the init method above.
1503       */
1504      protected function __construct() {
1505          // Nothing to do here by default.
1506      }
1507  
1508      /**
1509       * Adds a selector to the end of the current selector
1510       * @param string $selector
1511       */
1512      public function add($selector) {
1513          $selector = trim($selector);
1514          $count = 0;
1515          $count += preg_match_all('/(\.|#)/', $selector, $matchesarray);
1516          if (strpos($selector, '.') !== 0 && strpos($selector, '#') !== 0) {
1517              $count ++;
1518          }
1519          // If its already false then no need to continue, its not basic.
1520          if ($this->isbasic !== false) {
1521              // If theres more than one part making up this selector its not basic.
1522              if ($count > 1) {
1523                  $this->isbasic = false;
1524              } else {
1525                  // Check whether it is a basic element (a-z+) with possible psuedo selector.
1526                  $this->isbasic = (bool)preg_match('#^[a-z]+(:[a-zA-Z]+)?$#', $selector);
1527              }
1528          }
1529          $this->count = $count;
1530          $this->selectors[] = $selector;
1531      }
1532      /**
1533       * Returns the number of individual components that make up this selector
1534       * @return int
1535       */
1536      public function get_selector_count() {
1537          return $this->count;
1538      }
1539  
1540      /**
1541       * Returns the selector for use in a CSS rule
1542       * @return string
1543       */
1544      public function out() {
1545          return css_writer::selector($this->selectors);
1546      }
1547  
1548      /**
1549       * Returns true is all of the selectors act only upon basic elements (no classes/ids)
1550       * @return bool
1551       */
1552      public function is_basic() {
1553          return ($this->isbasic === true);
1554      }
1555  }
1556  
1557  /**
1558   * A structure to represent a CSS rule.
1559   *
1560   * @package core
1561   * @subpackage cssoptimiser
1562   * @copyright 2012 Sam Hemelryk
1563   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1564   */
1565  class css_rule {
1566  
1567      /**
1568       * An array of CSS selectors {@link css_selector}
1569       * @var css_selector[]
1570       */
1571      protected $selectors = array();
1572  
1573      /**
1574       * An array of CSS styles {@link css_style}
1575       * @var css_style[]
1576       */
1577      protected $styles = array();
1578  
1579      /**
1580       * Created a new CSS rule. This is the only way to create a new CSS rule externally.
1581       * @return css_rule
1582       */
1583      public static function init() {
1584          return new css_rule();
1585      }
1586  
1587      /**
1588       * Constructs a new css rule.
1589       *
1590       * @param string $selector The selector or array of selectors that make up this rule.
1591       * @param css_style[] $styles An array of styles that belong to this rule.
1592       */
1593      protected function __construct($selector = null, array $styles = array()) {
1594          if ($selector != null) {
1595              if (is_array($selector)) {
1596                  $this->selectors = $selector;
1597              } else {
1598                  $this->selectors = array($selector);
1599              }
1600              $this->add_styles($styles);
1601          }
1602      }
1603  
1604      /**
1605       * Adds a new CSS selector to this rule
1606       *
1607       * e.g. $rule->add_selector('.one #two.two');
1608       *
1609       * @param css_selector $selector Adds a CSS selector to this rule.
1610       */
1611      public function add_selector(css_selector $selector) {
1612          $this->selectors[] = $selector;
1613      }
1614  
1615      /**
1616       * Adds a new CSS style to this rule.
1617       *
1618       * @param css_style|string $style Adds a new style to this rule
1619       */
1620      public function add_style($style) {
1621          if (is_string($style)) {
1622              $style = trim($style);
1623              if (empty($style)) {
1624                  return;
1625              }
1626              $bits = explode(':', $style, 2);
1627              if (count($bits) == 2) {
1628                  list($name, $value) = array_map('trim', $bits);
1629              }
1630              if (isset($name) && isset($value) && $name !== '' && $value !== '') {
1631                  $style = css_style::init_automatic($name, $value);
1632              }
1633          } else if ($style instanceof css_style) {
1634              // Clone the style as it may be coming from another rule and we don't
1635              // want references as it will likely be overwritten by proceeding
1636              // rules.
1637              $style = clone($style);
1638          }
1639          if ($style instanceof css_style) {
1640              $name = $style->get_name();
1641              $exists = array_key_exists($name, $this->styles);
1642              // We need to find out if the current style support multiple values, or whether the style
1643              // is already set up to record multiple values. This can happen with background images which can have single
1644              // and multiple values.
1645              if ($style->allows_multiple_values() || ($exists && is_array($this->styles[$name]))) {
1646                  if (!$exists) {
1647                      $this->styles[$name] = array();
1648                  } else if ($this->styles[$name] instanceof css_style) {
1649                      $this->styles[$name] = array($this->styles[$name]);
1650                  }
1651                  $this->styles[$name][] = $style;
1652              } else if ($exists) {
1653                  $this->styles[$name]->set_value($style->get_value());
1654              } else {
1655                  $this->styles[$name] = $style;
1656              }
1657          } else if (is_array($style)) {
1658              // We probably shouldn't worry about processing styles here but to
1659              // be truthful it doesn't hurt.
1660              foreach ($style as $astyle) {
1661                  $this->add_style($astyle);
1662              }
1663          }
1664      }
1665  
1666      /**
1667       * An easy method of adding several styles at once. Just calls add_style.
1668       *
1669       * This method simply iterates over the array and calls {@link css_rule::add_style()}
1670       * with each.
1671       *
1672       * @param css_style[] $styles Adds an array of styles
1673       */
1674      public function add_styles(array $styles) {
1675          foreach ($styles as $style) {
1676              $this->add_style($style);
1677          }
1678      }
1679  
1680      /**
1681       * Returns the array of selectors
1682       *
1683       * @return css_selector[]
1684       */
1685      public function get_selectors() {
1686          return $this->selectors;
1687      }
1688  
1689      /**
1690       * Returns the array of styles
1691       *
1692       * @return css_style[]
1693       */
1694      public function get_styles() {
1695          return $this->styles;
1696      }
1697  
1698      /**
1699       * Outputs this rule as a fragment of CSS
1700       *
1701       * @return string
1702       */
1703      public function out() {
1704          $selectors = css_writer::selectors($this->selectors);
1705          $styles = css_writer::styles($this->get_consolidated_styles());
1706          return css_writer::rule($selectors, $styles);
1707      }
1708  
1709      /**
1710       * Consolidates all styles associated with this rule
1711       *
1712       * @return css_style[] An array of consolidated styles
1713       */
1714      public function get_consolidated_styles() {
1715          /* @var css_style[] $organisedstyles */
1716          $organisedstyles = array();
1717          /* @var css_style[] $finalstyles */
1718          $finalstyles = array();
1719          /* @var core_css_consolidatable_style[] $consolidate */
1720          $consolidate = array();
1721          /* @var css_style[] $advancedstyles */
1722          $advancedstyles = array();
1723          foreach ($this->styles as $style) {
1724              // If the style is an array then we are processing an advanced style. An advanced style is a style that can have
1725              // one or more values. Background-image is one such example as it can have browser specific styles.
1726              if (is_array($style)) {
1727                  $single = null;
1728                  $count = 0;
1729                  foreach ($style as $advstyle) {
1730                      /* @var css_style $advstyle */
1731                      $key = $count++;
1732                      $advancedstyles[$key] = $advstyle;
1733                      if (!$advstyle->allows_multiple_values()) {
1734                          if (!is_null($single)) {
1735                              unset($advancedstyles[$single]);
1736                          }
1737                          $single = $key;
1738                      }
1739                  }
1740                  if (!is_null($single)) {
1741                      $style = $advancedstyles[$single];
1742  
1743                      $consolidatetoclass = $style->consolidate_to();
1744                      if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1745                              class_exists('css_style_'.$consolidatetoclass)) {
1746                          $class = 'css_style_'.$consolidatetoclass;
1747                          if (!array_key_exists($class, $consolidate)) {
1748                              $consolidate[$class] = array();
1749                              $organisedstyles[$class] = true;
1750                          }
1751                          $consolidate[$class][] = $style;
1752                          unset($advancedstyles[$single]);
1753                      }
1754                  }
1755  
1756                  continue;
1757              }
1758              $consolidatetoclass = $style->consolidate_to();
1759              if (($style->is_valid() || $style->is_special_empty_value()) && !empty($consolidatetoclass) &&
1760                      class_exists('css_style_'.$consolidatetoclass)) {
1761                  $class = 'css_style_'.$consolidatetoclass;
1762                  if (!array_key_exists($class, $consolidate)) {
1763                      $consolidate[$class] = array();
1764                      $organisedstyles[$class] = true;
1765                  }
1766                  $consolidate[$class][] = $style;
1767              } else {
1768                  $organisedstyles[$style->get_name()] = $style;
1769              }
1770          }
1771  
1772          foreach ($consolidate as $class => $styles) {
1773              $organisedstyles[$class] = call_user_func(array($class, 'consolidate'), $styles);
1774          }
1775  
1776          foreach ($organisedstyles as $style) {
1777              if (is_array($style)) {
1778                  foreach ($style as $s) {
1779                      $finalstyles[] = $s;
1780                  }
1781              } else {
1782                  $finalstyles[] = $style;
1783              }
1784          }
1785          $finalstyles = array_merge($finalstyles, $advancedstyles);
1786          return $finalstyles;
1787      }
1788  
1789      /**
1790       * Splits this rules into an array of CSS rules. One for each of the selectors
1791       * that make up this rule.
1792       *
1793       * @return css_rule[]
1794       */
1795      public function split_by_selector() {
1796          $return = array();
1797          foreach ($this->selectors as $selector) {
1798              $return[] = new css_rule($selector, $this->styles);
1799          }
1800          return $return;
1801      }
1802  
1803      /**
1804       * Splits this rule into an array of rules. One for each of the styles that
1805       * make up this rule
1806       *
1807       * @return css_rule[] Array of css_rule objects
1808       */
1809      public function split_by_style() {
1810          $return = array();
1811          foreach ($this->styles as $style) {
1812              if (is_array($style)) {
1813                  $return[] = new css_rule($this->selectors, $style);
1814                  continue;
1815              }
1816              $return[] = new css_rule($this->selectors, array($style));
1817          }
1818          return $return;
1819      }
1820  
1821      /**
1822       * Gets a hash for the styles of this rule
1823       *
1824       * @return string
1825       */
1826      public function get_style_hash() {
1827          return md5(css_writer::styles($this->styles));
1828      }
1829  
1830      /**
1831       * Gets a hash for the selectors of this rule
1832       *
1833       * @return string
1834       */
1835      public function get_selector_hash() {
1836          return md5(css_writer::selectors($this->selectors));
1837      }
1838  
1839      /**
1840       * Gets the number of selectors that make up this rule.
1841       *
1842       * @return int
1843       */
1844      public function get_selector_count() {
1845          $count = 0;
1846          foreach ($this->selectors as $selector) {
1847              $count += $selector->get_selector_count();
1848          }
1849          return $count;
1850      }
1851  
1852      /**
1853       * Returns true if there are any errors with this rule.
1854       *
1855       * @return bool
1856       */
1857      public function has_errors() {
1858          foreach ($this->styles as $style) {
1859              if (is_array($style)) {
1860                  /* @var css_style[] $style */
1861                  foreach ($style as $advstyle) {
1862                      if ($advstyle->has_error()) {
1863                          return true;
1864                      }
1865                  }
1866                  continue;
1867              }
1868              if ($style->has_error()) {
1869                  return true;
1870              }
1871          }
1872          return false;
1873      }
1874  
1875      /**
1876       * Returns the error strings that were recorded when processing this rule.
1877       *
1878       * Before calling this function you should first call {@link css_rule::has_errors()}
1879       * to make sure there are errors (hopefully there arn't).
1880       *
1881       * @return string
1882       */
1883      public function get_error_string() {
1884          $css = $this->out();
1885          $errors = array();
1886          foreach ($this->styles as $style) {
1887              if (is_array($style)) {
1888                  /* @var css_style[] $style */
1889                  foreach ($style as $advstyle) {
1890                      if ($advstyle instanceof css_style && $advstyle->has_error()) {
1891                          $errors[] = "  * ".$advstyle->get_last_error();
1892                      }
1893                  }
1894              } else if ($style instanceof css_style && $style->has_error()) {
1895                  $errors[] = "  * ".$style->get_last_error();
1896              }
1897          }
1898          return $css." has the following errors:\n".join("\n", $errors);
1899      }
1900  
1901      /**
1902       * Returns true if this rule could be considered a reset rule.
1903       *
1904       * A reset rule is a rule that acts upon an HTML element and does not include any other parts to its selector.
1905       *
1906       * @return bool
1907       */
1908      public function is_reset_rule() {
1909          foreach ($this->selectors as $selector) {
1910              if (!$selector->is_basic()) {
1911                  return false;
1912              }
1913          }
1914          return true;
1915      }
1916  }
1917  
1918  /**
1919   * An abstract CSS rule collection class.
1920   *
1921   * This class is extended by things such as media and keyframe declaration. They are declarations that
1922   * group rules together for a purpose.
1923   * When no declaration is specified rules accumulate into @media all.
1924   *
1925   * @package core
1926   * @subpackage cssoptimiser
1927   * @copyright 2012 Sam Hemelryk
1928   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
1929   */
1930  abstract class css_rule_collection {
1931      /**
1932       * An array of rules within this collection instance
1933       * @var css_rule[]
1934       */
1935      protected $rules = array();
1936  
1937      /**
1938       * The collection must be able to print itself.
1939       */
1940      abstract public function out();
1941  
1942      /**
1943       * Adds a new CSS rule to this collection instance
1944       *
1945       * @param css_rule $newrule
1946       */
1947      public function add_rule(css_rule $newrule) {
1948          foreach ($newrule->split_by_selector() as $rule) {
1949              $hash = $rule->get_selector_hash();
1950              if (!array_key_exists($hash, $this->rules)) {
1951                  $this->rules[$hash] = $rule;
1952              } else {
1953                  $this->rules[$hash]->add_styles($rule->get_styles());
1954              }
1955          }
1956      }
1957  
1958      /**
1959       * Returns the rules used by this collection
1960       *
1961       * @return css_rule[]
1962       */
1963      public function get_rules() {
1964          return $this->rules;
1965      }
1966  
1967      /**
1968       * Organises rules by gropuing selectors based upon the styles and consolidating
1969       * those selectors into single rules.
1970       *
1971       * @return bool True if the CSS was optimised by this method
1972       */
1973      public function organise_rules_by_selectors() {
1974          /* @var css_rule[] $optimisedrules */
1975          $optimisedrules = array();
1976          $beforecount = count($this->rules);
1977          $lasthash = null;
1978          /* @var css_rule $lastrule */
1979          $lastrule = null;
1980          foreach ($this->rules as $rule) {
1981              $hash = $rule->get_style_hash();
1982              if ($lastrule !== null && $lasthash !== null && $hash === $lasthash) {
1983                  foreach ($rule->get_selectors() as $selector) {
1984                      $lastrule->add_selector($selector);
1985                  }
1986                  continue;
1987              }
1988              $lastrule = clone($rule);
1989              $lasthash = $hash;
1990              $optimisedrules[] = $lastrule;
1991          }
1992          $this->rules = array();
1993          foreach ($optimisedrules as $optimised) {
1994              $this->rules[$optimised->get_selector_hash()] = $optimised;
1995          }
1996          $aftercount = count($this->rules);
1997          return ($beforecount < $aftercount);
1998      }
1999  
2000      /**
2001       * Returns the total number of rules that exist within this collection
2002       *
2003       * @return int
2004       */
2005      public function count_rules() {
2006          return count($this->rules);
2007      }
2008  
2009      /**
2010       * Returns the total number of selectors that exist within this collection
2011       *
2012       * @return int
2013       */
2014      public function count_selectors() {
2015          $count = 0;
2016          foreach ($this->rules as $rule) {
2017              $count += $rule->get_selector_count();
2018          }
2019          return $count;
2020      }
2021  
2022      /**
2023       * Returns true if the collection has any rules that have errors
2024       *
2025       * @return boolean
2026       */
2027      public function has_errors() {
2028          foreach ($this->rules as $rule) {
2029              if ($rule->has_errors()) {
2030                  return true;
2031              }
2032          }
2033          return false;
2034      }
2035  
2036      /**
2037       * Returns any errors that have happened within rules in this collection.
2038       *
2039       * @return string[]
2040       */
2041      public function get_errors() {
2042          $errors = array();
2043          foreach ($this->rules as $rule) {
2044              if ($rule->has_errors()) {
2045                  $errors[] = $rule->get_error_string();
2046              }
2047          }
2048          return $errors;
2049      }
2050  }
2051  
2052  /**
2053   * A media class to organise rules by the media they apply to.
2054   *
2055   * @package core
2056   * @subpackage cssoptimiser
2057   * @copyright 2012 Sam Hemelryk
2058   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2059   */
2060  class css_media extends css_rule_collection {
2061  
2062      /**
2063       * An array of the different media types this instance applies to.
2064       * @var array
2065       */
2066      protected $types = array();
2067  
2068      /**
2069       * Initalises a new media instance
2070       *
2071       * @param string $for The media that the contained rules are destined for.
2072       */
2073      public function __construct($for = 'all') {
2074          $types = explode(',', $for);
2075          $this->types = array_map('trim', $types);
2076      }
2077  
2078      /**
2079       * Returns the CSS for this media and all of its rules.
2080       *
2081       * @return string
2082       */
2083      public function out() {
2084          return css_writer::media(join(',', $this->types), $this->rules);
2085      }
2086  
2087      /**
2088       * Returns an array of media that this media instance applies to
2089       *
2090       * @return array
2091       */
2092      public function get_types() {
2093          return $this->types;
2094      }
2095  
2096      /**
2097       * Returns all of the reset rules known by this media set.
2098       * @param bool $remove If set to true reset rules will be removed before being returned.
2099       * @return array
2100       */
2101      public function get_reset_rules($remove = false) {
2102          $resetrules = array();
2103          foreach ($this->rules as $key => $rule) {
2104              if ($rule->is_reset_rule()) {
2105                  $resetrules[] = clone $rule;
2106                  if ($remove) {
2107                      unset($this->rules[$key]);
2108                  }
2109              }
2110          }
2111          return $resetrules;
2112      }
2113  }
2114  
2115  /**
2116   * A media class to organise rules by the media they apply to.
2117   *
2118   * @package core
2119   * @subpackage cssoptimiser
2120   * @copyright 2012 Sam Hemelryk
2121   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2122   */
2123  class css_keyframe extends css_rule_collection {
2124  
2125      /**
2126       * The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2127       * @var string
2128       */
2129      protected $for;
2130  
2131      /**
2132       * The name for the keyframes
2133       * @var string
2134       */
2135      protected $name;
2136      /**
2137       * Constructs a new keyframe
2138       *
2139       * @param string $for The directive e.g. keyframes, -moz-keyframes, -webkit-keyframes
2140       * @param string $name The name for the keyframes
2141       */
2142      public function __construct($for, $name) {
2143          $this->for = $for;
2144          $this->name = $name;
2145      }
2146      /**
2147       * Returns the directive of this keyframe
2148       *
2149       * e.g. keyframes, -moz-keyframes, -webkit-keyframes
2150       * @return string
2151       */
2152      public function get_for() {
2153          return $this->for;
2154      }
2155      /**
2156       * Returns the name of this keyframe
2157       * @return string
2158       */
2159      public function get_name() {
2160          return $this->name;
2161      }
2162      /**
2163       * Returns the CSS for this collection of keyframes and all of its rules.
2164       *
2165       * @return string
2166       */
2167      public function out() {
2168          return css_writer::keyframe($this->for, $this->name, $this->rules);
2169      }
2170  }
2171  
2172  /**
2173   * An absract class to represent CSS styles
2174   *
2175   * @package core
2176   * @subpackage cssoptimiser
2177   * @copyright 2012 Sam Hemelryk
2178   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2179   */
2180  abstract class css_style {
2181  
2182      /** Constant used for recongise a special empty value in a CSS style */
2183      const NULL_VALUE = '@@$NULL$@@';
2184  
2185      /**
2186       * The name of the style
2187       * @var string
2188       */
2189      protected $name;
2190  
2191      /**
2192       * The value for the style
2193       * @var mixed
2194       */
2195      protected $value;
2196  
2197      /**
2198       * If set to true this style was defined with the !important rule.
2199       * Only trolls use !important.
2200       * Don't hide under bridges.. its not good for your skin. Do the proper thing
2201       * and fix the issue don't just force a fix that will undoubtedly one day
2202       * lead to further frustration.
2203       * @var bool
2204       */
2205      protected $important = false;
2206  
2207      /**
2208       * Gets set to true if this style has an error
2209       * @var bool
2210       */
2211      protected $error = false;
2212  
2213      /**
2214       * The last error message that occured
2215       * @var string
2216       */
2217      protected $errormessage = null;
2218  
2219      /**
2220       * Initialises a new style.
2221       *
2222       * This is the only public way to create a style to ensure they that appropriate
2223       * style class is used if it exists.
2224       *
2225       * @param string $name The name of the style.
2226       * @param string $value The value of the style.
2227       * @return css_style_generic
2228       */
2229      public static function init_automatic($name, $value) {
2230          $cleanedname = preg_replace('#[^a-zA-Z0-9]+#', '', $name);
2231          $specificclass = 'css_style_'.$cleanedname;
2232          if (class_exists($specificclass)) {
2233              $style = call_user_func(array($specificclass, 'init'), $value);
2234              if ($cleanedname !== $name && !is_array($style)) {
2235                  $style->set_actual_name($name);
2236              }
2237              return $style;
2238          }
2239          return new css_style_generic($name, $value);
2240      }
2241  
2242      /**
2243       * Creates a new style when given its name and value
2244       *
2245       * @param string $name The name of the style.
2246       * @param string $value The value of the style.
2247       */
2248      protected function __construct($name, $value) {
2249          $this->name = $name;
2250          $this->set_value($value);
2251      }
2252  
2253      /**
2254       * Sets the value for the style
2255       *
2256       * @param string $value
2257       */
2258      final public function set_value($value) {
2259          $value = trim($value);
2260          $important = preg_match('#(\!important\s*;?\s*)$#', $value, $matches);
2261          if ($important) {
2262              $value = substr($value, 0, -(strlen($matches[1])));
2263              $value = rtrim($value);
2264          }
2265          if (!$this->important || $important) {
2266              $this->value = $this->clean_value($value);
2267              $this->important = $important;
2268          }
2269          if (!$this->is_valid()) {
2270              $this->set_error('Invalid value for '.$this->name);
2271          }
2272      }
2273  
2274      /**
2275       * Returns true if the value associated with this style is valid
2276       *
2277       * @return bool
2278       */
2279      public function is_valid() {
2280          return true;
2281      }
2282  
2283      /**
2284       * Returns the name for the style
2285       *
2286       * @return string
2287       */
2288      public function get_name() {
2289          return $this->name;
2290      }
2291  
2292      /**
2293       * Returns the value for the style
2294       *
2295       * @param bool $includeimportant If set to true and the rule is important !important postfix will be used.
2296       * @return string
2297       */
2298      public function get_value($includeimportant = true) {
2299          $value = $this->value;
2300          if ($includeimportant && $this->important) {
2301              $value .= ' !important';
2302          }
2303          return $value;
2304      }
2305  
2306      /**
2307       * Returns the style ready for use in CSS
2308       *
2309       * @param string|null $value A value to use to override the value for this style.
2310       * @return string
2311       */
2312      public function out($value = null) {
2313          if (is_null($value)) {
2314              $value = $this->get_value();
2315          }
2316          return css_writer::style($this->name, $value, $this->important);
2317      }
2318  
2319      /**
2320       * This can be overridden by a specific style allowing it to clean its values
2321       * consistently.
2322       *
2323       * @param mixed $value
2324       * @return mixed
2325       */
2326      protected function clean_value($value) {
2327          return $value;
2328      }
2329  
2330      /**
2331       * If this particular style can be consolidated into another style this function
2332       * should return the style that it can be consolidated into.
2333       *
2334       * @return string|null
2335       */
2336      public function consolidate_to() {
2337          return null;
2338      }
2339  
2340      /**
2341       * Sets the last error message.
2342       *
2343       * @param string $message
2344       */
2345      protected function set_error($message) {
2346          $this->error = true;
2347          $this->errormessage = $message;
2348      }
2349  
2350      /**
2351       * Returns true if an error has occured
2352       *
2353       * @return bool
2354       */
2355      public function has_error() {
2356          return $this->error;
2357      }
2358  
2359      /**
2360       * Returns the last error that occured or null if no errors have happened.
2361       *
2362       * @return string
2363       */
2364      public function get_last_error() {
2365          return $this->errormessage;
2366      }
2367  
2368      /**
2369       * Returns true if the value for this style is the special null value.
2370       *
2371       * This should only be overriden in circumstances where a shorthand style can lead
2372       * to move explicit styles being overwritten. Not a common place occurenace.
2373       *
2374       * Example:
2375       *   This occurs if the shorthand background property was used but no proper value
2376       *   was specified for this style.
2377       *   This leads to a null value being used unless otherwise overridden.
2378       *
2379       * @return bool
2380       */
2381      public function is_special_empty_value() {
2382          return false;
2383      }
2384  
2385      /**
2386       * Returns true if this style permits multiple values.
2387       *
2388       * This occurs for styles such as background image that can have browser specific values that need to be maintained because
2389       * of course we don't know what browser the user is using, and optimisation occurs before caching.
2390       * Thus we must always server all values we encounter in the order we encounter them for when this is set to true.
2391       *
2392       * @return boolean False by default, true if the style supports muliple values.
2393       */
2394      public function allows_multiple_values() {
2395          return false;
2396      }
2397  
2398      /**
2399       * Returns true if this style was marked important.
2400       * @return bool
2401       */
2402      public function is_important() {
2403          return !empty($this->important);
2404      }
2405  
2406      /**
2407       * Sets the important flag for this style and its current value.
2408       * @param bool $important
2409       */
2410      public function set_important($important = true) {
2411          $this->important = (bool) $important;
2412      }
2413  
2414      /**
2415       * Sets the actual name used within the style.
2416       *
2417       * This method allows us to support browser hacks like *width:0;
2418       *
2419       * @param string $name
2420       */
2421      public function set_actual_name($name) {
2422          $this->name = $name;
2423      }
2424  }
2425  
2426  /**
2427   * A generic CSS style class to use when a more specific class does not exist.
2428   *
2429   * @package core
2430   * @subpackage cssoptimiser
2431   * @copyright 2012 Sam Hemelryk
2432   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2433   */
2434  class css_style_generic extends css_style {
2435  
2436      /**
2437       * Cleans incoming values for typical things that can be optimised.
2438       *
2439       * @param mixed $value Cleans the provided value optimising it if possible
2440       * @return string
2441       */
2442      protected function clean_value($value) {
2443          if (trim($value) == '0px') {
2444              $value = 0;
2445          } else if (preg_match('/^#([a-fA-F0-9]{3,6})/', $value, $matches)) {
2446              $value = '#'.strtoupper($matches[1]);
2447          }
2448          return $value;
2449      }
2450  }
2451  
2452  /**
2453   * A colour CSS style
2454   *
2455   * @package core
2456   * @subpackage cssoptimiser
2457   * @copyright 2012 Sam Hemelryk
2458   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2459   */
2460  class css_style_color extends css_style {
2461  
2462      /**
2463       * Creates a new colour style
2464       *
2465       * @param mixed $value Initialises a new colour style
2466       * @return css_style_color
2467       */
2468      public static function init($value) {
2469          return new css_style_color('color', $value);
2470      }
2471  
2472      /**
2473       * Cleans the colour unifing it to a 6 char hash colour if possible
2474       * Doing this allows us to associate identical colours being specified in
2475       * different ways. e.g. Red, red, #F00, and #F00000
2476       *
2477       * @param mixed $value Cleans the provided value optimising it if possible
2478       * @return string
2479       */
2480      protected function clean_value($value) {
2481          $value = trim($value);
2482          if (css_is_colour($value)) {
2483              if (preg_match('/#([a-fA-F0-9]{6})/', $value, $matches)) {
2484                  $value = '#'.strtoupper($matches[1]);
2485              } else if (preg_match('/#([a-fA-F0-9])([a-fA-F0-9])([a-fA-F0-9])/', $value, $matches)) {
2486                  $value = $matches[1] . $matches[1] . $matches[2] . $matches[2] . $matches[3] . $matches[3];
2487                  $value = '#'.strtoupper($value);
2488              } else if (array_key_exists(strtolower($value), css_optimiser::$htmlcolours)) {
2489                  $value = css_optimiser::$htmlcolours[strtolower($value)];
2490              }
2491          }
2492          return $value;
2493      }
2494  
2495      /**
2496       * Returns the colour style for use within CSS.
2497       * Will return an optimised hash colour.
2498       *
2499       * e.g #123456
2500       *     #123 instead of #112233
2501       *     #F00 instead of red
2502       *
2503       * @param string $overridevalue If provided then this value will be used instead
2504       *     of the styles current value.
2505       * @return string
2506       */
2507      public function out($overridevalue = null) {
2508          if ($overridevalue === null) {
2509              $overridevalue = $this->value;
2510          }
2511          return parent::out(self::shrink_value($overridevalue));
2512      }
2513  
2514      /**
2515       * Shrinks the colour value is possible.
2516       *
2517       * @param string $value Shrinks the current value to an optimial form if possible
2518       * @return string
2519       */
2520      public static function shrink_value($value) {
2521          if (preg_match('/#([a-fA-F0-9])\1([a-fA-F0-9])\2([a-fA-F0-9])\3/', $value, $matches)) {
2522              return '#'.$matches[1].$matches[2].$matches[3];
2523          }
2524          return $value;
2525      }
2526  
2527      /**
2528       * Returns true if the value is a valid colour.
2529       *
2530       * @return bool
2531       */
2532      public function is_valid() {
2533          return css_is_colour($this->value);
2534      }
2535  }
2536  
2537  /**
2538   * A width style
2539   *
2540   * @package core
2541   * @subpackage cssoptimiser
2542   * @copyright 2012 Sam Hemelryk
2543   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2544   */
2545  class css_style_width extends css_style {
2546  
2547      /**
2548       * Checks if the width is valid
2549       * @return bool
2550       */
2551      public function is_valid() {
2552          return css_is_width($this->value);
2553      }
2554  
2555      /**
2556       * Cleans the provided value
2557       *
2558       * @param mixed $value Cleans the provided value optimising it if possible
2559       * @return string
2560       */
2561      protected function clean_value($value) {
2562          if (!css_is_width($value)) {
2563              // Note we don't actually change the value to something valid. That
2564              // would be bad for futureproofing.
2565              $this->set_error('Invalid width specified for '.$this->name);
2566          } else if (preg_match('#^0\D+$#', $value)) {
2567              $value = 0;
2568          }
2569          return trim($value);
2570      }
2571  
2572      /**
2573       * Initialises a new width style
2574       *
2575       * @param mixed $value The value this style has
2576       * @return css_style_width
2577       */
2578      public static function init($value) {
2579          return new css_style_width('width', $value);
2580      }
2581  }
2582  
2583  /**
2584   * A margin style
2585   *
2586   * @package core
2587   * @subpackage cssoptimiser
2588   * @copyright 2012 Sam Hemelryk
2589   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2590   */
2591  class css_style_margin extends css_style_width implements core_css_consolidatable_style {
2592  
2593      /**
2594       * Initialises a margin style.
2595       *
2596       * In this case we split the margin into several other margin styles so that
2597       * we can properly condense overrides and then reconsolidate them later into
2598       * an optimal form.
2599       *
2600       * @param string $value The value the style has
2601       * @return array An array of margin values that can later be consolidated
2602       */
2603      public static function init($value) {
2604          $important = '';
2605          if (strpos($value, '!important') !== false) {
2606              $important = ' !important';
2607              $value = str_replace('!important', '', $value);
2608          }
2609  
2610          $value = preg_replace('#\s+#', ' ', trim($value));
2611          $bits = explode(' ', $value, 4);
2612  
2613          $top = $right = $bottom = $left = null;
2614          if (count($bits) > 0) {
2615              $top = $right = $bottom = $left = array_shift($bits);
2616          }
2617          if (count($bits) > 0) {
2618              $right = $left = array_shift($bits);
2619          }
2620          if (count($bits) > 0) {
2621              $bottom = array_shift($bits);
2622          }
2623          if (count($bits) > 0) {
2624              $left = array_shift($bits);
2625          }
2626          return array(
2627              new css_style_margintop('margin-top', $top.$important),
2628              new css_style_marginright('margin-right', $right.$important),
2629              new css_style_marginbottom('margin-bottom', $bottom.$important),
2630              new css_style_marginleft('margin-left', $left.$important)
2631          );
2632      }
2633  
2634      /**
2635       * Consolidates individual margin styles into a single margin style
2636       *
2637       * @param css_style[] $styles
2638       * @return css_style[] An array of consolidated styles
2639       */
2640      public static function consolidate(array $styles) {
2641          if (count($styles) != 4) {
2642              return $styles;
2643          }
2644  
2645          $someimportant = false;
2646          $allimportant = null;
2647          $notimportantequal = null;
2648          $firstvalue = null;
2649          foreach ($styles as $style) {
2650              if ($style->is_important()) {
2651                  $someimportant = true;
2652                  if ($allimportant === null) {
2653                      $allimportant = true;
2654                  }
2655              } else {
2656                  if ($allimportant === true) {
2657                      $allimportant = false;
2658                  }
2659                  if ($firstvalue == null) {
2660                      $firstvalue = $style->get_value(false);
2661                      $notimportantequal = true;
2662                  } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
2663                      $notimportantequal = false;
2664                  }
2665              }
2666          }
2667  
2668          if ($someimportant && !$allimportant && !$notimportantequal) {
2669              return $styles;
2670          }
2671  
2672          if ($someimportant && !$allimportant && $notimportantequal) {
2673              $return = array(
2674                  new css_style_margin('margin', $firstvalue)
2675              );
2676              foreach ($styles as $style) {
2677                  if ($style->is_important()) {
2678                      $return[] = $style;
2679                  }
2680              }
2681              return $return;
2682          } else {
2683              $top = null;
2684              $right = null;
2685              $bottom = null;
2686              $left = null;
2687              foreach ($styles as $style) {
2688                  switch ($style->get_name()) {
2689                      case 'margin-top' :
2690                          $top = $style->get_value(false);
2691                          break;
2692                      case 'margin-right' :
2693                          $right = $style->get_value(false);
2694                          break;
2695                      case 'margin-bottom' :
2696                          $bottom = $style->get_value(false);
2697                          break;
2698                      case 'margin-left' :
2699                          $left = $style->get_value(false);
2700                          break;
2701                  }
2702              }
2703              if ($top == $bottom && $left == $right) {
2704                  if ($top == $left) {
2705                      $returnstyle = new css_style_margin('margin', $top);
2706                  } else {
2707                      $returnstyle = new css_style_margin('margin', "{$top} {$left}");
2708                  }
2709              } else if ($left == $right) {
2710                  $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom}");
2711              } else {
2712                  $returnstyle = new css_style_margin('margin', "{$top} {$right} {$bottom} {$left}");
2713              }
2714              if ($allimportant) {
2715                  $returnstyle->set_important();
2716              }
2717              return array($returnstyle);
2718          }
2719      }
2720  }
2721  
2722  /**
2723   * A margin top style
2724   *
2725   * @package core
2726   * @subpackage cssoptimiser
2727   * @copyright 2012 Sam Hemelryk
2728   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2729   */
2730  class css_style_margintop extends css_style_margin {
2731  
2732      /**
2733       * A simple init, just a single style
2734       *
2735       * @param string $value The value the style has
2736       * @return css_style_margintop
2737       */
2738      public static function init($value) {
2739          return new css_style_margintop('margin-top', $value);
2740      }
2741  
2742      /**
2743       * This style can be consolidated into a single margin style
2744       *
2745       * @return string
2746       */
2747      public function consolidate_to() {
2748          return 'margin';
2749      }
2750  }
2751  
2752  /**
2753   * A margin right style
2754   *
2755   * @package core
2756   * @subpackage cssoptimiser
2757   * @copyright 2012 Sam Hemelryk
2758   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2759   */
2760  class css_style_marginright extends css_style_margin {
2761  
2762      /**
2763       * A simple init, just a single style
2764       *
2765       * @param string $value The value the style has
2766       * @return css_style_margintop
2767       */
2768      public static function init($value) {
2769          return new css_style_marginright('margin-right', $value);
2770      }
2771  
2772      /**
2773       * This style can be consolidated into a single margin style
2774       *
2775       * @return string
2776       */
2777      public function consolidate_to() {
2778          return 'margin';
2779      }
2780  }
2781  
2782  /**
2783   * A margin bottom style
2784   *
2785   * @package core
2786   * @subpackage cssoptimiser
2787   * @copyright 2012 Sam Hemelryk
2788   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2789   */
2790  class css_style_marginbottom extends css_style_margin {
2791  
2792      /**
2793       * A simple init, just a single style
2794       *
2795       * @param string $value The value the style has
2796       * @return css_style_margintop
2797       */
2798      public static function init($value) {
2799          return new css_style_marginbottom('margin-bottom', $value);
2800      }
2801  
2802      /**
2803       * This style can be consolidated into a single margin style
2804       *
2805       * @return string
2806       */
2807      public function consolidate_to() {
2808          return 'margin';
2809      }
2810  }
2811  
2812  /**
2813   * A margin left style
2814   *
2815   * @package core
2816   * @subpackage cssoptimiser
2817   * @copyright 2012 Sam Hemelryk
2818   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2819   */
2820  class css_style_marginleft extends css_style_margin {
2821  
2822      /**
2823       * A simple init, just a single style
2824       *
2825       * @param string $value The value the style has
2826       * @return css_style_margintop
2827       */
2828      public static function init($value) {
2829          return new css_style_marginleft('margin-left', $value);
2830      }
2831  
2832      /**
2833       * This style can be consolidated into a single margin style
2834       *
2835       * @return string
2836       */
2837      public function consolidate_to() {
2838          return 'margin';
2839      }
2840  }
2841  
2842  /**
2843   * A border style
2844   *
2845   * @package core
2846   * @subpackage cssoptimiser
2847   * @copyright 2012 Sam Hemelryk
2848   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
2849   */
2850  class css_style_border extends css_style implements core_css_consolidatable_style {
2851  
2852      /**
2853       * Initalises the border style into an array of individual style compontents
2854       *
2855       * @param string $value The value the style has
2856       * @return css_style_bordercolor
2857       */
2858      public static function init($value) {
2859          $value = preg_replace('#\s+#', ' ', $value);
2860          $bits = explode(' ', $value, 3);
2861  
2862          $return = array();
2863          if (count($bits) > 0) {
2864              $width = array_shift($bits);
2865              if (!css_style_borderwidth::is_border_width($width)) {
2866                  $width = '0';
2867              }
2868              $return[] = css_style_bordertopwidth::init($width);
2869              $return[] = css_style_borderrightwidth::init($width);
2870              $return[] = css_style_borderbottomwidth::init($width);
2871              $return[] = css_style_borderleftwidth::init($width);
2872          }
2873          if (count($bits) > 0) {
2874              $style = array_shift($bits);
2875              $return[] = css_style_bordertopstyle::init($style);
2876              $return[] = css_style_borderrightstyle::init($style);
2877              $return[] = css_style_borderbottomstyle::init($style);
2878              $return[] = css_style_borderleftstyle::init($style);
2879          }
2880          if (count($bits) > 0) {
2881              $colour = array_shift($bits);
2882              $return[] = css_style_bordertopcolor::init($colour);
2883              $return[] = css_style_borderrightcolor::init($colour);
2884              $return[] = css_style_borderbottomcolor::init($colour);
2885              $return[] = css_style_borderleftcolor::init($colour);
2886          }
2887          return $return;
2888      }
2889  
2890      /**
2891       * Consolidates all border styles into a single style
2892       *
2893       * @param css_style[] $styles An array of border styles
2894       * @return css_style[] An optimised array of border styles
2895       */
2896      public static function consolidate(array $styles) {
2897  
2898          $borderwidths = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2899          $borderstyles = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2900          $bordercolors = array('top' => null, 'right' => null, 'bottom' => null, 'left' => null);
2901  
2902          foreach ($styles as $style) {
2903              switch ($style->get_name()) {
2904                  case 'border-top-width':
2905                      $borderwidths['top'] = $style->get_value();
2906                      break;
2907                  case 'border-right-width':
2908                      $borderwidths['right'] = $style->get_value();
2909                      break;
2910                  case 'border-bottom-width':
2911                      $borderwidths['bottom'] = $style->get_value();
2912                      break;
2913                  case 'border-left-width':
2914                      $borderwidths['left'] = $style->get_value();
2915                      break;
2916  
2917                  case 'border-top-style':
2918                      $borderstyles['top'] = $style->get_value();
2919                      break;
2920                  case 'border-right-style':
2921                      $borderstyles['right'] = $style->get_value();
2922                      break;
2923                  case 'border-bottom-style':
2924                      $borderstyles['bottom'] = $style->get_value();
2925                      break;
2926                  case 'border-left-style':
2927                      $borderstyles['left'] = $style->get_value();
2928                      break;
2929  
2930                  case 'border-top-color':
2931                      $bordercolors['top'] = css_style_color::shrink_value($style->get_value());
2932                      break;
2933                  case 'border-right-color':
2934                      $bordercolors['right'] = css_style_color::shrink_value($style->get_value());
2935                      break;
2936                  case 'border-bottom-color':
2937                      $bordercolors['bottom'] = css_style_color::shrink_value($style->get_value());
2938                      break;
2939                  case 'border-left-color':
2940                      $bordercolors['left'] = css_style_color::shrink_value($style->get_value());
2941                      break;
2942              }
2943          }
2944  
2945          $uniquewidths = count(array_unique($borderwidths));
2946          $uniquestyles = count(array_unique($borderstyles));
2947          $uniquecolors = count(array_unique($bordercolors));
2948  
2949          $nullwidths = in_array(null, $borderwidths, true);
2950          $nullstyles = in_array(null, $borderstyles, true);
2951          $nullcolors = in_array(null, $bordercolors, true);
2952  
2953          $allwidthsthesame = ($uniquewidths === 1)?1:0;
2954          $allstylesthesame = ($uniquestyles === 1)?1:0;
2955          $allcolorsthesame = ($uniquecolors === 1)?1:0;
2956  
2957          $allwidthsnull = $allwidthsthesame && $nullwidths;
2958          $allstylesnull = $allstylesthesame && $nullstyles;
2959          $allcolorsnull = $allcolorsthesame && $nullcolors;
2960  
2961          /* @var css_style[] $return */
2962          $return = array();
2963          if ($allwidthsnull && $allstylesnull && $allcolorsnull) {
2964              // Everything is null still... boo.
2965              return array(new css_style_border('border', ''));
2966  
2967          } else if ($allwidthsnull && $allstylesnull) {
2968  
2969              self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2970              return $return;
2971  
2972          } else if ($allwidthsnull && $allcolorsnull) {
2973  
2974              self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2975              return $return;
2976  
2977          } else if ($allcolorsnull && $allstylesnull) {
2978  
2979              self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
2980              return $return;
2981  
2982          }
2983  
2984          if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 3) {
2985  
2986              $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top'].' '.$bordercolors['top']);
2987  
2988          } else if ($allwidthsthesame + $allstylesthesame + $allcolorsthesame == 2) {
2989  
2990              if ($allwidthsthesame && $allstylesthesame && !$nullwidths && !$nullstyles) {
2991  
2992                  $return[] = new css_style_border('border', $borderwidths['top'].' '.$borderstyles['top']);
2993                  self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
2994  
2995              } else if ($allwidthsthesame && $allcolorsthesame && !$nullwidths && !$nullcolors) {
2996  
2997                  $return[] = new css_style_border('border', $borderwidths['top'].' solid '.$bordercolors['top']);
2998                  self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
2999  
3000              } else if ($allstylesthesame && $allcolorsthesame && !$nullstyles && !$nullcolors) {
3001  
3002                  $return[] = new css_style_border('border', '1px '.$borderstyles['top'].' '.$bordercolors['top']);
3003                  self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
3004  
3005              } else {
3006                  self::consolidate_styles_by_direction($return, 'css_style_borderwidth', 'border-width', $borderwidths);
3007                  self::consolidate_styles_by_direction($return, 'css_style_borderstyle', 'border-style', $borderstyles);
3008                  self::consolidate_styles_by_direction($return, 'css_style_bordercolor', 'border-color', $bordercolors);
3009              }
3010  
3011          } else if (!$nullwidths && !$nullcolors && !$nullstyles &&
3012              max(array_count_values($borderwidths)) == 3 &&
3013              max(array_count_values($borderstyles)) == 3 &&
3014              max(array_count_values($bordercolors)) == 3) {
3015  
3016              $widthkeys = array();
3017              $stylekeys = array();
3018              $colorkeys = array();
3019  
3020              foreach ($borderwidths as $key => $value) {
3021                  if (!array_key_exists($value, $widthkeys)) {
3022                      $widthkeys[$value] = array();
3023                  }
3024                  $widthkeys[$value][] = $key;
3025              }
3026              usort($widthkeys, 'css_sort_by_count');
3027              $widthkeys = array_values($widthkeys);
3028  
3029              foreach ($borderstyles as $key => $value) {
3030                  if (!array_key_exists($value, $stylekeys)) {
3031                      $stylekeys[$value] = array();
3032                  }
3033                  $stylekeys[$value][] = $key;
3034              }
3035              usort($stylekeys, 'css_sort_by_count');
3036              $stylekeys = array_values($stylekeys);
3037  
3038              foreach ($bordercolors as $key => $value) {
3039                  if (!array_key_exists($value, $colorkeys)) {
3040                      $colorkeys[$value] = array();
3041                  }
3042                  $colorkeys[$value][] = $key;
3043              }
3044              usort($colorkeys, 'css_sort_by_count');
3045              $colorkeys = array_values($colorkeys);
3046  
3047              if ($widthkeys == $stylekeys && $stylekeys == $colorkeys) {
3048                  $key = $widthkeys[0][0];
3049                  self::build_style_string($return, 'css_style_border', 'border',
3050                      $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
3051                  $key = $widthkeys[1][0];
3052                  self::build_style_string($return, 'css_style_border'.$key, 'border-'.$key,
3053                      $borderwidths[$key], $borderstyles[$key], $bordercolors[$key]);
3054              } else {
3055                  self::build_style_string($return, 'css_style_bordertop', 'border-top',
3056                      $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
3057                  self::build_style_string($return, 'css_style_borderright', 'border-right',
3058                      $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
3059                  self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
3060                      $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
3061                  self::build_style_string($return, 'css_style_borderleft', 'border-left',
3062                      $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
3063              }
3064          } else {
3065              self::build_style_string($return, 'css_style_bordertop', 'border-top',
3066                  $borderwidths['top'], $borderstyles['top'], $bordercolors['top']);
3067              self::build_style_string($return, 'css_style_borderright', 'border-right',
3068                  $borderwidths['right'], $borderstyles['right'], $bordercolors['right']);
3069              self::build_style_string($return, 'css_style_borderbottom', 'border-bottom',
3070                  $borderwidths['bottom'], $borderstyles['bottom'], $bordercolors['bottom']);
3071              self::build_style_string($return, 'css_style_borderleft', 'border-left',
3072                  $borderwidths['left'], $borderstyles['left'], $bordercolors['left']);
3073          }
3074          foreach ($return as $key => $style) {
3075              if ($style->get_value() == '') {
3076                  unset($return[$key]);
3077              }
3078          }
3079          return $return;
3080      }
3081  
3082      /**
3083       * Border styles get consolidated to a single border style.
3084       *
3085       * @return string
3086       */
3087      public function consolidate_to() {
3088          return 'border';
3089      }
3090  
3091      /**
3092       * Consolidates a series of border styles into an optimised array of border
3093       * styles by looking at the direction of the border and prioritising that
3094       * during the optimisation.
3095       *
3096       * @param array $array An array to add styles into during consolidation. Passed by reference.
3097       * @param string $class The class type to initalise
3098       * @param string $style The style to create
3099       * @param string|array $top The top value
3100       * @param string $right The right value
3101       * @param string $bottom The bottom value
3102       * @param string $left The left value
3103       * @return bool
3104       */
3105      public static function consolidate_styles_by_direction(&$array, $class, $style,
3106                                                             $top, $right = null, $bottom = null, $left = null) {
3107          if (is_array($top)) {
3108              $right = $top['right'];
3109              $bottom = $top['bottom'];
3110              $left = $top['left'];
3111              $top = $top['top'];
3112          }
3113  
3114          if ($top == $bottom && $left == $right && $top == $left) {
3115              if (is_null($top)) {
3116                  $array[] = new $class($style, '');
3117              } else {
3118                  $array[] =  new $class($style, $top);
3119              }
3120          } else if ($top == null || $right == null || $bottom == null || $left == null) {
3121              if ($top !== null) {
3122                  $array[] = new $class(str_replace('border-', 'border-top-', $style), $top);
3123              }
3124              if ($right !== null) {
3125                  $array[] = new $class(str_replace('border-', 'border-right-', $style), $right);
3126              }
3127              if ($bottom !== null) {
3128                  $array[] = new $class(str_replace('border-', 'border-bottom-', $style), $bottom);
3129              }
3130              if ($left !== null) {
3131                  $array[] = new $class(str_replace('border-', 'border-left-', $style), $left);
3132              }
3133          } else if ($top == $bottom && $left == $right) {
3134              $array[] = new $class($style, $top.' '.$right);
3135          } else if ($left == $right) {
3136              $array[] = new $class($style, $top.' '.$right.' '.$bottom);
3137          } else {
3138              $array[] = new $class($style, $top.' '.$right.' '.$bottom.' '.$left);
3139          }
3140          return true;
3141      }
3142  
3143      /**
3144       * Builds a border style for a set of width, style, and colour values
3145       *
3146       * @param array $array An array into which the generated style is added
3147       * @param string $class The class type to initialise
3148       * @param string $cssstyle The style to use
3149       * @param string $width The width of the border
3150       * @param string $style The style of the border
3151       * @param string $color The colour of the border
3152       * @return bool
3153       */
3154      public static function build_style_string(&$array, $class, $cssstyle, $width = null, $style = null, $color = null) {
3155          if (!is_null($width) && !is_null($style) && !is_null($color)) {
3156              $array[] = new $class($cssstyle, $width.' '.$style.' '.$color);
3157          } else if (!is_null($width) && !is_null($style) && is_null($color)) {
3158              $array[] = new $class($cssstyle, $width.' '.$style);
3159          } else if (!is_null($width) && is_null($style) && is_null($color)) {
3160              $array[] = new $class($cssstyle, $width);
3161          } else {
3162              if (!is_null($width)) {
3163                  $array[] = new $class($cssstyle, $width);
3164              }
3165              if (!is_null($style)) {
3166                  $array[] = new $class($cssstyle, $style);
3167              }
3168              if (!is_null($color)) {
3169                  $array[] = new $class($cssstyle, $color);
3170              }
3171          }
3172          return true;
3173      }
3174  }
3175  
3176  /**
3177   * A border colour style
3178   *
3179   * @package core
3180   * @subpackage cssoptimiser
3181   * @copyright 2012 Sam Hemelryk
3182   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3183   */
3184  class css_style_bordercolor extends css_style_color {
3185  
3186      /**
3187       * Creates a new border colour style
3188       *
3189       * Based upon the colour style
3190       *
3191       * @param mixed $value
3192       * @return Array of css_style_bordercolor
3193       */
3194      public static function init($value) {
3195          $value = preg_replace('#\s+#', ' ', $value);
3196          $bits = explode(' ', $value, 4);
3197  
3198          $top = $right = $bottom = $left = null;
3199          if (count($bits) > 0) {
3200              $top = $right = $bottom = $left = array_shift($bits);
3201          }
3202          if (count($bits) > 0) {
3203              $right = $left = array_shift($bits);
3204          }
3205          if (count($bits) > 0) {
3206              $bottom = array_shift($bits);
3207          }
3208          if (count($bits) > 0) {
3209              $left = array_shift($bits);
3210          }
3211          return array(
3212              css_style_bordertopcolor::init($top),
3213              css_style_borderrightcolor::init($right),
3214              css_style_borderbottomcolor::init($bottom),
3215              css_style_borderleftcolor::init($left)
3216          );
3217      }
3218  
3219      /**
3220       * Consolidate this to a single border style
3221       *
3222       * @return string
3223       */
3224      public function consolidate_to() {
3225          return 'border';
3226      }
3227  
3228      /**
3229       * Cleans the value
3230       *
3231       * @param string $value Cleans the provided value optimising it if possible
3232       * @return string
3233       */
3234      protected function clean_value($value) {
3235          $values = explode(' ', $value);
3236          $values = array_map('parent::clean_value', $values);
3237          return join (' ', $values);
3238      }
3239  
3240      /**
3241       * Outputs this style
3242       *
3243       * @param string $overridevalue
3244       * @return string
3245       */
3246      public function out($overridevalue = null) {
3247          if ($overridevalue === null) {
3248              $overridevalue = $this->value;
3249          }
3250          $values = explode(' ', $overridevalue);
3251          $values = array_map('css_style_color::shrink_value', $values);
3252          return parent::out(join (' ', $values));
3253      }
3254  }
3255  
3256  /**
3257   * A border left style
3258   *
3259   * @package core
3260   * @subpackage cssoptimiser
3261   * @copyright 2012 Sam Hemelryk
3262   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3263   */
3264  class css_style_borderleft extends css_style_generic {
3265  
3266      /**
3267       * Initialises the border left style into individual components
3268       *
3269       * @param string $value
3270       * @return array Array of css_style_borderleftwidth|css_style_borderleftstyle|css_style_borderleftcolor
3271       */
3272      public static function init($value) {
3273          $value = preg_replace('#\s+#', ' ', $value);
3274          $bits = explode(' ', $value, 3);
3275  
3276          $return = array();
3277          if (count($bits) > 0) {
3278              $return[] = css_style_borderleftwidth::init(array_shift($bits));
3279          }
3280          if (count($bits) > 0) {
3281              $return[] = css_style_borderleftstyle::init(array_shift($bits));
3282          }
3283          if (count($bits) > 0) {
3284              $return[] = css_style_borderleftcolor::init(array_shift($bits));
3285          }
3286          return $return;
3287      }
3288  
3289      /**
3290       * Consolidate this to a single border style
3291       *
3292       * @return string
3293       */
3294      public function consolidate_to() {
3295          return 'border';
3296      }
3297  }
3298  
3299  /**
3300   * A border right style
3301   *
3302   * @package core
3303   * @subpackage cssoptimiser
3304   * @copyright 2012 Sam Hemelryk
3305   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3306   */
3307  class css_style_borderright extends css_style_generic {
3308  
3309      /**
3310       * Initialises the border right style into individual components
3311       *
3312       * @param string $value The value of the style
3313       * @return array Array of css_style_borderrightwidth|css_style_borderrightstyle|css_style_borderrightcolor
3314       */
3315      public static function init($value) {
3316          $value = preg_replace('#\s+#', ' ', $value);
3317          $bits = explode(' ', $value, 3);
3318  
3319          $return = array();
3320          if (count($bits) > 0) {
3321              $return[] = css_style_borderrightwidth::init(array_shift($bits));
3322          }
3323          if (count($bits) > 0) {
3324              $return[] = css_style_borderrightstyle::init(array_shift($bits));
3325          }
3326          if (count($bits) > 0) {
3327              $return[] = css_style_borderrightcolor::init(array_shift($bits));
3328          }
3329          return $return;
3330      }
3331  
3332      /**
3333       * Consolidate this to a single border style
3334       *
3335       * @return string
3336       */
3337      public function consolidate_to() {
3338          return 'border';
3339      }
3340  }
3341  
3342  /**
3343   * A border top style
3344   *
3345   * @package core
3346   * @subpackage cssoptimiser
3347   * @copyright 2012 Sam Hemelryk
3348   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3349   */
3350  class css_style_bordertop extends css_style_generic {
3351  
3352      /**
3353       * Initialises the border top style into individual components
3354       *
3355       * @param string $value The value of the style
3356       * @return array Array of css_style_bordertopwidth|css_style_bordertopstyle|css_style_bordertopcolor
3357       */
3358      public static function init($value) {
3359          $value = preg_replace('#\s+#', ' ', $value);
3360          $bits = explode(' ', $value, 3);
3361  
3362          $return = array();
3363          if (count($bits) > 0) {
3364              $return[] = css_style_bordertopwidth::init(array_shift($bits));
3365          }
3366          if (count($bits) > 0) {
3367              $return[] = css_style_bordertopstyle::init(array_shift($bits));
3368          }
3369          if (count($bits) > 0) {
3370              $return[] = css_style_bordertopcolor::init(array_shift($bits));
3371          }
3372          return $return;
3373      }
3374  
3375      /**
3376       * Consolidate this to a single border style
3377       *
3378       * @return string
3379       */
3380      public function consolidate_to() {
3381          return 'border';
3382      }
3383  }
3384  
3385  /**
3386   * A border bottom style
3387   *
3388   * @package core
3389   * @subpackage cssoptimiser
3390   * @copyright 2012 Sam Hemelryk
3391   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3392   */
3393  class css_style_borderbottom extends css_style_generic {
3394  
3395      /**
3396       * Initialises the border bottom style into individual components
3397       *
3398       * @param string $value The value of the style
3399       * @return array Array of css_style_borderbottomwidth|css_style_borderbottomstyle|css_style_borderbottomcolor
3400       */
3401      public static function init($value) {
3402          $value = preg_replace('#\s+#', ' ', $value);
3403          $bits = explode(' ', $value, 3);
3404  
3405          $return = array();
3406          if (count($bits) > 0) {
3407              $return[] = css_style_borderbottomwidth::init(array_shift($bits));
3408          }
3409          if (count($bits) > 0) {
3410              $return[] = css_style_borderbottomstyle::init(array_shift($bits));
3411          }
3412          if (count($bits) > 0) {
3413              $return[] = css_style_borderbottomcolor::init(array_shift($bits));
3414          }
3415          return $return;
3416      }
3417  
3418      /**
3419       * Consolidate this to a single border style
3420       *
3421       * @return string
3422       */
3423      public function consolidate_to() {
3424          return 'border';
3425      }
3426  }
3427  
3428  /**
3429   * A border width style
3430   *
3431   * @package core
3432   * @subpackage cssoptimiser
3433   * @copyright 2012 Sam Hemelryk
3434   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3435   */
3436  class css_style_borderwidth extends css_style_width {
3437  
3438      /**
3439       * Creates a new border colour style
3440       *
3441       * Based upon the colour style
3442       *
3443       * @param string $value The value of the style
3444       * @return array Array of css_style_border*width
3445       */
3446      public static function init($value) {
3447          $value = preg_replace('#\s+#', ' ', $value);
3448          $bits = explode(' ', $value, 4);
3449  
3450          $top = $right = $bottom = $left = null;
3451          if (count($bits) > 0) {
3452              $top = $right = $bottom = $left = array_shift($bits);
3453          }
3454          if (count($bits) > 0) {
3455              $right = $left = array_shift($bits);
3456          }
3457          if (count($bits) > 0) {
3458              $bottom = array_shift($bits);
3459          }
3460          if (count($bits) > 0) {
3461              $left = array_shift($bits);
3462          }
3463          return array(
3464              css_style_bordertopwidth::init($top),
3465              css_style_borderrightwidth::init($right),
3466              css_style_borderbottomwidth::init($bottom),
3467              css_style_borderleftwidth::init($left)
3468          );
3469      }
3470  
3471      /**
3472       * Consolidate this to a single border style
3473       *
3474       * @return string
3475       */
3476      public function consolidate_to() {
3477          return 'border';
3478      }
3479  
3480      /**
3481       * Checks if the width is valid
3482       * @return bool
3483       */
3484      public function is_valid() {
3485          return self::is_border_width($this->value);
3486      }
3487  
3488      /**
3489       * Cleans the provided value
3490       *
3491       * @param mixed $value Cleans the provided value optimising it if possible
3492       * @return string
3493       */
3494      protected function clean_value($value) {
3495          $isvalid = self::is_border_width($value);
3496          if (!$isvalid) {
3497              $this->set_error('Invalid width specified for '.$this->name);
3498          } else if (preg_match('#^0\D+$#', $value)) {
3499              return '0';
3500          }
3501          return trim($value);
3502      }
3503  
3504      /**
3505       * Returns true if the provided value is a permitted border width
3506       * @param string $value The value to check
3507       * @return bool
3508       */
3509      public static function is_border_width($value) {
3510          $altwidthvalues = array('thin', 'medium', 'thick');
3511          return css_is_width($value) || in_array($value, $altwidthvalues);
3512      }
3513  }
3514  
3515  /**
3516   * A border style style
3517   *
3518   * @package core
3519   * @subpackage cssoptimiser
3520   * @copyright 2012 Sam Hemelryk
3521   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3522   */
3523  class css_style_borderstyle extends css_style_generic {
3524  
3525      /**
3526       * Creates a new border colour style
3527       *
3528       * Based upon the colour style
3529       *
3530       * @param string $value The value of the style
3531       * @return array Array of css_style_border*style
3532       */
3533      public static function init($value) {
3534          $value = preg_replace('#\s+#', ' ', $value);
3535          $bits = explode(' ', $value, 4);
3536  
3537          $top = $right = $bottom = $left = null;
3538          if (count($bits) > 0) {
3539              $top = $right = $bottom = $left = array_shift($bits);
3540          }
3541          if (count($bits) > 0) {
3542              $right = $left = array_shift($bits);
3543          }
3544          if (count($bits) > 0) {
3545              $bottom = array_shift($bits);
3546          }
3547          if (count($bits) > 0) {
3548              $left = array_shift($bits);
3549          }
3550          return array(
3551              css_style_bordertopstyle::init($top),
3552              css_style_borderrightstyle::init($right),
3553              css_style_borderbottomstyle::init($bottom),
3554              css_style_borderleftstyle::init($left)
3555          );
3556      }
3557  
3558      /**
3559       * Consolidate this to a single border style
3560       *
3561       * @return string
3562       */
3563      public function consolidate_to() {
3564          return 'border';
3565      }
3566  }
3567  
3568  /**
3569   * A border top colour style
3570   *
3571   * @package core
3572   * @subpackage cssoptimiser
3573   * @copyright 2012 Sam Hemelryk
3574   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3575   */
3576  class css_style_bordertopcolor extends css_style_bordercolor {
3577  
3578      /**
3579       * Initialises this style object
3580       *
3581       * @param string $value The value of the style
3582       * @return css_style_bordertopcolor
3583       */
3584      public static function init($value) {
3585          return new css_style_bordertopcolor('border-top-color', $value);
3586      }
3587  
3588      /**
3589       * Consolidate this to a single border style
3590       *
3591       * @return string
3592       */
3593      public function consolidate_to() {
3594          return 'border';
3595      }
3596  }
3597  
3598  /**
3599   * A border left colour style
3600   *
3601   * @package core
3602   * @subpackage cssoptimiser
3603   * @copyright 2012 Sam Hemelryk
3604   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3605   */
3606  class css_style_borderleftcolor extends css_style_bordercolor {
3607  
3608      /**
3609       * Initialises this style object
3610       *
3611       * @param string $value The value of the style
3612       * @return css_style_borderleftcolor
3613       */
3614      public static function init($value) {
3615          return new css_style_borderleftcolor('border-left-color', $value);
3616      }
3617  
3618      /**
3619       * Consolidate this to a single border style
3620       *
3621       * @return string
3622       */
3623      public function consolidate_to() {
3624          return 'border';
3625      }
3626  }
3627  
3628  /**
3629   * A border right colour style
3630   *
3631   * @package core
3632   * @subpackage cssoptimiser
3633   * @copyright 2012 Sam Hemelryk
3634   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3635   */
3636  class css_style_borderrightcolor extends css_style_bordercolor {
3637  
3638      /**
3639       * Initialises this style object
3640       *
3641       * @param string $value The value of the style
3642       * @return css_style_borderrightcolor
3643       */
3644      public static function init($value) {
3645          return new css_style_borderrightcolor('border-right-color', $value);
3646      }
3647  
3648      /**
3649       * Consolidate this to a single border style
3650       *
3651       * @return string
3652       */
3653      public function consolidate_to() {
3654          return 'border';
3655      }
3656  }
3657  
3658  /**
3659   * A border bottom colour style
3660   *
3661   * @package core
3662   * @subpackage cssoptimiser
3663   * @copyright 2012 Sam Hemelryk
3664   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3665   */
3666  class css_style_borderbottomcolor extends css_style_bordercolor {
3667  
3668      /**
3669       * Initialises this style object
3670       *
3671       * @param string $value The value of the style
3672       * @return css_style_borderbottomcolor
3673       */
3674      public static function init($value) {
3675          return new css_style_borderbottomcolor('border-bottom-color', $value);
3676      }
3677  
3678      /**
3679       * Consolidate this to a single border style
3680       *
3681       * @return string
3682       */
3683      public function consolidate_to() {
3684          return 'border';
3685      }
3686  }
3687  
3688  /**
3689   * A border width top style
3690   *
3691   * @package core
3692   * @subpackage cssoptimiser
3693   * @copyright 2012 Sam Hemelryk
3694   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3695   */
3696  class css_style_bordertopwidth extends css_style_borderwidth {
3697  
3698      /**
3699       * Initialises this style object
3700       *
3701       * @param string $value The value of the style
3702       * @return css_style_bordertopwidth
3703       */
3704      public static function init($value) {
3705          return new css_style_bordertopwidth('border-top-width', $value);
3706      }
3707  
3708      /**
3709       * Consolidate this to a single border style
3710       *
3711       * @return string
3712       */
3713      public function consolidate_to() {
3714          return 'border';
3715      }
3716  }
3717  
3718  /**
3719   * A border width left style
3720   *
3721   * @package core
3722   * @subpackage cssoptimiser
3723   * @copyright 2012 Sam Hemelryk
3724   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3725   */
3726  class css_style_borderleftwidth extends css_style_borderwidth {
3727  
3728      /**
3729       * Initialises this style object
3730       *
3731       * @param string $value The value of the style
3732       * @return css_style_borderleftwidth
3733       */
3734      public static function init($value) {
3735          return new css_style_borderleftwidth('border-left-width', $value);
3736      }
3737  
3738      /**
3739       * Consolidate this to a single border style
3740       *
3741       * @return string
3742       */
3743      public function consolidate_to() {
3744          return 'border';
3745      }
3746  }
3747  
3748  /**
3749   * A border width right style
3750   *
3751   * @package core
3752   * @subpackage cssoptimiser
3753   * @copyright 2012 Sam Hemelryk
3754   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3755   */
3756  class css_style_borderrightwidth extends css_style_borderwidth {
3757  
3758      /**
3759       * Initialises this style object
3760       *
3761       * @param string $value The value of the style
3762       * @return css_style_borderrightwidth
3763       */
3764      public static function init($value) {
3765          return new css_style_borderrightwidth('border-right-width', $value);
3766      }
3767  
3768      /**
3769       * Consolidate this to a single border style
3770       *
3771       * @return string
3772       */
3773      public function consolidate_to() {
3774          return 'border';
3775      }
3776  }
3777  
3778  /**
3779   * A border width bottom style
3780   *
3781   * @package core
3782   * @subpackage cssoptimiser
3783   * @copyright 2012 Sam Hemelryk
3784   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3785   */
3786  class css_style_borderbottomwidth extends css_style_borderwidth {
3787  
3788      /**
3789       * Initialises this style object
3790       *
3791       * @param string $value The value of the style
3792       * @return css_style_borderbottomwidth
3793       */
3794      public static function init($value) {
3795          return new css_style_borderbottomwidth('border-bottom-width', $value);
3796      }
3797  
3798      /**
3799       * Consolidate this to a single border style
3800       *
3801       * @return string
3802       */
3803      public function consolidate_to() {
3804          return 'border';
3805      }
3806  }
3807  
3808  /**
3809   * A border top style
3810   *
3811   * @package core
3812   * @subpackage cssoptimiser
3813   * @copyright 2012 Sam Hemelryk
3814   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3815   */
3816  class css_style_bordertopstyle extends css_style_borderstyle {
3817  
3818      /**
3819       * Initialises this style object
3820       *
3821       * @param string $value The value of the style
3822       * @return css_style_bordertopstyle
3823       */
3824      public static function init($value) {
3825          return new css_style_bordertopstyle('border-top-style', $value);
3826      }
3827  
3828      /**
3829       * Consolidate this to a single border style
3830       *
3831       * @return string
3832       */
3833      public function consolidate_to() {
3834          return 'border';
3835      }
3836  }
3837  
3838  /**
3839   * A border left style
3840   *
3841   * @package core
3842   * @subpackage cssoptimiser
3843   * @copyright 2012 Sam Hemelryk
3844   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3845   */
3846  class css_style_borderleftstyle extends css_style_borderstyle {
3847  
3848      /**
3849       * Initialises this style object
3850       *
3851       * @param string $value The value of the style
3852       * @return css_style_borderleftstyle
3853       */
3854      public static function init($value) {
3855          return new css_style_borderleftstyle('border-left-style', $value);
3856      }
3857  
3858      /**
3859       * Consolidate this to a single border style
3860       *
3861       * @return string
3862       */
3863      public function consolidate_to() {
3864          return 'border';
3865      }
3866  }
3867  
3868  /**
3869   * A border right style
3870   *
3871   * @package core
3872   * @subpackage cssoptimiser
3873   * @copyright 2012 Sam Hemelryk
3874   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3875   */
3876  class css_style_borderrightstyle extends css_style_borderstyle {
3877  
3878      /**
3879       * Initialises this style object
3880       *
3881       * @param string $value The value of the style
3882       * @return css_style_borderrightstyle
3883       */
3884      public static function init($value) {
3885          return new css_style_borderrightstyle('border-right-style', $value);
3886      }
3887  
3888      /**
3889       * Consolidate this to a single border style
3890       *
3891       * @return string
3892       */
3893      public function consolidate_to() {
3894          return 'border';
3895      }
3896  }
3897  
3898  /**
3899   * A border bottom style
3900   *
3901   * @package core
3902   * @subpackage cssoptimiser
3903   * @copyright 2012 Sam Hemelryk
3904   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3905   */
3906  class css_style_borderbottomstyle extends css_style_borderstyle {
3907  
3908      /**
3909       * Initialises this style object
3910       *
3911       * @param string $value The value for the style
3912       * @return css_style_borderbottomstyle
3913       */
3914      public static function init($value) {
3915          return new css_style_borderbottomstyle('border-bottom-style', $value);
3916      }
3917  
3918      /**
3919       * Consolidate this to a single border style
3920       *
3921       * @return string
3922       */
3923      public function consolidate_to() {
3924          return 'border';
3925      }
3926  }
3927  
3928  /**
3929   * A background style
3930   *
3931   * @package core
3932   * @subpackage cssoptimiser
3933   * @copyright 2012 Sam Hemelryk
3934   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
3935   */
3936  class css_style_background extends css_style implements core_css_consolidatable_style {
3937  
3938      /**
3939       * Initialises a background style
3940       *
3941       * @param string $value The value of the style
3942       * @return array An array of background component.
3943       */
3944      public static function init($value) {
3945          // Colour - image - repeat - attachment - position.
3946          $imageurl = null;
3947          if (preg_match('#url\(([^\)]+)\)#', $value, $matches)) {
3948              $imageurl = trim($matches[1]);
3949              $value = str_replace($matches[1], '', $value);
3950          }
3951  
3952          // Switch out the brackets so that they don't get messed up when we explode.
3953          $brackets = array();
3954          $bracketcount = 0;
3955          while (preg_match('#\([^\)\(]+\)#', $value, $matches)) {
3956              $key = "##BRACKET-{$bracketcount}##";
3957              $bracketcount++;
3958              $brackets[$key] = $matches[0];
3959              $value = str_replace($matches[0], $key, $value);
3960          }
3961  
3962          $important = (stripos($value, '!important') !== false);
3963          if ($important) {
3964              // Great some genius put !important in the background shorthand property.
3965              $value = str_replace('!important', '', $value);
3966          }
3967  
3968          $value = preg_replace('#\s+#', ' ', $value);
3969          $bits = explode(' ', $value);
3970  
3971          foreach ($bits as $key => $bit) {
3972              $bits[$key] = self::replace_bracket_placeholders($bit, $brackets);
3973          }
3974          unset($bracketcount);
3975          unset($brackets);
3976  
3977          $repeats = array('repeat', 'repeat-x', 'repeat-y', 'no-repeat', 'inherit');
3978          $attachments = array('scroll' , 'fixed', 'inherit');
3979          $positions = array('top', 'left', 'bottom', 'right', 'center');
3980  
3981          /* @var css_style_background[] $return */
3982          $return = array();
3983          $unknownbits = array();
3984  
3985          $color = self::NULL_VALUE;
3986          if (count($bits) > 0 && css_is_colour(reset($bits))) {
3987              $color = array_shift($bits);
3988          }
3989  
3990          $image = self::NULL_VALUE;
3991          if (count($bits) > 0 && preg_match('#^\s*(none|inherit|url\(\))\s*$#', reset($bits))) {
3992              $image = array_shift($bits);
3993              if ($image == 'url()') {
3994                  $image = "url({$imageurl})";
3995              }
3996          }
3997  
3998          $repeat = self::NULL_VALUE;
3999          if (count($bits) > 0 && in_array(reset($bits), $repeats)) {
4000              $repeat = array_shift($bits);
4001          }
4002  
4003          $attachment = self::NULL_VALUE;
4004          if (count($bits) > 0 && in_array(reset($bits), $attachments)) {
4005              // Scroll , fixed, inherit.
4006              $attachment = array_shift($bits);
4007          }
4008  
4009          $position = self::NULL_VALUE;
4010          if (count($bits) > 0) {
4011              $widthbits = array();
4012              foreach ($bits as $bit) {
4013                  if (in_array($bit, $positions) || css_is_width($bit)) {
4014                      $widthbits[] = $bit;
4015                  } else {
4016                      $unknownbits[] = $bit;
4017                  }
4018              }
4019              if (count($widthbits)) {
4020                  $position = join(' ', $widthbits);
4021              }
4022          }
4023  
4024          if (count($unknownbits)) {
4025              foreach ($unknownbits as $bit) {
4026                  $bit = trim($bit);
4027                  if ($color === self::NULL_VALUE && css_is_colour($bit)) {
4028                      $color = $bit;
4029                  } else if ($repeat === self::NULL_VALUE && in_array($bit, $repeats)) {
4030                      $repeat = $bit;
4031                  } else if ($attachment === self::NULL_VALUE && in_array($bit, $attachments)) {
4032                      $attachment = $bit;
4033                  } else if ($bit !== '') {
4034                      $advanced = css_style_background_advanced::init($bit);
4035                      if ($important) {
4036                          $advanced->set_important();
4037                      }
4038                      $return[] = $advanced;
4039                  }
4040              }
4041          }
4042  
4043          if ($color === self::NULL_VALUE &&
4044              $image === self::NULL_VALUE &&
4045              $repeat === self::NULL_VALUE && $attachment === self::NULL_VALUE &&
4046              $position === self::NULL_VALUE) {
4047              // All primaries are null, return without doing anything else. There may be advanced madness there.
4048              return $return;
4049          }
4050  
4051          $return[] = css_style_backgroundcolor::init($color);
4052          $return[] = css_style_backgroundimage::init($image);
4053          $return[] = css_style_backgroundrepeat::init($repeat);
4054          $return[] = css_style_backgroundattachment::init($attachment);
4055          $return[] = css_style_backgroundposition::init($position);
4056  
4057          if ($important) {
4058              foreach ($return as $style) {
4059                  $style->set_important();
4060              }
4061          }
4062  
4063          return $return;
4064      }
4065  
4066      /**
4067       * Static helper method to switch in bracket replacements
4068       *
4069       * @param string $value
4070       * @param array $placeholders
4071       * @return string
4072       */
4073      protected static function replace_bracket_placeholders($value, array $placeholders) {
4074          while (preg_match('/##BRACKET-\d+##/', $value, $matches)) {
4075              $value = str_replace($matches[0], $placeholders[$matches[0]], $value);
4076          }
4077          return $value;
4078      }
4079  
4080      /**
4081       * Consolidates background styles into a single background style
4082       *
4083       * @param css_style_background[] $styles Consolidates the provided array of background styles
4084       * @return css_style[] Consolidated optimised background styles
4085       */
4086      public static function consolidate(array $styles) {
4087  
4088          if (empty($styles)) {
4089              return $styles;
4090          }
4091  
4092          $color = null;
4093          $image = null;
4094          $repeat = null;
4095          $attachment = null;
4096          $position = null;
4097          $size = null;
4098          $origin = null;
4099          $clip = null;
4100  
4101          $someimportant = false;
4102          $allimportant = null;
4103          foreach ($styles as $style) {
4104              if ($style instanceof css_style_backgroundimage_advanced) {
4105                  continue;
4106              }
4107              if ($style->is_important()) {
4108                  $someimportant = true;
4109                  if ($allimportant === null) {
4110                      $allimportant = true;
4111                  }
4112              } else if ($allimportant === true) {
4113                  $allimportant = false;
4114              }
4115          }
4116  
4117          /* @var css_style[] $organisedstyles */
4118          $organisedstyles = array();
4119          /* @var css_style[] $advancedstyles */
4120          $advancedstyles = array();
4121          /* @var css_style[] $importantstyles */
4122          $importantstyles = array();
4123          foreach ($styles as $style) {
4124              if ($style instanceof css_style_backgroundimage_advanced) {
4125                  $advancedstyles[] = $style;
4126                  continue;
4127              }
4128              if ($someimportant && !$allimportant && $style->is_important()) {
4129                  $importantstyles[] = $style;
4130                  continue;
4131              }
4132              $organisedstyles[$style->get_name()] = $style;
4133              switch ($style->get_name()) {
4134                  case 'background-color' :
4135                      $color = css_style_color::shrink_value($style->get_value(false));
4136                      break;
4137                  case 'background-image' :
4138                      $image = $style->get_value(false);
4139                      break;
4140                  case 'background-repeat' :
4141                      $repeat = $style->get_value(false);
4142                      break;
4143                  case 'background-attachment' :
4144                      $attachment = $style->get_value(false);
4145                      break;
4146                  case 'background-position' :
4147                      $position = $style->get_value(false);
4148                      break;
4149                  case 'background-clip' :
4150                      $clip = $style->get_value();
4151                      break;
4152                  case 'background-origin' :
4153                      $origin = $style->get_value();
4154                      break;
4155                  case 'background-size' :
4156                      $size = $style->get_value();
4157                      break;
4158              }
4159          }
4160  
4161          /* @var css_style[] $consolidatetosingle */
4162          $consolidatetosingle = array();
4163          if (!is_null($color) && !is_null($image) && !is_null($repeat) && !is_null($attachment) && !is_null($position)) {
4164              // We can use the shorthand background-style!
4165              if (!$organisedstyles['background-color']->is_special_empty_value()) {
4166                  $consolidatetosingle[] = $color;
4167              }
4168              if (!$organisedstyles['background-image']->is_special_empty_value()) {
4169                  $consolidatetosingle[] = $image;
4170              }
4171              if (!$organisedstyles['background-repeat']->is_special_empty_value()) {
4172                  $consolidatetosingle[] = $repeat;
4173              }
4174              if (!$organisedstyles['background-attachment']->is_special_empty_value()) {
4175                  $consolidatetosingle[] = $attachment;
4176              }
4177              if (!$organisedstyles['background-position']->is_special_empty_value()) {
4178                  $consolidatetosingle[] = $position;
4179              }
4180              // Reset them all to null so we don't use them again.
4181              $color = null;
4182              $image = null;
4183              $repeat = null;
4184              $attachment = null;
4185              $position = null;
4186          }
4187  
4188          $return = array();
4189          // Single background style needs to come first.
4190          if (count($consolidatetosingle) > 0) {
4191              $returnstyle = new css_style_background('background', join(' ', $consolidatetosingle));
4192              if ($allimportant) {
4193                  $returnstyle->set_important();
4194              }
4195              $return[] = $returnstyle;
4196          }
4197          foreach ($styles as $style) {
4198              $value = null;
4199              switch ($style->get_name()) {
4200                  case 'background-color' :
4201                      $value = $color;
4202                      break;
4203                  case 'background-image' :
4204                      $value = $image;
4205                      break;
4206                  case 'background-repeat' :
4207                      $value = $repeat;
4208                      break;
4209                  case 'background-attachment' :
4210                      $value = $attachment;
4211                      break;
4212                  case 'background-position' :
4213                      $value = $position;
4214                      break;
4215                  case 'background-clip' :
4216                      $value = $clip;
4217                      break;
4218                  case 'background-origin':
4219                      $value = $origin;
4220                      break;
4221                  case 'background-size':
4222                      $value = $size;
4223                      break;
4224              }
4225              if (!is_null($value)) {
4226                  $return[] = $style;
4227              }
4228          }
4229          $return = array_merge($return, $importantstyles, $advancedstyles);
4230          return $return;
4231      }
4232  }
4233  
4234  /**
4235   * A advanced background style that allows multiple values to preserve unknown entities
4236   *
4237   * @package core
4238   * @subpackage cssoptimiser
4239   * @copyright 2012 Sam Hemelryk
4240   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4241   */
4242  class css_style_background_advanced extends css_style_generic {
4243      /**
4244       * Creates a new background colour style
4245       *
4246       * @param string $value The value of the style
4247       * @return css_style_backgroundimage
4248       */
4249      public static function init($value) {
4250          $value = preg_replace('#\s+#', ' ', $value);
4251          return new css_style_background_advanced('background', $value);
4252      }
4253  
4254      /**
4255       * Returns true because the advanced background image supports multiple values.
4256       * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4257       *
4258       * @return boolean
4259       */
4260      public function allows_multiple_values() {
4261          return true;
4262      }
4263  }
4264  
4265  /**
4266   * A background colour style.
4267   *
4268   * Based upon the colour style.
4269   *
4270   * @package core
4271   * @subpackage cssoptimiser
4272   * @copyright 2012 Sam Hemelryk
4273   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4274   */
4275  class css_style_backgroundcolor extends css_style_color {
4276  
4277      /**
4278       * Creates a new background colour style
4279       *
4280       * @param string $value The value of the style
4281       * @return css_style_backgroundcolor
4282       */
4283      public static function init($value) {
4284          return new css_style_backgroundcolor('background-color', $value);
4285      }
4286  
4287      /**
4288       * css_style_backgroundcolor consolidates to css_style_background
4289       *
4290       * @return string
4291       */
4292      public function consolidate_to() {
4293          return 'background';
4294      }
4295  
4296      /**
4297       * Returns true if the value for this style is the special null value.
4298       *
4299       * This occurs if the shorthand background property was used but no proper value
4300       * was specified for this style.
4301       * This leads to a null value being used unless otherwise overridden.
4302       *
4303       * @return bool
4304       */
4305      public function is_special_empty_value() {
4306          return ($this->value === self::NULL_VALUE);
4307      }
4308  
4309      /**
4310       * Returns true if the value for this style is valid
4311       * @return bool
4312       */
4313      public function is_valid() {
4314          return $this->is_special_empty_value() || parent::is_valid();
4315      }
4316  }
4317  
4318  /**
4319   * A background image style.
4320   *
4321   * @package core
4322   * @subpackage cssoptimiser
4323   * @copyright 2012 Sam Hemelryk
4324   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4325   */
4326  class css_style_backgroundimage extends css_style_generic {
4327  
4328      /**
4329       * Creates a new background image style
4330       *
4331       * @param string $value The value of the style
4332       * @return css_style_backgroundimage
4333       */
4334      public static function init($value) {
4335          if ($value !== self::NULL_VALUE && !preg_match('#^\s*(none|inherit|url\()#i', $value)) {
4336              return css_style_backgroundimage_advanced::init($value);
4337          }
4338          return new css_style_backgroundimage('background-image', $value);
4339      }
4340  
4341      /**
4342       * Consolidates this style into a single background style
4343       *
4344       * @return string
4345       */
4346      public function consolidate_to() {
4347          return 'background';
4348      }
4349  
4350      /**
4351       * Returns true if the value for this style is the special null value.
4352       *
4353       * This occurs if the shorthand background property was used but no proper value
4354       * was specified for this style.
4355       * This leads to a null value being used unless otherwise overridden.
4356       *
4357       * @return bool
4358       */
4359      public function is_special_empty_value() {
4360          return ($this->value === self::NULL_VALUE);
4361      }
4362  
4363      /**
4364       * Returns true if the value for this style is valid
4365       * @return bool
4366       */
4367      public function is_valid() {
4368          return $this->is_special_empty_value() || parent::is_valid();
4369      }
4370  }
4371  
4372  /**
4373   * A background image style that supports multiple values and masquerades as a background-image
4374   *
4375   * @package core
4376   * @subpackage cssoptimiser
4377   * @copyright 2012 Sam Hemelryk
4378   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4379   */
4380  class css_style_backgroundimage_advanced extends css_style_generic {
4381      /**
4382       * Creates a new background colour style
4383       *
4384       * @param string $value The value of the style
4385       * @return css_style_backgroundimage
4386       */
4387      public static function init($value) {
4388          $value = preg_replace('#\s+#', ' ', $value);
4389          return new css_style_backgroundimage_advanced('background-image', $value);
4390      }
4391  
4392      /**
4393       * Returns true because the advanced background image supports multiple values.
4394       * e.g. -webkit-linear-gradient and -moz-linear-gradient.
4395       *
4396       * @return boolean
4397       */
4398      public function allows_multiple_values() {
4399          return true;
4400      }
4401  }
4402  
4403  /**
4404   * A background repeat style.
4405   *
4406   * @package core
4407   * @subpackage cssoptimiser
4408   * @copyright 2012 Sam Hemelryk
4409   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4410   */
4411  class css_style_backgroundrepeat extends css_style_generic {
4412  
4413      /**
4414       * Creates a new background colour style
4415       *
4416       * @param string $value The value of the style
4417       * @return css_style_backgroundrepeat
4418       */
4419      public static function init($value) {
4420          return new css_style_backgroundrepeat('background-repeat', $value);
4421      }
4422  
4423      /**
4424       * Consolidates this style into a single background style
4425       *
4426       * @return string
4427       */
4428      public function consolidate_to() {
4429          return 'background';
4430      }
4431  
4432      /**
4433       * Returns true if the value for this style is the special null value.
4434       *
4435       * This occurs if the shorthand background property was used but no proper value
4436       * was specified for this style.
4437       * This leads to a null value being used unless otherwise overridden.
4438       *
4439       * @return bool
4440       */
4441      public function is_special_empty_value() {
4442          return ($this->value === self::NULL_VALUE);
4443      }
4444  
4445      /**
4446       * Returns true if the value for this style is valid
4447       * @return bool
4448       */
4449      public function is_valid() {
4450          return $this->is_special_empty_value() || parent::is_valid();
4451      }
4452  }
4453  
4454  /**
4455   * A background attachment style.
4456   *
4457   * @package core
4458   * @subpackage cssoptimiser
4459   * @copyright 2012 Sam Hemelryk
4460   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4461   */
4462  class css_style_backgroundattachment extends css_style_generic {
4463  
4464      /**
4465       * Creates a new background colour style
4466       *
4467       * @param string $value The value of the style
4468       * @return css_style_backgroundattachment
4469       */
4470      public static function init($value) {
4471          return new css_style_backgroundattachment('background-attachment', $value);
4472      }
4473  
4474      /**
4475       * Consolidates this style into a single background style
4476       *
4477       * @return string
4478       */
4479      public function consolidate_to() {
4480          return 'background';
4481      }
4482  
4483      /**
4484       * Returns true if the value for this style is the special null value.
4485       *
4486       * This occurs if the shorthand background property was used but no proper value
4487       * was specified for this style.
4488       * This leads to a null value being used unless otherwise overridden.
4489       *
4490       * @return bool
4491       */
4492      public function is_special_empty_value() {
4493          return ($this->value === self::NULL_VALUE);
4494      }
4495  
4496      /**
4497       * Returns true if the value for this style is valid
4498       * @return bool
4499       */
4500      public function is_valid() {
4501          return $this->is_special_empty_value() || parent::is_valid();
4502      }
4503  }
4504  
4505  /**
4506   * A background position style.
4507   *
4508   * @package core
4509   * @subpackage cssoptimiser
4510   * @copyright 2012 Sam Hemelryk
4511   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4512   */
4513  class css_style_backgroundposition extends css_style_generic {
4514  
4515      /**
4516       * Creates a new background colour style
4517       *
4518       * @param string $value The value of the style
4519       * @return css_style_backgroundposition
4520       */
4521      public static function init($value) {
4522          return new css_style_backgroundposition('background-position', $value);
4523      }
4524  
4525      /**
4526       * Consolidates this style into a single background style
4527       *
4528       * @return string
4529       */
4530      public function consolidate_to() {
4531          return 'background';
4532      }
4533  
4534      /**
4535       * Returns true if the value for this style is the special null value.
4536       *
4537       * This occurs if the shorthand background property was used but no proper value
4538       * was specified for this style.
4539       * This leads to a null value being used unless otherwise overridden.
4540       *
4541       * @return bool
4542       */
4543      public function is_special_empty_value() {
4544          return ($this->value === self::NULL_VALUE);
4545      }
4546  
4547      /**
4548       * Returns true if the value for this style is valid
4549       * @return bool
4550       */
4551      public function is_valid() {
4552          return $this->is_special_empty_value() || parent::is_valid();
4553      }
4554  }
4555  
4556  /**
4557   * A background size style.
4558   *
4559   * @package core
4560   * @subpackage cssoptimiser
4561   * @copyright 2012 Sam Hemelryk
4562   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4563   */
4564  class css_style_backgroundsize extends css_style_generic {
4565  
4566      /**
4567       * Creates a new background size style
4568       *
4569       * @param string $value The value of the style
4570       * @return css_style_backgroundposition
4571       */
4572      public static function init($value) {
4573          return new css_style_backgroundsize('background-size', $value);
4574      }
4575  
4576      /**
4577       * Consolidates this style into a single background style
4578       *
4579       * @return string
4580       */
4581      public function consolidate_to() {
4582          return 'background';
4583      }
4584  }
4585  
4586  /**
4587   * A background clip style.
4588   *
4589   * @package core
4590   * @subpackage cssoptimiser
4591   * @copyright 2012 Sam Hemelryk
4592   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4593   */
4594  class css_style_backgroundclip extends css_style_generic {
4595  
4596      /**
4597       * Creates a new background clip style
4598       *
4599       * @param string $value The value of the style
4600       * @return css_style_backgroundposition
4601       */
4602      public static function init($value) {
4603          return new css_style_backgroundclip('background-clip', $value);
4604      }
4605  
4606      /**
4607       * Consolidates this style into a single background style
4608       *
4609       * @return string
4610       */
4611      public function consolidate_to() {
4612          return 'background';
4613      }
4614  }
4615  
4616  /**
4617   * A background origin style.
4618   *
4619   * @package core
4620   * @subpackage cssoptimiser
4621   * @copyright 2012 Sam Hemelryk
4622   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4623   */
4624  class css_style_backgroundorigin extends css_style_generic {
4625  
4626      /**
4627       * Creates a new background origin style
4628       *
4629       * @param string $value The value of the style
4630       * @return css_style_backgroundposition
4631       */
4632      public static function init($value) {
4633          return new css_style_backgroundorigin('background-origin', $value);
4634      }
4635  
4636      /**
4637       * Consolidates this style into a single background style
4638       *
4639       * @return string
4640       */
4641      public function consolidate_to() {
4642          return 'background';
4643      }
4644  }
4645  
4646  /**
4647   * A padding style.
4648   *
4649   * @package core
4650   * @subpackage cssoptimiser
4651   * @copyright 2012 Sam Hemelryk
4652   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4653   */
4654  class css_style_padding extends css_style_width implements core_css_consolidatable_style {
4655  
4656      /**
4657       * Initialises this padding style into several individual padding styles
4658       *
4659       * @param string $value The value fo the style
4660       * @return array An array of padding styles
4661       */
4662      public static function init($value) {
4663          $important = '';
4664          if (strpos($value, '!important') !== false) {
4665              $important = ' !important';
4666              $value = str_replace('!important', '', $value);
4667          }
4668  
4669          $value = preg_replace('#\s+#', ' ', trim($value));
4670          $bits = explode(' ', $value, 4);
4671  
4672          $top = $right = $bottom = $left = null;
4673          if (count($bits) > 0) {
4674              $top = $right = $bottom = $left = array_shift($bits);
4675          }
4676          if (count($bits) > 0) {
4677              $right = $left = array_shift($bits);
4678          }
4679          if (count($bits) > 0) {
4680              $bottom = array_shift($bits);
4681          }
4682          if (count($bits) > 0) {
4683              $left = array_shift($bits);
4684          }
4685          return array(
4686              new css_style_paddingtop('padding-top', $top.$important),
4687              new css_style_paddingright('padding-right', $right.$important),
4688              new css_style_paddingbottom('padding-bottom', $bottom.$important),
4689              new css_style_paddingleft('padding-left', $left.$important)
4690          );
4691      }
4692  
4693      /**
4694       * Consolidates several padding styles into a single style.
4695       *
4696       * @param css_style_padding[] $styles Array of padding styles
4697       * @return css_style[] Optimised+consolidated array of padding styles
4698       */
4699      public static function consolidate(array $styles) {
4700          if (count($styles) != 4) {
4701              return $styles;
4702          }
4703  
4704          $someimportant = false;
4705          $allimportant = null;
4706          $notimportantequal = null;
4707          $firstvalue = null;
4708          foreach ($styles as $style) {
4709              if ($style->is_important()) {
4710                  $someimportant = true;
4711                  if ($allimportant === null) {
4712                      $allimportant = true;
4713                  }
4714              } else {
4715                  if ($allimportant === true) {
4716                      $allimportant = false;
4717                  }
4718                  if ($firstvalue == null) {
4719                      $firstvalue = $style->get_value(false);
4720                      $notimportantequal = true;
4721                  } else if ($notimportantequal && $firstvalue !== $style->get_value(false)) {
4722                      $notimportantequal = false;
4723                  }
4724              }
4725          }
4726  
4727          if ($someimportant && !$allimportant && !$notimportantequal) {
4728              return $styles;
4729          }
4730  
4731          if ($someimportant && !$allimportant && $notimportantequal) {
4732              $return = array(
4733                  new css_style_padding('padding', $firstvalue)
4734              );
4735              foreach ($styles as $style) {
4736                  if ($style->is_important()) {
4737                      $return[] = $style;
4738                  }
4739              }
4740              return $return;
4741          } else {
4742              $top = null;
4743              $right = null;
4744              $bottom = null;
4745              $left = null;
4746              foreach ($styles as $style) {
4747                  switch ($style->get_name()) {
4748                      case 'padding-top' :
4749                          $top = $style->get_value(false);
4750                          break;
4751                      case 'padding-right' :
4752                          $right = $style->get_value(false);
4753                          break;
4754                      case 'padding-bottom' :
4755                          $bottom = $style->get_value(false);
4756                          break;
4757                      case 'padding-left' :
4758                          $left = $style->get_value(false);
4759                          break;
4760                  }
4761              }
4762              if ($top == $bottom && $left == $right) {
4763                  if ($top == $left) {
4764                      $returnstyle = new css_style_padding('padding', $top);
4765                  } else {
4766                      $returnstyle = new css_style_padding('padding', "{$top} {$left}");
4767                  }
4768              } else if ($left == $right) {
4769                  $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom}");
4770              } else {
4771                  $returnstyle = new css_style_padding('padding', "{$top} {$right} {$bottom} {$left}");
4772              }
4773              if ($allimportant) {
4774                  $returnstyle->set_important();
4775              }
4776              return array($returnstyle);
4777          }
4778      }
4779  }
4780  
4781  /**
4782   * A padding top style.
4783   *
4784   * @package core
4785   * @subpackage cssoptimiser
4786   * @copyright 2012 Sam Hemelryk
4787   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4788   */
4789  class css_style_paddingtop extends css_style_padding {
4790  
4791      /**
4792       * Initialises this style
4793       *
4794       * @param string $value The value of the style
4795       * @return css_style_paddingtop
4796       */
4797      public static function init($value) {
4798          return new css_style_paddingtop('padding-top', $value);
4799      }
4800  
4801      /**
4802       * Consolidates this style into a single padding style
4803       *
4804       * @return string
4805       */
4806      public function consolidate_to() {
4807          return 'padding';
4808      }
4809  }
4810  
4811  /**
4812   * A padding right style.
4813   *
4814   * @package core
4815   * @subpackage cssoptimiser
4816   * @copyright 2012 Sam Hemelryk
4817   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4818   */
4819  class css_style_paddingright extends css_style_padding {
4820  
4821      /**
4822       * Initialises this style
4823       *
4824       * @param string $value The value of the style
4825       * @return css_style_paddingright
4826       */
4827      public static function init($value) {
4828          return new css_style_paddingright('padding-right', $value);
4829      }
4830  
4831      /**
4832       * Consolidates this style into a single padding style
4833       *
4834       * @return string
4835       */
4836      public function consolidate_to() {
4837          return 'padding';
4838      }
4839  }
4840  
4841  /**
4842   * A padding bottom style.
4843   *
4844   * @package core
4845   * @subpackage cssoptimiser
4846   * @copyright 2012 Sam Hemelryk
4847   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4848   */
4849  class css_style_paddingbottom extends css_style_padding {
4850  
4851      /**
4852       * Initialises this style
4853       *
4854       * @param string $value The value of the style
4855       * @return css_style_paddingbottom
4856       */
4857      public static function init($value) {
4858          return new css_style_paddingbottom('padding-bottom', $value);
4859      }
4860  
4861      /**
4862       * Consolidates this style into a single padding style
4863       *
4864       * @return string
4865       */
4866      public function consolidate_to() {
4867          return 'padding';
4868      }
4869  }
4870  
4871  /**
4872   * A padding left style.
4873   *
4874   * @package core
4875   * @subpackage cssoptimiser
4876   * @copyright 2012 Sam Hemelryk
4877   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4878   */
4879  class css_style_paddingleft extends css_style_padding {
4880  
4881      /**
4882       * Initialises this style
4883       *
4884       * @param string $value The value of the style
4885       * @return css_style_paddingleft
4886       */
4887      public static function init($value) {
4888          return new css_style_paddingleft('padding-left', $value);
4889      }
4890  
4891      /**
4892       * Consolidates this style into a single padding style
4893       *
4894       * @return string
4895       */
4896      public function consolidate_to() {
4897          return 'padding';
4898      }
4899  }
4900  
4901  /**
4902   * A cursor style.
4903   *
4904   * @package core
4905   * @subpackage cssoptimiser
4906   * @copyright 2012 Sam Hemelryk
4907   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4908   */
4909  class css_style_cursor extends css_style_generic {
4910      /**
4911       * Initialises a new cursor style
4912       * @param string $value
4913       * @return css_style_cursor
4914       */
4915      public static function init($value) {
4916          return new css_style_cursor('cursor', $value);
4917      }
4918      /**
4919       * Cleans the given value and returns it.
4920       *
4921       * @param string $value
4922       * @return string
4923       */
4924      protected function clean_value($value) {
4925          // Allowed values for the cursor style.
4926          $allowed = array('auto', 'crosshair', 'default', 'e-resize', 'help', 'move', 'n-resize', 'ne-resize', 'nw-resize',
4927                           'pointer', 'progress', 's-resize', 'se-resize', 'sw-resize', 'text', 'w-resize', 'wait', 'inherit');
4928          // Has to be one of the allowed values of an image to use. Loosely match the image... doesn't need to be thorough.
4929          if (!in_array($value, $allowed) && !preg_match('#\.[a-zA-Z0-9_\-]{1,5}$#', $value)) {
4930              $this->set_error('Invalid or unexpected cursor value specified: '.$value);
4931          }
4932          return trim($value);
4933      }
4934  }
4935  
4936  /**
4937   * A vertical alignment style.
4938   *
4939   * @package core
4940   * @subpackage cssoptimiser
4941   * @copyright 2012 Sam Hemelryk
4942   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4943   */
4944  class css_style_verticalalign extends css_style_generic {
4945      /**
4946       * Initialises a new vertical alignment style
4947       * @param string $value
4948       * @return css_style_verticalalign
4949       */
4950      public static function init($value) {
4951          return new css_style_verticalalign('vertical-align', $value);
4952      }
4953      /**
4954       * Cleans the given value and returns it.
4955       *
4956       * @param string $value
4957       * @return string
4958       */
4959      protected function clean_value($value) {
4960          $allowed = array('baseline', 'sub', 'super', 'top', 'text-top', 'middle', 'bottom', 'text-bottom', 'inherit');
4961          if (!css_is_width($value) && !in_array($value, $allowed)) {
4962              $this->set_error('Invalid vertical-align value specified: '.$value);
4963          }
4964          return trim($value);
4965      }
4966  }
4967  
4968  /**
4969   * A float style.
4970   *
4971   * @package core
4972   * @subpackage cssoptimiser
4973   * @copyright 2012 Sam Hemelryk
4974   * @license http://www.gnu.org/copyleft/gpl.html GNU GPL v3 or later
4975   */
4976  class css_style_float extends css_style_generic {
4977      /**
4978       * Initialises a new float style
4979       * @param string $value
4980       * @return css_style_float
4981       */
4982      public static function init($value) {
4983          return new css_style_float('float', $value);
4984      }
4985      /**
4986       * Cleans the given value and returns it.
4987       *
4988       * @param string $value
4989       * @return string
4990       */
4991      protected function clean_value($value) {
4992          $allowed = array('left', 'right', 'none', 'inherit');
4993          if (!css_is_width($value) && !in_array($value, $allowed)) {
4994              $this->set_error('Invalid float value specified: '.$value);
4995          }
4996          return trim($value);
4997      }
4998  }


Generated: Thu Aug 11 10:00:09 2016 Cross-referenced by PHPXref 0.7.1