[ Index ]

PHP Cross Reference of DokuWiki

title

Body

[close]

/inc/ -> lessc.inc.php (source)

   1  <?php
   2  
   3  /**
   4   * lessphp v0.4.0
   5   * http://leafo.net/lessphp
   6   *
   7   * LESS css compiler, adapted from http://lesscss.org
   8   *
   9   * Copyright 2012, Leaf Corcoran <leafot@gmail.com>
  10   * Licensed under MIT or GPLv3, see LICENSE
  11   */
  12  
  13  
  14  /**
  15   * The less compiler and parser.
  16   *
  17   * Converting LESS to CSS is a three stage process. The incoming file is parsed
  18   * by `lessc_parser` into a syntax tree, then it is compiled into another tree
  19   * representing the CSS structure by `lessc`. The CSS tree is fed into a
  20   * formatter, like `lessc_formatter` which then outputs CSS as a string.
  21   *
  22   * During the first compile, all values are *reduced*, which means that their
  23   * types are brought to the lowest form before being dump as strings. This
  24   * handles math equations, variable dereferences, and the like.
  25   *
  26   * The `parse` function of `lessc` is the entry point.
  27   *
  28   * In summary:
  29   *
  30   * The `lessc` class creates an intstance of the parser, feeds it LESS code,
  31   * then transforms the resulting tree to a CSS tree. This class also holds the
  32   * evaluation context, such as all available mixins and variables at any given
  33   * time.
  34   *
  35   * The `lessc_parser` class is only concerned with parsing its input.
  36   *
  37   * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
  38   * handling things like indentation.
  39   */
  40  class lessc {
  41      static public $VERSION = "v0.4.0";
  42      static protected $TRUE = array("keyword", "true");
  43      static protected $FALSE = array("keyword", "false");
  44  
  45      protected $libFunctions = array();
  46      protected $registeredVars = array();
  47      protected $preserveComments = false;
  48  
  49      public $vPrefix = '@'; // prefix of abstract properties
  50      public $mPrefix = '$'; // prefix of abstract blocks
  51      public $parentSelector = '&';
  52  
  53      public $importDisabled = false;
  54      /** @var  string|string[] */
  55      public $importDir;
  56  
  57      protected $numberPrecision = null;
  58  
  59      protected $allParsedFiles = array();
  60  
  61      // set to the parser that generated the current line when compiling
  62      // so we know how to create error messages
  63      protected $sourceParser = null;
  64      protected $sourceLoc = null;
  65  
  66      static public $defaultValue = array("keyword", "");
  67  
  68      static protected $nextImportId = 0; // uniquely identify imports
  69  
  70      // attempts to find the path of an import url, returns null for css files
  71  	protected function findImport($url) {
  72          foreach ((array)$this->importDir as $dir) {
  73              $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
  74              if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
  75                  return $file;
  76              }
  77          }
  78  
  79          return null;
  80      }
  81  
  82  	protected function fileExists($name) {
  83          return is_file($name);
  84      }
  85  
  86  	static public function compressList($items, $delim) {
  87          if (!isset($items[1]) && isset($items[0])) return $items[0];
  88          else return array('list', $delim, $items);
  89      }
  90  
  91  	static public function preg_quote($what) {
  92          return preg_quote($what, '/');
  93      }
  94  
  95  	protected function tryImport($importPath, $parentBlock, $out) {
  96          if ($importPath[0] == "function" && $importPath[1] == "url") {
  97              $importPath = $this->flattenList($importPath[2]);
  98          }
  99  
 100          $str = $this->coerceString($importPath);
 101          if ($str === null) return false;
 102  
 103          $url = $this->compileValue($this->lib_e($str));
 104  
 105          // don't import if it ends in css
 106          if (substr_compare($url, '.css', -4, 4) === 0) return false;
 107  
 108          $realPath = $this->findImport($url);
 109  
 110          if ($realPath === null) return false;
 111  
 112          if ($this->importDisabled) {
 113              return array(false, "/* import disabled */");
 114          }
 115  
 116          if (isset($this->allParsedFiles[realpath($realPath)])) {
 117              return array(false, null);
 118          }
 119  
 120          $this->addParsedFile($realPath);
 121          $parser = $this->makeParser($realPath);
 122          $root = $parser->parse(file_get_contents($realPath));
 123  
 124          // set the parents of all the block props
 125          foreach ($root->props as $prop) {
 126              if ($prop[0] == "block") {
 127                  $prop[1]->parent = $parentBlock;
 128              }
 129          }
 130  
 131          // copy mixins into scope, set their parents
 132          // bring blocks from import into current block
 133          // TODO: need to mark the source parser    these came from this file
 134          foreach ($root->children as $childName => $child) {
 135              if (isset($parentBlock->children[$childName])) {
 136                  $parentBlock->children[$childName] = array_merge(
 137                      $parentBlock->children[$childName],
 138                      $child);
 139              } else {
 140                  $parentBlock->children[$childName] = $child;
 141              }
 142          }
 143  
 144          $pi = pathinfo($realPath);
 145          $dir = $pi["dirname"];
 146  
 147          list($top, $bottom) = $this->sortProps($root->props, true);
 148          $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
 149  
 150          return array(true, $bottom, $parser, $dir);
 151      }
 152  
 153  	protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
 154          $oldSourceParser = $this->sourceParser;
 155  
 156          $oldImport = $this->importDir;
 157  
 158          // TODO: this is because the importDir api is stupid
 159          $this->importDir = (array)$this->importDir;
 160          array_unshift($this->importDir, $importDir);
 161  
 162          foreach ($props as $prop) {
 163              $this->compileProp($prop, $block, $out);
 164          }
 165  
 166          $this->importDir = $oldImport;
 167          $this->sourceParser = $oldSourceParser;
 168      }
 169  
 170      /**
 171       * Recursively compiles a block.
 172       *
 173       * A block is analogous to a CSS block in most cases. A single LESS document
 174       * is encapsulated in a block when parsed, but it does not have parent tags
 175       * so all of it's children appear on the root level when compiled.
 176       *
 177       * Blocks are made up of props and children.
 178       *
 179       * Props are property instructions, array tuples which describe an action
 180       * to be taken, eg. write a property, set a variable, mixin a block.
 181       *
 182       * The children of a block are just all the blocks that are defined within.
 183       * This is used to look up mixins when performing a mixin.
 184       *
 185       * Compiling the block involves pushing a fresh environment on the stack,
 186       * and iterating through the props, compiling each one.
 187       *
 188       * See lessc::compileProp()
 189       *
 190       * @param stdClass $block
 191       */
 192  	protected function compileBlock($block) {
 193          switch ($block->type) {
 194          case "root":
 195              $this->compileRoot($block);
 196              break;
 197          case null:
 198              $this->compileCSSBlock($block);
 199              break;
 200          case "media":
 201              $this->compileMedia($block);
 202              break;
 203          case "directive":
 204              $name = "@" . $block->name;
 205              if (!empty($block->value)) {
 206                  $name .= " " . $this->compileValue($this->reduce($block->value));
 207              }
 208  
 209              $this->compileNestedBlock($block, array($name));
 210              break;
 211          default:
 212              $this->throwError("unknown block type: $block->type\n");
 213          }
 214      }
 215  
 216  	protected function compileCSSBlock($block) {
 217          $env = $this->pushEnv();
 218  
 219          $selectors = $this->compileSelectors($block->tags);
 220          $env->selectors = $this->multiplySelectors($selectors);
 221          $out = $this->makeOutputBlock(null, $env->selectors);
 222  
 223          $this->scope->children[] = $out;
 224          $this->compileProps($block, $out);
 225  
 226          $block->scope = $env; // mixins carry scope with them!
 227          $this->popEnv();
 228      }
 229  
 230  	protected function compileMedia($media) {
 231          $env = $this->pushEnv($media);
 232          $parentScope = $this->mediaParent($this->scope);
 233  
 234          $query = $this->compileMediaQuery($this->multiplyMedia($env));
 235  
 236          $this->scope = $this->makeOutputBlock($media->type, array($query));
 237          $parentScope->children[] = $this->scope;
 238  
 239          $this->compileProps($media, $this->scope);
 240  
 241          if (count($this->scope->lines) > 0) {
 242              $orphanSelelectors = $this->findClosestSelectors();
 243              if (!is_null($orphanSelelectors)) {
 244                  $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
 245                  $orphan->lines = $this->scope->lines;
 246                  array_unshift($this->scope->children, $orphan);
 247                  $this->scope->lines = array();
 248              }
 249          }
 250  
 251          $this->scope = $this->scope->parent;
 252          $this->popEnv();
 253      }
 254  
 255  	protected function mediaParent($scope) {
 256          while (!empty($scope->parent)) {
 257              if (!empty($scope->type) && $scope->type != "media") {
 258                  break;
 259              }
 260              $scope = $scope->parent;
 261          }
 262  
 263          return $scope;
 264      }
 265  
 266  	protected function compileNestedBlock($block, $selectors) {
 267          $this->pushEnv($block);
 268          $this->scope = $this->makeOutputBlock($block->type, $selectors);
 269          $this->scope->parent->children[] = $this->scope;
 270  
 271          $this->compileProps($block, $this->scope);
 272  
 273          $this->scope = $this->scope->parent;
 274          $this->popEnv();
 275      }
 276  
 277  	protected function compileRoot($root) {
 278          $this->pushEnv();
 279          $this->scope = $this->makeOutputBlock($root->type);
 280          $this->compileProps($root, $this->scope);
 281          $this->popEnv();
 282      }
 283  
 284  	protected function compileProps($block, $out) {
 285          foreach ($this->sortProps($block->props) as $prop) {
 286              $this->compileProp($prop, $block, $out);
 287          }
 288  
 289          $out->lines = array_values(array_unique($out->lines));
 290      }
 291  
 292  	protected function sortProps($props, $split = false) {
 293          $vars = array();
 294          $imports = array();
 295          $other = array();
 296  
 297          foreach ($props as $prop) {
 298              switch ($prop[0]) {
 299              case "assign":
 300                  if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
 301                      $vars[] = $prop;
 302                  } else {
 303                      $other[] = $prop;
 304                  }
 305                  break;
 306              case "import":
 307                  $id = self::$nextImportId++;
 308                  $prop[] = $id;
 309                  $imports[] = $prop;
 310                  $other[] = array("import_mixin", $id);
 311                  break;
 312              default:
 313                  $other[] = $prop;
 314              }
 315          }
 316  
 317          if ($split) {
 318              return array(array_merge($vars, $imports), $other);
 319          } else {
 320              return array_merge($vars, $imports, $other);
 321          }
 322      }
 323  
 324  	protected function compileMediaQuery($queries) {
 325          $compiledQueries = array();
 326          foreach ($queries as $query) {
 327              $parts = array();
 328              foreach ($query as $q) {
 329                  switch ($q[0]) {
 330                  case "mediaType":
 331                      $parts[] = implode(" ", array_slice($q, 1));
 332                      break;
 333                  case "mediaExp":
 334                      if (isset($q[2])) {
 335                          $parts[] = "($q[1]: " .
 336                              $this->compileValue($this->reduce($q[2])) . ")";
 337                      } else {
 338                          $parts[] = "($q[1])";
 339                      }
 340                      break;
 341                  case "variable":
 342                      $parts[] = $this->compileValue($this->reduce($q));
 343                  break;
 344                  }
 345              }
 346  
 347              if (count($parts) > 0) {
 348                  $compiledQueries[] =  implode(" and ", $parts);
 349              }
 350          }
 351  
 352          $out = "@media";
 353          if (!empty($parts)) {
 354              $out .= " " .
 355                  implode($this->formatter->selectorSeparator, $compiledQueries);
 356          }
 357          return $out;
 358      }
 359  
 360  	protected function multiplyMedia($env, $childQueries = null) {
 361          if (is_null($env) ||
 362              !empty($env->block->type) && $env->block->type != "media")
 363          {
 364              return $childQueries;
 365          }
 366  
 367          // plain old block, skip
 368          if (empty($env->block->type)) {
 369              return $this->multiplyMedia($env->parent, $childQueries);
 370          }
 371  
 372          $out = array();
 373          $queries = $env->block->queries;
 374          if (is_null($childQueries)) {
 375              $out = $queries;
 376          } else {
 377              foreach ($queries as $parent) {
 378                  foreach ($childQueries as $child) {
 379                      $out[] = array_merge($parent, $child);
 380                  }
 381              }
 382          }
 383  
 384          return $this->multiplyMedia($env->parent, $out);
 385      }
 386  
 387  	protected function expandParentSelectors(&$tag, $replace) {
 388          $parts = explode("$&$", $tag);
 389          $count = 0;
 390          foreach ($parts as &$part) {
 391              $part = str_replace($this->parentSelector, $replace, $part, $c);
 392              $count += $c;
 393          }
 394          $tag = implode($this->parentSelector, $parts);
 395          return $count;
 396      }
 397  
 398  	protected function findClosestSelectors() {
 399          $env = $this->env;
 400          $selectors = null;
 401          while ($env !== null) {
 402              if (isset($env->selectors)) {
 403                  $selectors = $env->selectors;
 404                  break;
 405              }
 406              $env = $env->parent;
 407          }
 408  
 409          return $selectors;
 410      }
 411  
 412  
 413      // multiply $selectors against the nearest selectors in env
 414  	protected function multiplySelectors($selectors) {
 415          // find parent selectors
 416  
 417          $parentSelectors = $this->findClosestSelectors();
 418          if (is_null($parentSelectors)) {
 419              // kill parent reference in top level selector
 420              foreach ($selectors as &$s) {
 421                  $this->expandParentSelectors($s, "");
 422              }
 423  
 424              return $selectors;
 425          }
 426  
 427          $out = array();
 428          foreach ($parentSelectors as $parent) {
 429              foreach ($selectors as $child) {
 430                  $count = $this->expandParentSelectors($child, $parent);
 431  
 432                  // don't prepend the parent tag if & was used
 433                  if ($count > 0) {
 434                      $out[] = trim($child);
 435                  } else {
 436                      $out[] = trim($parent . ' ' . $child);
 437                  }
 438              }
 439          }
 440  
 441          return $out;
 442      }
 443  
 444      // reduces selector expressions
 445  	protected function compileSelectors($selectors) {
 446          $out = array();
 447  
 448          foreach ($selectors as $s) {
 449              if (is_array($s)) {
 450                  list(, $value) = $s;
 451                  $out[] = trim($this->compileValue($this->reduce($value)));
 452              } else {
 453                  $out[] = $s;
 454              }
 455          }
 456  
 457          return $out;
 458      }
 459  
 460      protected function eq($left, $right) {
 461          return $left == $right;
 462      }
 463  
 464  	protected function patternMatch($block, $orderedArgs, $keywordArgs) {
 465          // match the guards if it has them
 466          // any one of the groups must have all its guards pass for a match
 467          if (!empty($block->guards)) {
 468              $groupPassed = false;
 469              foreach ($block->guards as $guardGroup) {
 470                  foreach ($guardGroup as $guard) {
 471                      $this->pushEnv();
 472                      $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
 473  
 474                      $negate = false;
 475                      if ($guard[0] == "negate") {
 476                          $guard = $guard[1];
 477                          $negate = true;
 478                      }
 479  
 480                      $passed = $this->reduce($guard) == self::$TRUE;
 481                      if ($negate) $passed = !$passed;
 482  
 483                      $this->popEnv();
 484  
 485                      if ($passed) {
 486                          $groupPassed = true;
 487                      } else {
 488                          $groupPassed = false;
 489                          break;
 490                      }
 491                  }
 492  
 493                  if ($groupPassed) break;
 494              }
 495  
 496              if (!$groupPassed) {
 497                  return false;
 498              }
 499          }
 500  
 501          if (empty($block->args)) {
 502              return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
 503          }
 504  
 505          $remainingArgs = $block->args;
 506          if ($keywordArgs) {
 507              $remainingArgs = array();
 508              foreach ($block->args as $arg) {
 509                  if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
 510                      continue;
 511                  }
 512  
 513                  $remainingArgs[] = $arg;
 514              }
 515          }
 516  
 517          $i = -1; // no args
 518          // try to match by arity or by argument literal
 519          foreach ($remainingArgs as $i => $arg) {
 520              switch ($arg[0]) {
 521              case "lit":
 522                  if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
 523                      return false;
 524                  }
 525                  break;
 526              case "arg":
 527                  // no arg and no default value
 528                  if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
 529                      return false;
 530                  }
 531                  break;
 532              case "rest":
 533                  $i--; // rest can be empty
 534                  break 2;
 535              }
 536          }
 537  
 538          if ($block->isVararg) {
 539              return true; // not having enough is handled above
 540          } else {
 541              $numMatched = $i + 1;
 542              // greater than becuase default values always match
 543              return $numMatched >= count($orderedArgs);
 544          }
 545      }
 546  
 547  	protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
 548          $matches = null;
 549          foreach ($blocks as $block) {
 550              // skip seen blocks that don't have arguments
 551              if (isset($skip[$block->id]) && !isset($block->args)) {
 552                  continue;
 553              }
 554  
 555              if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
 556                  $matches[] = $block;
 557              }
 558          }
 559  
 560          return $matches;
 561      }
 562  
 563      // attempt to find blocks matched by path and args
 564  	protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
 565          if ($searchIn == null) return null;
 566          if (isset($seen[$searchIn->id])) return null;
 567          $seen[$searchIn->id] = true;
 568  
 569          $name = $path[0];
 570  
 571          if (isset($searchIn->children[$name])) {
 572              $blocks = $searchIn->children[$name];
 573              if (count($path) == 1) {
 574                  $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
 575                  if (!empty($matches)) {
 576                      // This will return all blocks that match in the closest
 577                      // scope that has any matching block, like lessjs
 578                      return $matches;
 579                  }
 580              } else {
 581                  $matches = array();
 582                  foreach ($blocks as $subBlock) {
 583                      $subMatches = $this->findBlocks($subBlock,
 584                          array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
 585  
 586                      if (!is_null($subMatches)) {
 587                          foreach ($subMatches as $sm) {
 588                              $matches[] = $sm;
 589                          }
 590                      }
 591                  }
 592  
 593                  return count($matches) > 0 ? $matches : null;
 594              }
 595          }
 596          if ($searchIn->parent === $searchIn) return null;
 597          return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
 598      }
 599  
 600      // sets all argument names in $args to either the default value
 601      // or the one passed in through $values
 602  	protected function zipSetArgs($args, $orderedValues, $keywordValues) {
 603          $assignedValues = array();
 604  
 605          $i = 0;
 606          foreach ($args as  $a) {
 607              if ($a[0] == "arg") {
 608                  if (isset($keywordValues[$a[1]])) {
 609                      // has keyword arg
 610                      $value = $keywordValues[$a[1]];
 611                  } elseif (isset($orderedValues[$i])) {
 612                      // has ordered arg
 613                      $value = $orderedValues[$i];
 614                      $i++;
 615                  } elseif (isset($a[2])) {
 616                      // has default value
 617                      $value = $a[2];
 618                  } else {
 619                      $this->throwError("Failed to assign arg " . $a[1]);
 620                      $value = null; // :(
 621                  }
 622  
 623                  $value = $this->reduce($value);
 624                  $this->set($a[1], $value);
 625                  $assignedValues[] = $value;
 626              } else {
 627                  // a lit
 628                  $i++;
 629              }
 630          }
 631  
 632          // check for a rest
 633          $last = end($args);
 634          if ($last[0] == "rest") {
 635              $rest = array_slice($orderedValues, count($args) - 1);
 636              $this->set($last[1], $this->reduce(array("list", " ", $rest)));
 637          }
 638  
 639          // wow is this the only true use of PHP's + operator for arrays?
 640          $this->env->arguments = $assignedValues + $orderedValues;
 641      }
 642  
 643      // compile a prop and update $lines or $blocks appropriately
 644  	protected function compileProp($prop, $block, $out) {
 645          // set error position context
 646          $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
 647  
 648          switch ($prop[0]) {
 649          case 'assign':
 650              list(, $name, $value) = $prop;
 651              if ($name[0] == $this->vPrefix) {
 652                  $this->set($name, $value);
 653              } else {
 654                  $out->lines[] = $this->formatter->property($name,
 655                          $this->compileValue($this->reduce($value)));
 656              }
 657              break;
 658          case 'block':
 659              list(, $child) = $prop;
 660              $this->compileBlock($child);
 661              break;
 662          case 'mixin':
 663              list(, $path, $args, $suffix) = $prop;
 664  
 665              $orderedArgs = array();
 666              $keywordArgs = array();
 667              foreach ((array)$args as $arg) {
 668                  $argval = null;
 669                  switch ($arg[0]) {
 670                  case "arg":
 671                      if (!isset($arg[2])) {
 672                          $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
 673                      } else {
 674                          $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
 675                      }
 676                      break;
 677  
 678                  case "lit":
 679                      $orderedArgs[] = $this->reduce($arg[1]);
 680                      break;
 681                  default:
 682                      $this->throwError("Unknown arg type: " . $arg[0]);
 683                  }
 684              }
 685  
 686              $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
 687  
 688              if ($mixins === null) {
 689                  // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n");
 690                  break; // throw error here??
 691              }
 692  
 693              foreach ($mixins as $mixin) {
 694                  if ($mixin === $block && !$orderedArgs) {
 695                      continue;
 696                  }
 697  
 698                  $haveScope = false;
 699                  if (isset($mixin->parent->scope)) {
 700                      $haveScope = true;
 701                      $mixinParentEnv = $this->pushEnv();
 702                      $mixinParentEnv->storeParent = $mixin->parent->scope;
 703                  }
 704  
 705                  $haveArgs = false;
 706                  if (isset($mixin->args)) {
 707                      $haveArgs = true;
 708                      $this->pushEnv();
 709                      $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
 710                  }
 711  
 712                  $oldParent = $mixin->parent;
 713                  if ($mixin !== $block) $mixin->parent = $block;
 714  
 715                  foreach ($this->sortProps($mixin->props) as $subProp) {
 716                      if ($suffix !== null &&
 717                          $subProp[0] == "assign" &&
 718                          is_string($subProp[1]) &&
 719                          $subProp[1]{0} != $this->vPrefix)
 720                      {
 721                          $subProp[2] = array(
 722                              'list', ' ',
 723                              array($subProp[2], array('keyword', $suffix))
 724                          );
 725                      }
 726  
 727                      $this->compileProp($subProp, $mixin, $out);
 728                  }
 729  
 730                  $mixin->parent = $oldParent;
 731  
 732                  if ($haveArgs) $this->popEnv();
 733                  if ($haveScope) $this->popEnv();
 734              }
 735  
 736              break;
 737          case 'raw':
 738              $out->lines[] = $prop[1];
 739              break;
 740          case "directive":
 741              list(, $name, $value) = $prop;
 742              $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
 743              break;
 744          case "comment":
 745              $out->lines[] = $prop[1];
 746              break;
 747          case "import";
 748              list(, $importPath, $importId) = $prop;
 749              $importPath = $this->reduce($importPath);
 750  
 751              if (!isset($this->env->imports)) {
 752                  $this->env->imports = array();
 753              }
 754  
 755              $result = $this->tryImport($importPath, $block, $out);
 756  
 757              $this->env->imports[$importId] = $result === false ?
 758                  array(false, "@import " . $this->compileValue($importPath).";") :
 759                  $result;
 760  
 761              break;
 762          case "import_mixin":
 763              list(,$importId) = $prop;
 764              $import = $this->env->imports[$importId];
 765              if ($import[0] === false) {
 766                  if (isset($import[1])) {
 767                      $out->lines[] = $import[1];
 768                  }
 769              } else {
 770                  list(, $bottom, $parser, $importDir) = $import;
 771                  $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
 772              }
 773  
 774              break;
 775          default:
 776              $this->throwError("unknown op: {$prop[0]}\n");
 777          }
 778      }
 779  
 780  
 781      /**
 782       * Compiles a primitive value into a CSS property value.
 783       *
 784       * Values in lessphp are typed by being wrapped in arrays, their format is
 785       * typically:
 786       *
 787       *     array(type, contents [, additional_contents]*)
 788       *
 789       * The input is expected to be reduced. This function will not work on
 790       * things like expressions and variables.
 791       *
 792       * @param array $value
 793       *
 794       * @return string
 795       */
 796  	protected function compileValue($value) {
 797          switch ($value[0]) {
 798          case 'list':
 799              // [1] - delimiter
 800              // [2] - array of values
 801              return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
 802          case 'raw_color':
 803              if (!empty($this->formatter->compressColors)) {
 804                  return $this->compileValue($this->coerceColor($value));
 805              }
 806              return $value[1];
 807          case 'keyword':
 808              // [1] - the keyword
 809              return $value[1];
 810          case 'number':
 811              list(, $num, $unit) = $value;
 812              // [1] - the number
 813              // [2] - the unit
 814              if ($this->numberPrecision !== null) {
 815                  $num = round($num, $this->numberPrecision);
 816              }
 817              return $num . $unit;
 818          case 'string':
 819              // [1] - contents of string (includes quotes)
 820              list(, $delim, $content) = $value;
 821              foreach ($content as &$part) {
 822                  if (is_array($part)) {
 823                      $part = $this->compileValue($part);
 824                  }
 825              }
 826              return $delim . implode($content) . $delim;
 827          case 'color':
 828              // [1] - red component (either number or a %)
 829              // [2] - green component
 830              // [3] - blue component
 831              // [4] - optional alpha component
 832              list(, $r, $g, $b) = $value;
 833              $r = round($r);
 834              $g = round($g);
 835              $b = round($b);
 836  
 837              if (count($value) == 5 && $value[4] != 1) { // rgba
 838                  return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
 839              }
 840  
 841              $h = sprintf("#%02x%02x%02x", $r, $g, $b);
 842  
 843              if (!empty($this->formatter->compressColors)) {
 844                  // Converting hex color to short notation (e.g. #003399 to #039)
 845                  if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
 846                      $h = '#' . $h[1] . $h[3] . $h[5];
 847                  }
 848              }
 849  
 850              return $h;
 851  
 852          case 'function':
 853              list(, $name, $args) = $value;
 854              return $name.'('.$this->compileValue($args).')';
 855          default: // assumed to be unit
 856              $this->throwError("unknown value type: $value[0]");
 857          }
 858      }
 859  
 860  	protected function lib_pow($args) {
 861          list($base, $exp) = $this->assertArgs($args, 2, "pow");
 862          return pow($this->assertNumber($base), $this->assertNumber($exp));
 863      }
 864  
 865  	protected function lib_pi() {
 866          return pi();
 867      }
 868  
 869  	protected function lib_mod($args) {
 870          list($a, $b) = $this->assertArgs($args, 2, "mod");
 871          return $this->assertNumber($a) % $this->assertNumber($b);
 872      }
 873  
 874  	protected function lib_tan($num) {
 875          return tan($this->assertNumber($num));
 876      }
 877  
 878  	protected function lib_sin($num) {
 879          return sin($this->assertNumber($num));
 880      }
 881  
 882  	protected function lib_cos($num) {
 883          return cos($this->assertNumber($num));
 884      }
 885  
 886  	protected function lib_atan($num) {
 887          $num = atan($this->assertNumber($num));
 888          return array("number", $num, "rad");
 889      }
 890  
 891  	protected function lib_asin($num) {
 892          $num = asin($this->assertNumber($num));
 893          return array("number", $num, "rad");
 894      }
 895  
 896  	protected function lib_acos($num) {
 897          $num = acos($this->assertNumber($num));
 898          return array("number", $num, "rad");
 899      }
 900  
 901  	protected function lib_sqrt($num) {
 902          return sqrt($this->assertNumber($num));
 903      }
 904  
 905  	protected function lib_extract($value) {
 906          list($list, $idx) = $this->assertArgs($value, 2, "extract");
 907          $idx = $this->assertNumber($idx);
 908          // 1 indexed
 909          if ($list[0] == "list" && isset($list[2][$idx - 1])) {
 910              return $list[2][$idx - 1];
 911          }
 912      }
 913  
 914  	protected function lib_isnumber($value) {
 915          return $this->toBool($value[0] == "number");
 916      }
 917  
 918  	protected function lib_isstring($value) {
 919          return $this->toBool($value[0] == "string");
 920      }
 921  
 922  	protected function lib_iscolor($value) {
 923          return $this->toBool($this->coerceColor($value));
 924      }
 925  
 926  	protected function lib_iskeyword($value) {
 927          return $this->toBool($value[0] == "keyword");
 928      }
 929  
 930  	protected function lib_ispixel($value) {
 931          return $this->toBool($value[0] == "number" && $value[2] == "px");
 932      }
 933  
 934  	protected function lib_ispercentage($value) {
 935          return $this->toBool($value[0] == "number" && $value[2] == "%");
 936      }
 937  
 938  	protected function lib_isem($value) {
 939          return $this->toBool($value[0] == "number" && $value[2] == "em");
 940      }
 941  
 942  	protected function lib_isrem($value) {
 943          return $this->toBool($value[0] == "number" && $value[2] == "rem");
 944      }
 945  
 946  	protected function lib_rgbahex($color) {
 947          $color = $this->coerceColor($color);
 948          if (is_null($color))
 949              $this->throwError("color expected for rgbahex");
 950  
 951          return sprintf("#%02x%02x%02x%02x",
 952              isset($color[4]) ? $color[4]*255 : 255,
 953              $color[1],$color[2], $color[3]);
 954      }
 955  
 956  	protected function lib_argb($color){
 957          return $this->lib_rgbahex($color);
 958      }
 959  
 960      // utility func to unquote a string
 961  	protected function lib_e($arg) {
 962          switch ($arg[0]) {
 963              case "list":
 964                  $items = $arg[2];
 965                  if (isset($items[0])) {
 966                      return $this->lib_e($items[0]);
 967                  }
 968                  return self::$defaultValue;
 969              case "string":
 970                  $arg[1] = "";
 971                  return $arg;
 972              case "keyword":
 973                  return $arg;
 974              default:
 975                  return array("keyword", $this->compileValue($arg));
 976          }
 977      }
 978  
 979  	protected function lib__sprintf($args) {
 980          if ($args[0] != "list") return $args;
 981          $values = $args[2];
 982          $string = array_shift($values);
 983          $template = $this->compileValue($this->lib_e($string));
 984  
 985          $i = 0;
 986          if (preg_match_all('/%[dsa]/', $template, $m)) {
 987              foreach ($m[0] as $match) {
 988                  $val = isset($values[$i]) ?
 989                      $this->reduce($values[$i]) : array('keyword', '');
 990  
 991                  // lessjs compat, renders fully expanded color, not raw color
 992                  if ($color = $this->coerceColor($val)) {
 993                      $val = $color;
 994                  }
 995  
 996                  $i++;
 997                  $rep = $this->compileValue($this->lib_e($val));
 998                  $template = preg_replace('/'.self::preg_quote($match).'/',
 999                      $rep, $template, 1);
1000              }
1001          }
1002  
1003          $d = $string[0] == "string" ? $string[1] : '"';
1004          return array("string", $d, array($template));
1005      }
1006  
1007  	protected function lib_floor($arg) {
1008          $value = $this->assertNumber($arg);
1009          return array("number", floor($value), $arg[2]);
1010      }
1011  
1012  	protected function lib_ceil($arg) {
1013          $value = $this->assertNumber($arg);
1014          return array("number", ceil($value), $arg[2]);
1015      }
1016  
1017  	protected function lib_round($arg) {
1018          $value = $this->assertNumber($arg);
1019          return array("number", round($value), $arg[2]);
1020      }
1021  
1022  	protected function lib_unit($arg) {
1023          if ($arg[0] == "list") {
1024              list($number, $newUnit) = $arg[2];
1025              return array("number", $this->assertNumber($number),
1026                  $this->compileValue($this->lib_e($newUnit)));
1027          } else {
1028              return array("number", $this->assertNumber($arg), "");
1029          }
1030      }
1031  
1032      /**
1033       * Helper function to get arguments for color manipulation functions.
1034       * takes a list that contains a color like thing and a percentage
1035       *
1036       * @param array $args
1037       *
1038       * @return array
1039       */
1040  	protected function colorArgs($args) {
1041          if ($args[0] != 'list' || count($args[2]) < 2) {
1042              return array(array('color', 0, 0, 0), 0);
1043          }
1044          list($color, $delta) = $args[2];
1045          $color = $this->assertColor($color);
1046          $delta = floatval($delta[1]);
1047  
1048          return array($color, $delta);
1049      }
1050  
1051  	protected function lib_darken($args) {
1052          list($color, $delta) = $this->colorArgs($args);
1053  
1054          $hsl = $this->toHSL($color);
1055          $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
1056          return $this->toRGB($hsl);
1057      }
1058  
1059  	protected function lib_lighten($args) {
1060          list($color, $delta) = $this->colorArgs($args);
1061  
1062          $hsl = $this->toHSL($color);
1063          $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
1064          return $this->toRGB($hsl);
1065      }
1066  
1067  	protected function lib_saturate($args) {
1068          list($color, $delta) = $this->colorArgs($args);
1069  
1070          $hsl = $this->toHSL($color);
1071          $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
1072          return $this->toRGB($hsl);
1073      }
1074  
1075  	protected function lib_desaturate($args) {
1076          list($color, $delta) = $this->colorArgs($args);
1077  
1078          $hsl = $this->toHSL($color);
1079          $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
1080          return $this->toRGB($hsl);
1081      }
1082  
1083  	protected function lib_spin($args) {
1084          list($color, $delta) = $this->colorArgs($args);
1085  
1086          $hsl = $this->toHSL($color);
1087  
1088          $hsl[1] = $hsl[1] + $delta % 360;
1089          if ($hsl[1] < 0) $hsl[1] += 360;
1090  
1091          return $this->toRGB($hsl);
1092      }
1093  
1094  	protected function lib_fadeout($args) {
1095          list($color, $delta) = $this->colorArgs($args);
1096          $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
1097          return $color;
1098      }
1099  
1100  	protected function lib_fadein($args) {
1101          list($color, $delta) = $this->colorArgs($args);
1102          $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
1103          return $color;
1104      }
1105  
1106  	protected function lib_hue($color) {
1107          $hsl = $this->toHSL($this->assertColor($color));
1108          return round($hsl[1]);
1109      }
1110  
1111  	protected function lib_saturation($color) {
1112          $hsl = $this->toHSL($this->assertColor($color));
1113          return round($hsl[2]);
1114      }
1115  
1116  	protected function lib_lightness($color) {
1117          $hsl = $this->toHSL($this->assertColor($color));
1118          return round($hsl[3]);
1119      }
1120  
1121      // get the alpha of a color
1122      // defaults to 1 for non-colors or colors without an alpha
1123  	protected function lib_alpha($value) {
1124          if (!is_null($color = $this->coerceColor($value))) {
1125              return isset($color[4]) ? $color[4] : 1;
1126          }
1127      }
1128  
1129      // set the alpha of the color
1130  	protected function lib_fade($args) {
1131          list($color, $alpha) = $this->colorArgs($args);
1132          $color[4] = $this->clamp($alpha / 100.0);
1133          return $color;
1134      }
1135  
1136  	protected function lib_percentage($arg) {
1137          $num = $this->assertNumber($arg);
1138          return array("number", $num*100, "%");
1139      }
1140  
1141      // mixes two colors by weight
1142      // mix(@color1, @color2, [@weight: 50%]);
1143      // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
1144  	protected function lib_mix($args) {
1145          if ($args[0] != "list" || count($args[2]) < 2)
1146              $this->throwError("mix expects (color1, color2, weight)");
1147  
1148          list($first, $second) = $args[2];
1149          $first = $this->assertColor($first);
1150          $second = $this->assertColor($second);
1151  
1152          $first_a = $this->lib_alpha($first);
1153          $second_a = $this->lib_alpha($second);
1154  
1155          if (isset($args[2][2])) {
1156              $weight = $args[2][2][1] / 100.0;
1157          } else {
1158              $weight = 0.5;
1159          }
1160  
1161          $w = $weight * 2 - 1;
1162          $a = $first_a - $second_a;
1163  
1164          $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
1165          $w2 = 1.0 - $w1;
1166  
1167          $new = array('color',
1168              $w1 * $first[1] + $w2 * $second[1],
1169              $w1 * $first[2] + $w2 * $second[2],
1170              $w1 * $first[3] + $w2 * $second[3],
1171          );
1172  
1173          if ($first_a != 1.0 || $second_a != 1.0) {
1174              $new[] = $first_a * $weight + $second_a * ($weight - 1);
1175          }
1176  
1177          return $this->fixColor($new);
1178      }
1179  
1180  	protected function lib_contrast($args) {
1181          if ($args[0] != 'list' || count($args[2]) < 3) {
1182              return array(array('color', 0, 0, 0), 0);
1183          }
1184  
1185          list($inputColor, $darkColor, $lightColor) = $args[2];
1186  
1187          $inputColor = $this->assertColor($inputColor);
1188          $darkColor = $this->assertColor($darkColor);
1189          $lightColor = $this->assertColor($lightColor);
1190          $hsl = $this->toHSL($inputColor);
1191  
1192          if ($hsl[3] > 50) {
1193              return $darkColor;
1194          }
1195  
1196          return $lightColor;
1197      }
1198  
1199  	protected function assertColor($value, $error = "expected color value") {
1200          $color = $this->coerceColor($value);
1201          if (is_null($color)) $this->throwError($error);
1202          return $color;
1203      }
1204  
1205  	protected function assertNumber($value, $error = "expecting number") {
1206          if ($value[0] == "number") return $value[1];
1207          $this->throwError($error);
1208      }
1209  
1210  	protected function assertArgs($value, $expectedArgs, $name="") {
1211          if ($expectedArgs == 1) {
1212              return $value;
1213          } else {
1214              if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
1215              $values = $value[2];
1216              $numValues = count($values);
1217              if ($expectedArgs != $numValues) {
1218                  if ($name) {
1219                      $name = $name . ": ";
1220                  }
1221  
1222                  $this->throwError("$name}expecting $expectedArgs arguments, got $numValues");
1223              }
1224  
1225              return $values;
1226          }
1227      }
1228  
1229  	protected function toHSL($color) {
1230          if ($color[0] == 'hsl') return $color;
1231  
1232          $r = $color[1] / 255;
1233          $g = $color[2] / 255;
1234          $b = $color[3] / 255;
1235  
1236          $min = min($r, $g, $b);
1237          $max = max($r, $g, $b);
1238  
1239          $L = ($min + $max) / 2;
1240          if ($min == $max) {
1241              $S = $H = 0;
1242          } else {
1243              if ($L < 0.5)
1244                  $S = ($max - $min)/($max + $min);
1245              else
1246                  $S = ($max - $min)/(2.0 - $max - $min);
1247  
1248              if ($r == $max) $H = ($g - $b)/($max - $min);
1249              elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
1250              elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
1251  
1252          }
1253  
1254          $out = array('hsl',
1255              ($H < 0 ? $H + 6 : $H)*60,
1256              $S*100,
1257              $L*100,
1258          );
1259  
1260          if (count($color) > 4) $out[] = $color[4]; // copy alpha
1261          return $out;
1262      }
1263  
1264  	protected function toRGB_helper($comp, $temp1, $temp2) {
1265          if ($comp < 0) $comp += 1.0;
1266          elseif ($comp > 1) $comp -= 1.0;
1267  
1268          if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
1269          if (2 * $comp < 1) return $temp2;
1270          if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
1271  
1272          return $temp1;
1273      }
1274  
1275      /**
1276       * Converts a hsl array into a color value in rgb.
1277       * Expects H to be in range of 0 to 360, S and L in 0 to 100
1278       *
1279       * @param array $color
1280       *
1281       * @return array
1282       */
1283  	protected function toRGB($color) {
1284          if ($color[0] == 'color') return $color;
1285  
1286          $H = $color[1] / 360;
1287          $S = $color[2] / 100;
1288          $L = $color[3] / 100;
1289  
1290          if ($S == 0) {
1291              $r = $g = $b = $L;
1292          } else {
1293              $temp2 = $L < 0.5 ?
1294                  $L*(1.0 + $S) :
1295                  $L + $S - $L * $S;
1296  
1297              $temp1 = 2.0 * $L - $temp2;
1298  
1299              $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
1300              $g = $this->toRGB_helper($H, $temp1, $temp2);
1301              $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
1302          }
1303  
1304          // $out = array('color', round($r*255), round($g*255), round($b*255));
1305          $out = array('color', $r*255, $g*255, $b*255);
1306          if (count($color) > 4) $out[] = $color[4]; // copy alpha
1307          return $out;
1308      }
1309  
1310  	protected function clamp($v, $max = 1, $min = 0) {
1311          return min($max, max($min, $v));
1312      }
1313  
1314      /**
1315       * Convert the rgb, rgba, hsl color literals of function type
1316       * as returned by the parser into values of color type.
1317       *
1318       * @param array $func
1319       *
1320       * @return bool|mixed
1321       */
1322  	protected function funcToColor($func) {
1323          $fname = $func[1];
1324          if ($func[2][0] != 'list') return false; // need a list of arguments
1325          $rawComponents = $func[2][2];
1326  
1327          if ($fname == 'hsl' || $fname == 'hsla') {
1328              $hsl = array('hsl');
1329              $i = 0;
1330              foreach ($rawComponents as $c) {
1331                  $val = $this->reduce($c);
1332                  $val = isset($val[1]) ? floatval($val[1]) : 0;
1333  
1334                  if ($i == 0) $clamp = 360;
1335                  elseif ($i < 3) $clamp = 100;
1336                  else $clamp = 1;
1337  
1338                  $hsl[] = $this->clamp($val, $clamp);
1339                  $i++;
1340              }
1341  
1342              while (count($hsl) < 4) $hsl[] = 0;
1343              return $this->toRGB($hsl);
1344  
1345          } elseif ($fname == 'rgb' || $fname == 'rgba') {
1346              $components = array();
1347              $i = 1;
1348              foreach    ($rawComponents as $c) {
1349                  $c = $this->reduce($c);
1350                  if ($i < 4) {
1351                      if ($c[0] == "number" && $c[2] == "%") {
1352                          $components[] = 255 * ($c[1] / 100);
1353                      } else {
1354                          $components[] = floatval($c[1]);
1355                      }
1356                  } elseif ($i == 4) {
1357                      if ($c[0] == "number" && $c[2] == "%") {
1358                          $components[] = 1.0 * ($c[1] / 100);
1359                      } else {
1360                          $components[] = floatval($c[1]);
1361                      }
1362                  } else break;
1363  
1364                  $i++;
1365              }
1366              while (count($components) < 3) $components[] = 0;
1367              array_unshift($components, 'color');
1368              return $this->fixColor($components);
1369          }
1370  
1371          return false;
1372      }
1373  
1374  	protected function reduce($value, $forExpression = false) {
1375          switch ($value[0]) {
1376          case "interpolate":
1377              $reduced = $this->reduce($value[1]);
1378              $var = $this->compileValue($reduced);
1379              $res = $this->reduce(array("variable", $this->vPrefix . $var));
1380  
1381              if ($res[0] == "raw_color") {
1382                  $res = $this->coerceColor($res);
1383              }
1384  
1385              if (empty($value[2])) $res = $this->lib_e($res);
1386  
1387              return $res;
1388          case "variable":
1389              $key = $value[1];
1390              if (is_array($key)) {
1391                  $key = $this->reduce($key);
1392                  $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
1393              }
1394  
1395              $seen =& $this->env->seenNames;
1396  
1397              if (!empty($seen[$key])) {
1398                  $this->throwError("infinite loop detected: $key");
1399              }
1400  
1401              $seen[$key] = true;
1402              $out = $this->reduce($this->get($key, self::$defaultValue));
1403              $seen[$key] = false;
1404              return $out;
1405          case "list":
1406              foreach ($value[2] as &$item) {
1407                  $item = $this->reduce($item, $forExpression);
1408              }
1409              return $value;
1410          case "expression":
1411              return $this->evaluate($value);
1412          case "string":
1413              foreach ($value[2] as &$part) {
1414                  if (is_array($part)) {
1415                      $strip = $part[0] == "variable";
1416                      $part = $this->reduce($part);
1417                      if ($strip) $part = $this->lib_e($part);
1418                  }
1419              }
1420              return $value;
1421          case "escape":
1422              list(,$inner) = $value;
1423              return $this->lib_e($this->reduce($inner));
1424          case "function":
1425              $color = $this->funcToColor($value);
1426              if ($color) return $color;
1427  
1428              list(, $name, $args) = $value;
1429              if ($name == "%") $name = "_sprintf";
1430              $f = isset($this->libFunctions[$name]) ?
1431                  $this->libFunctions[$name] : array($this, 'lib_'.$name);
1432  
1433              if (is_callable($f)) {
1434                  if ($args[0] == 'list')
1435                      $args = self::compressList($args[2], $args[1]);
1436  
1437                  $ret = call_user_func($f, $this->reduce($args, true), $this);
1438  
1439                  if (is_null($ret)) {
1440                      return array("string", "", array(
1441                          $name, "(", $args, ")"
1442                      ));
1443                  }
1444  
1445                  // convert to a typed value if the result is a php primitive
1446                  if (is_numeric($ret)) $ret = array('number', $ret, "");
1447                  elseif (!is_array($ret)) $ret = array('keyword', $ret);
1448  
1449                  return $ret;
1450              }
1451  
1452              // plain function, reduce args
1453              $value[2] = $this->reduce($value[2]);
1454              return $value;
1455          case "unary":
1456              list(, $op, $exp) = $value;
1457              $exp = $this->reduce($exp);
1458  
1459              if ($exp[0] == "number") {
1460                  switch ($op) {
1461                  case "+":
1462                      return $exp;
1463                  case "-":
1464                      $exp[1] *= -1;
1465                      return $exp;
1466                  }
1467              }
1468              return array("string", "", array($op, $exp));
1469          }
1470  
1471          if ($forExpression) {
1472              switch ($value[0]) {
1473              case "keyword":
1474                  if ($color = $this->coerceColor($value)) {
1475                      return $color;
1476                  }
1477                  break;
1478              case "raw_color":
1479                  return $this->coerceColor($value);
1480              }
1481          }
1482  
1483          return $value;
1484      }
1485  
1486  
1487      // coerce a value for use in color operation
1488  	protected function coerceColor($value) {
1489          switch($value[0]) {
1490              case 'color': return $value;
1491              case 'raw_color':
1492                  $c = array("color", 0, 0, 0);
1493                  $colorStr = substr($value[1], 1);
1494                  $num = hexdec($colorStr);
1495                  $width = strlen($colorStr) == 3 ? 16 : 256;
1496  
1497                  for ($i = 3; $i > 0; $i--) { // 3 2 1
1498                      $t = $num % $width;
1499                      $num /= $width;
1500  
1501                      $c[$i] = $t * (256/$width) + $t * floor(16/$width);
1502                  }
1503  
1504                  return $c;
1505              case 'keyword':
1506                  $name = $value[1];
1507                  if (isset(self::$cssColors[$name])) {
1508                      $rgba = explode(',', self::$cssColors[$name]);
1509  
1510                      if(isset($rgba[3]))
1511                          return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
1512  
1513                      return array('color', $rgba[0], $rgba[1], $rgba[2]);
1514                  }
1515                  return null;
1516          }
1517      }
1518  
1519      // make something string like into a string
1520  	protected function coerceString($value) {
1521          switch ($value[0]) {
1522          case "string":
1523              return $value;
1524          case "keyword":
1525              return array("string", "", array($value[1]));
1526          }
1527          return null;
1528      }
1529  
1530      // turn list of length 1 into value type
1531  	protected function flattenList($value) {
1532          if ($value[0] == "list" && count($value[2]) == 1) {
1533              return $this->flattenList($value[2][0]);
1534          }
1535          return $value;
1536      }
1537  
1538  	protected function toBool($a) {
1539          if ($a) return self::$TRUE;
1540          else return self::$FALSE;
1541      }
1542  
1543      // evaluate an expression
1544  	protected function evaluate($exp) {
1545          list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1546  
1547          $left = $this->reduce($left, true);
1548          $right = $this->reduce($right, true);
1549  
1550          if ($leftColor = $this->coerceColor($left)) {
1551              $left = $leftColor;
1552          }
1553  
1554          if ($rightColor = $this->coerceColor($right)) {
1555              $right = $rightColor;
1556          }
1557  
1558          $ltype = $left[0];
1559          $rtype = $right[0];
1560  
1561          // operators that work on all types
1562          if ($op == "and") {
1563              return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1564          }
1565  
1566          if ($op == "=") {
1567              return $this->toBool($this->eq($left, $right) );
1568          }
1569  
1570          if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
1571              return $str;
1572          }
1573  
1574          // type based operators
1575          $fname = "op_$ltype}_$rtype}";
1576          if (is_callable(array($this, $fname))) {
1577              $out = $this->$fname($op, $left, $right);
1578              if (!is_null($out)) return $out;
1579          }
1580  
1581          // make the expression look it did before being parsed
1582          $paddedOp = $op;
1583          if ($whiteBefore) $paddedOp = " " . $paddedOp;
1584          if ($whiteAfter) $paddedOp .= " ";
1585  
1586          return array("string", "", array($left, $paddedOp, $right));
1587      }
1588  
1589  	protected function stringConcatenate($left, $right) {
1590          if ($strLeft = $this->coerceString($left)) {
1591              if ($right[0] == "string") {
1592                  $right[1] = "";
1593              }
1594              $strLeft[2][] = $right;
1595              return $strLeft;
1596          }
1597  
1598          if ($strRight = $this->coerceString($right)) {
1599              array_unshift($strRight[2], $left);
1600              return $strRight;
1601          }
1602      }
1603  
1604  
1605      // make sure a color's components don't go out of bounds
1606  	protected function fixColor($c) {
1607          foreach (range(1, 3) as $i) {
1608              if ($c[$i] < 0) $c[$i] = 0;
1609              if ($c[$i] > 255) $c[$i] = 255;
1610          }
1611  
1612          return $c;
1613      }
1614  
1615  	protected function op_number_color($op, $lft, $rgt) {
1616          if ($op == '+' || $op == '*') {
1617              return $this->op_color_number($op, $rgt, $lft);
1618          }
1619      }
1620  
1621  	protected function op_color_number($op, $lft, $rgt) {
1622          if ($rgt[0] == '%') $rgt[1] /= 100;
1623  
1624          return $this->op_color_color($op, $lft,
1625              array_fill(1, count($lft) - 1, $rgt[1]));
1626      }
1627  
1628  	protected function op_color_color($op, $left, $right) {
1629          $out = array('color');
1630          $max = count($left) > count($right) ? count($left) : count($right);
1631          foreach (range(1, $max - 1) as $i) {
1632              $lval = isset($left[$i]) ? $left[$i] : 0;
1633              $rval = isset($right[$i]) ? $right[$i] : 0;
1634              switch ($op) {
1635              case '+':
1636                  $out[] = $lval + $rval;
1637                  break;
1638              case '-':
1639                  $out[] = $lval - $rval;
1640                  break;
1641              case '*':
1642                  $out[] = $lval * $rval;
1643                  break;
1644              case '%':
1645                  $out[] = $lval % $rval;
1646                  break;
1647              case '/':
1648                  if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
1649                  $out[] = $lval / $rval;
1650                  break;
1651              default:
1652                  $this->throwError('evaluate error: color op number failed on op '.$op);
1653              }
1654          }
1655          return $this->fixColor($out);
1656      }
1657  
1658  	function lib_red($color){
1659          $color = $this->coerceColor($color);
1660          if (is_null($color)) {
1661              $this->throwError('color expected for red()');
1662          }
1663  
1664          return $color[1];
1665      }
1666  
1667  	function lib_green($color){
1668          $color = $this->coerceColor($color);
1669          if (is_null($color)) {
1670              $this->throwError('color expected for green()');
1671          }
1672  
1673          return $color[2];
1674      }
1675  
1676  	function lib_blue($color){
1677          $color = $this->coerceColor($color);
1678          if (is_null($color)) {
1679              $this->throwError('color expected for blue()');
1680          }
1681  
1682          return $color[3];
1683      }
1684  
1685  
1686      // operator on two numbers
1687  	protected function op_number_number($op, $left, $right) {
1688          $unit = empty($left[2]) ? $right[2] : $left[2];
1689  
1690          $value = 0;
1691          switch ($op) {
1692          case '+':
1693              $value = $left[1] + $right[1];
1694              break;
1695          case '*':
1696              $value = $left[1] * $right[1];
1697              break;
1698          case '-':
1699              $value = $left[1] - $right[1];
1700              break;
1701          case '%':
1702              $value = $left[1] % $right[1];
1703              break;
1704          case '/':
1705              if ($right[1] == 0) $this->throwError('parse error: divide by zero');
1706              $value = $left[1] / $right[1];
1707              break;
1708          case '<':
1709              return $this->toBool($left[1] < $right[1]);
1710          case '>':
1711              return $this->toBool($left[1] > $right[1]);
1712          case '>=':
1713              return $this->toBool($left[1] >= $right[1]);
1714          case '=<':
1715              return $this->toBool($left[1] <= $right[1]);
1716          default:
1717              $this->throwError('parse error: unknown number operator: '.$op);
1718          }
1719  
1720          return array("number", $value, $unit);
1721      }
1722  
1723  
1724      /* environment functions */
1725  
1726  	protected function makeOutputBlock($type, $selectors = null) {
1727          $b = new stdclass;
1728          $b->lines = array();
1729          $b->children = array();
1730          $b->selectors = $selectors;
1731          $b->type = $type;
1732          $b->parent = $this->scope;
1733          return $b;
1734      }
1735  
1736      // the state of execution
1737  	protected function pushEnv($block = null) {
1738          $e = new stdclass;
1739          $e->parent = $this->env;
1740          $e->store = array();
1741          $e->block = $block;
1742  
1743          $this->env = $e;
1744          return $e;
1745      }
1746  
1747      // pop something off the stack
1748  	protected function popEnv() {
1749          $old = $this->env;
1750          $this->env = $this->env->parent;
1751          return $old;
1752      }
1753  
1754      // set something in the current env
1755  	protected function set($name, $value) {
1756          $this->env->store[$name] = $value;
1757      }
1758  
1759  
1760      // get the highest occurrence entry for a name
1761  	protected function get($name, $default=null) {
1762          $current = $this->env;
1763  
1764          $isArguments = $name == $this->vPrefix . 'arguments';
1765          while ($current) {
1766              if ($isArguments && isset($current->arguments)) {
1767                  return array('list', ' ', $current->arguments);
1768              }
1769  
1770              if (isset($current->store[$name]))
1771                  return $current->store[$name];
1772              else {
1773                  $current = isset($current->storeParent) ?
1774                      $current->storeParent : $current->parent;
1775              }
1776          }
1777  
1778          return $default;
1779      }
1780  
1781      // inject array of unparsed strings into environment as variables
1782  	protected function injectVariables($args) {
1783          $this->pushEnv();
1784          $parser = new lessc_parser($this, __METHOD__);
1785          foreach ($args as $name => $strValue) {
1786              if ($name{0} != '@') $name = '@'.$name;
1787              $parser->count = 0;
1788              $parser->buffer = (string)$strValue;
1789              if (!$parser->propertyValue($value)) {
1790                  throw new Exception("failed to parse passed in variable $name: $strValue");
1791              }
1792  
1793              $this->set($name, $value);
1794          }
1795      }
1796  
1797      /**
1798       * Initialize any static state, can initialize parser for a file
1799       * $opts isn't used yet
1800       *
1801       * @param null|string $fname
1802       */
1803  	public function __construct($fname = null) {
1804          if ($fname !== null) {
1805              // used for deprecated parse method
1806              $this->_parseFile = $fname;
1807          }
1808      }
1809  
1810  	public function compile($string, $name = null) {
1811          $locale = setlocale(LC_NUMERIC, 0);
1812          setlocale(LC_NUMERIC, "C");
1813  
1814          $this->parser = $this->makeParser($name);
1815          $root = $this->parser->parse($string);
1816  
1817          $this->env = null;
1818          $this->scope = null;
1819  
1820          $this->formatter = $this->newFormatter();
1821  
1822          if (!empty($this->registeredVars)) {
1823              $this->injectVariables($this->registeredVars);
1824          }
1825  
1826          $this->sourceParser = $this->parser; // used for error messages
1827          $this->compileBlock($root);
1828  
1829          ob_start();
1830          $this->formatter->block($this->scope);
1831          $out = ob_get_clean();
1832          setlocale(LC_NUMERIC, $locale);
1833          return $out;
1834      }
1835  
1836  	public function compileFile($fname, $outFname = null) {
1837          if (!is_readable($fname)) {
1838              throw new Exception('load error: failed to find '.$fname);
1839          }
1840  
1841          $pi = pathinfo($fname);
1842  
1843          $oldImport = $this->importDir;
1844  
1845          $this->importDir = (array)$this->importDir;
1846          $this->importDir[] = $pi['dirname'].'/';
1847  
1848          $this->addParsedFile($fname);
1849  
1850          $out = $this->compile(file_get_contents($fname), $fname);
1851  
1852          $this->importDir = $oldImport;
1853  
1854          if ($outFname !== null) {
1855              return file_put_contents($outFname, $out);
1856          }
1857  
1858          return $out;
1859      }
1860  
1861      // compile only if changed input has changed or output doesn't exist
1862  	public function checkedCompile($in, $out) {
1863          if (!is_file($out) || filemtime($in) > filemtime($out)) {
1864              $this->compileFile($in, $out);
1865              return true;
1866          }
1867          return false;
1868      }
1869  
1870      /**
1871       * Execute lessphp on a .less file or a lessphp cache structure
1872       *
1873       * The lessphp cache structure contains information about a specific
1874       * less file having been parsed. It can be used as a hint for future
1875       * calls to determine whether or not a rebuild is required.
1876       *
1877       * The cache structure contains two important keys that may be used
1878       * externally:
1879       *
1880       * compiled: The final compiled CSS
1881       * updated: The time (in seconds) the CSS was last compiled
1882       *
1883       * The cache structure is a plain-ol' PHP associative array and can
1884       * be serialized and unserialized without a hitch.
1885       *
1886       * @param mixed $in Input
1887       * @param bool $force Force rebuild?
1888       * @return array lessphp cache structure
1889       */
1890  	public function cachedCompile($in, $force = false) {
1891          // assume no root
1892          $root = null;
1893  
1894          if (is_string($in)) {
1895              $root = $in;
1896          } elseif (is_array($in) and isset($in['root'])) {
1897              if ($force or ! isset($in['files'])) {
1898                  // If we are forcing a recompile or if for some reason the
1899                  // structure does not contain any file information we should
1900                  // specify the root to trigger a rebuild.
1901                  $root = $in['root'];
1902              } elseif (isset($in['files']) and is_array($in['files'])) {
1903                  foreach ($in['files'] as $fname => $ftime ) {
1904                      if (!file_exists($fname) or filemtime($fname) > $ftime) {
1905                          // One of the files we knew about previously has changed
1906                          // so we should look at our incoming root again.
1907                          $root = $in['root'];
1908                          break;
1909                      }
1910                  }
1911              }
1912          } else {
1913              // TODO: Throw an exception? We got neither a string nor something
1914              // that looks like a compatible lessphp cache structure.
1915              return null;
1916          }
1917  
1918          if ($root !== null) {
1919              // If we have a root value which means we should rebuild.
1920              $out = array();
1921              $out['root'] = $root;
1922              $out['compiled'] = $this->compileFile($root);
1923              $out['files'] = $this->allParsedFiles();
1924              $out['updated'] = time();
1925              return $out;
1926          } else {
1927              // No changes, pass back the structure
1928              // we were given initially.
1929              return $in;
1930          }
1931  
1932      }
1933  
1934      // parse and compile buffer
1935      // This is deprecated
1936  	public function parse($str = null, $initialVariables = null) {
1937          if (is_array($str)) {
1938              $initialVariables = $str;
1939              $str = null;
1940          }
1941  
1942          $oldVars = $this->registeredVars;
1943          if ($initialVariables !== null) {
1944              $this->setVariables($initialVariables);
1945          }
1946  
1947          if ($str == null) {
1948              if (empty($this->_parseFile)) {
1949                  throw new exception("nothing to parse");
1950              }
1951  
1952              $out = $this->compileFile($this->_parseFile);
1953          } else {
1954              $out = $this->compile($str);
1955          }
1956  
1957          $this->registeredVars = $oldVars;
1958          return $out;
1959      }
1960  
1961  	protected function makeParser($name) {
1962          $parser = new lessc_parser($this, $name);
1963          $parser->writeComments = $this->preserveComments;
1964  
1965          return $parser;
1966      }
1967  
1968  	public function setFormatter($name) {
1969          $this->formatterName = $name;
1970      }
1971  
1972  	protected function newFormatter() {
1973          $className = "lessc_formatter_lessjs";
1974          if (!empty($this->formatterName)) {
1975              if (!is_string($this->formatterName))
1976                  return $this->formatterName;
1977              $className = "lessc_formatter_$this->formatterName";
1978          }
1979  
1980          return new $className;
1981      }
1982  
1983  	public function setPreserveComments($preserve) {
1984          $this->preserveComments = $preserve;
1985      }
1986  
1987  	public function registerFunction($name, $func) {
1988          $this->libFunctions[$name] = $func;
1989      }
1990  
1991  	public function unregisterFunction($name) {
1992          unset($this->libFunctions[$name]);
1993      }
1994  
1995  	public function setVariables($variables) {
1996          $this->registeredVars = array_merge($this->registeredVars, $variables);
1997      }
1998  
1999  	public function unsetVariable($name) {
2000          unset($this->registeredVars[$name]);
2001      }
2002  
2003  	public function setImportDir($dirs) {
2004          $this->importDir = (array)$dirs;
2005      }
2006  
2007  	public function addImportDir($dir) {
2008          $this->importDir = (array)$this->importDir;
2009          $this->importDir[] = $dir;
2010      }
2011  
2012  	public function allParsedFiles() {
2013          return $this->allParsedFiles;
2014      }
2015  
2016  	protected function addParsedFile($file) {
2017          $this->allParsedFiles[realpath($file)] = filemtime($file);
2018      }
2019  
2020      /**
2021       * Uses the current value of $this->count to show line and line number
2022       *
2023       * @param null|string $msg
2024       *
2025       * @throws exception
2026       */
2027  	protected function throwError($msg = null) {
2028          if ($this->sourceLoc >= 0) {
2029              $this->sourceParser->throwError($msg, $this->sourceLoc);
2030          }
2031          throw new exception($msg);
2032      }
2033  
2034      // compile file $in to file $out if $in is newer than $out
2035      // returns true when it compiles, false otherwise
2036  	public static function ccompile($in, $out, $less = null) {
2037          if ($less === null) {
2038              $less = new self;
2039          }
2040          return $less->checkedCompile($in, $out);
2041      }
2042  
2043  	public static function cexecute($in, $force = false, $less = null) {
2044          if ($less === null) {
2045              $less = new self;
2046          }
2047          return $less->cachedCompile($in, $force);
2048      }
2049  
2050      static protected $cssColors = array(
2051          'aliceblue' => '240,248,255',
2052          'antiquewhite' => '250,235,215',
2053          'aqua' => '0,255,255',
2054          'aquamarine' => '127,255,212',
2055          'azure' => '240,255,255',
2056          'beige' => '245,245,220',
2057          'bisque' => '255,228,196',
2058          'black' => '0,0,0',
2059          'blanchedalmond' => '255,235,205',
2060          'blue' => '0,0,255',
2061          'blueviolet' => '138,43,226',
2062          'brown' => '165,42,42',
2063          'burlywood' => '222,184,135',
2064          'cadetblue' => '95,158,160',
2065          'chartreuse' => '127,255,0',
2066          'chocolate' => '210,105,30',
2067          'coral' => '255,127,80',
2068          'cornflowerblue' => '100,149,237',
2069          'cornsilk' => '255,248,220',
2070          'crimson' => '220,20,60',
2071          'cyan' => '0,255,255',
2072          'darkblue' => '0,0,139',
2073          'darkcyan' => '0,139,139',
2074          'darkgoldenrod' => '184,134,11',
2075          'darkgray' => '169,169,169',
2076          'darkgreen' => '0,100,0',
2077          'darkgrey' => '169,169,169',
2078          'darkkhaki' => '189,183,107',
2079          'darkmagenta' => '139,0,139',
2080          'darkolivegreen' => '85,107,47',
2081          'darkorange' => '255,140,0',
2082          'darkorchid' => '153,50,204',
2083          'darkred' => '139,0,0',
2084          'darksalmon' => '233,150,122',
2085          'darkseagreen' => '143,188,143',
2086          'darkslateblue' => '72,61,139',
2087          'darkslategray' => '47,79,79',
2088          'darkslategrey' => '47,79,79',
2089          'darkturquoise' => '0,206,209',
2090          'darkviolet' => '148,0,211',
2091          'deeppink' => '255,20,147',
2092          'deepskyblue' => '0,191,255',
2093          'dimgray' => '105,105,105',
2094          'dimgrey' => '105,105,105',
2095          'dodgerblue' => '30,144,255',
2096          'firebrick' => '178,34,34',
2097          'floralwhite' => '255,250,240',
2098          'forestgreen' => '34,139,34',
2099          'fuchsia' => '255,0,255',
2100          'gainsboro' => '220,220,220',
2101          'ghostwhite' => '248,248,255',
2102          'gold' => '255,215,0',
2103          'goldenrod' => '218,165,32',
2104          'gray' => '128,128,128',
2105          'green' => '0,128,0',
2106          'greenyellow' => '173,255,47',
2107          'grey' => '128,128,128',
2108          'honeydew' => '240,255,240',
2109          'hotpink' => '255,105,180',
2110          'indianred' => '205,92,92',
2111          'indigo' => '75,0,130',
2112          'ivory' => '255,255,240',
2113          'khaki' => '240,230,140',
2114          'lavender' => '230,230,250',
2115          'lavenderblush' => '255,240,245',
2116          'lawngreen' => '124,252,0',
2117          'lemonchiffon' => '255,250,205',
2118          'lightblue' => '173,216,230',
2119          'lightcoral' => '240,128,128',
2120          'lightcyan' => '224,255,255',
2121          'lightgoldenrodyellow' => '250,250,210',
2122          'lightgray' => '211,211,211',
2123          'lightgreen' => '144,238,144',
2124          'lightgrey' => '211,211,211',
2125          'lightpink' => '255,182,193',
2126          'lightsalmon' => '255,160,122',
2127          'lightseagreen' => '32,178,170',
2128          'lightskyblue' => '135,206,250',
2129          'lightslategray' => '119,136,153',
2130          'lightslategrey' => '119,136,153',
2131          'lightsteelblue' => '176,196,222',
2132          'lightyellow' => '255,255,224',
2133          'lime' => '0,255,0',
2134          'limegreen' => '50,205,50',
2135          'linen' => '250,240,230',
2136          'magenta' => '255,0,255',
2137          'maroon' => '128,0,0',
2138          'mediumaquamarine' => '102,205,170',
2139          'mediumblue' => '0,0,205',
2140          'mediumorchid' => '186,85,211',
2141          'mediumpurple' => '147,112,219',
2142          'mediumseagreen' => '60,179,113',
2143          'mediumslateblue' => '123,104,238',
2144          'mediumspringgreen' => '0,250,154',
2145          'mediumturquoise' => '72,209,204',
2146          'mediumvioletred' => '199,21,133',
2147          'midnightblue' => '25,25,112',
2148          'mintcream' => '245,255,250',
2149          'mistyrose' => '255,228,225',
2150          'moccasin' => '255,228,181',
2151          'navajowhite' => '255,222,173',
2152          'navy' => '0,0,128',
2153          'oldlace' => '253,245,230',
2154          'olive' => '128,128,0',
2155          'olivedrab' => '107,142,35',
2156          'orange' => '255,165,0',
2157          'orangered' => '255,69,0',
2158          'orchid' => '218,112,214',
2159          'palegoldenrod' => '238,232,170',
2160          'palegreen' => '152,251,152',
2161          'paleturquoise' => '175,238,238',
2162          'palevioletred' => '219,112,147',
2163          'papayawhip' => '255,239,213',
2164          'peachpuff' => '255,218,185',
2165          'peru' => '205,133,63',
2166          'pink' => '255,192,203',
2167          'plum' => '221,160,221',
2168          'powderblue' => '176,224,230',
2169          'purple' => '128,0,128',
2170          'red' => '255,0,0',
2171          'rosybrown' => '188,143,143',
2172          'royalblue' => '65,105,225',
2173          'saddlebrown' => '139,69,19',
2174          'salmon' => '250,128,114',
2175          'sandybrown' => '244,164,96',
2176          'seagreen' => '46,139,87',
2177          'seashell' => '255,245,238',
2178          'sienna' => '160,82,45',
2179          'silver' => '192,192,192',
2180          'skyblue' => '135,206,235',
2181          'slateblue' => '106,90,205',
2182          'slategray' => '112,128,144',
2183          'slategrey' => '112,128,144',
2184          'snow' => '255,250,250',
2185          'springgreen' => '0,255,127',
2186          'steelblue' => '70,130,180',
2187          'tan' => '210,180,140',
2188          'teal' => '0,128,128',
2189          'thistle' => '216,191,216',
2190          'tomato' => '255,99,71',
2191          'transparent' => '0,0,0,0',
2192          'turquoise' => '64,224,208',
2193          'violet' => '238,130,238',
2194          'wheat' => '245,222,179',
2195          'white' => '255,255,255',
2196          'whitesmoke' => '245,245,245',
2197          'yellow' => '255,255,0',
2198          'yellowgreen' => '154,205,50'
2199      );
2200  }
2201  
2202  // responsible for taking a string of LESS code and converting it into a
2203  // syntax tree
2204  class lessc_parser {
2205      static protected $nextBlockId = 0; // used to uniquely identify blocks
2206  
2207      static protected $precedence = array(
2208          '=<' => 0,
2209          '>=' => 0,
2210          '=' => 0,
2211          '<' => 0,
2212          '>' => 0,
2213  
2214          '+' => 1,
2215          '-' => 1,
2216          '*' => 2,
2217          '/' => 2,
2218          '%' => 2,
2219      );
2220  
2221      static protected $whitePattern;
2222      static protected $commentMulti;
2223  
2224      static protected $commentSingle = "//";
2225      static protected $commentMultiLeft = "/*";
2226      static protected $commentMultiRight = "*/";
2227  
2228      // regex string to match any of the operators
2229      static protected $operatorString;
2230  
2231      // these properties will supress division unless it's inside parenthases
2232      static protected $supressDivisionProps =
2233          array('/border-radius$/i', '/^font$/i');
2234  
2235      protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
2236      protected $lineDirectives = array("charset");
2237  
2238      /**
2239       * if we are in parens we can be more liberal with whitespace around
2240       * operators because it must evaluate to a single value and thus is less
2241       * ambiguous.
2242       *
2243       * Consider:
2244       *     property1: 10 -5; // is two numbers, 10 and -5
2245       *     property2: (10 -5); // should evaluate to 5
2246       */
2247      protected $inParens = false;
2248  
2249      // caches preg escaped literals
2250      static protected $literalCache = array();
2251  
2252  	public function __construct($lessc, $sourceName = null) {
2253          $this->eatWhiteDefault = true;
2254          // reference to less needed for vPrefix, mPrefix, and parentSelector
2255          $this->lessc = $lessc;
2256  
2257          $this->sourceName = $sourceName; // name used for error messages
2258  
2259          $this->writeComments = false;
2260  
2261          if (!self::$operatorString) {
2262              self::$operatorString =
2263                  '('.implode('|', array_map(array('lessc', 'preg_quote'),
2264                      array_keys(self::$precedence))).')';
2265  
2266              $commentSingle = lessc::preg_quote(self::$commentSingle);
2267              $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
2268              $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
2269  
2270              self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2271              self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2272          }
2273      }
2274  
2275  	public function parse($buffer) {
2276          $this->count = 0;
2277          $this->line = 1;
2278  
2279          $this->env = null; // block stack
2280          $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
2281          $this->pushSpecialBlock("root");
2282          $this->eatWhiteDefault = true;
2283          $this->seenComments = array();
2284  
2285          // trim whitespace on head
2286          // if (preg_match('/^\s+/', $this->buffer, $m)) {
2287          //     $this->line += substr_count($m[0], "\n");
2288          //     $this->buffer = ltrim($this->buffer);
2289          // }
2290          $this->whitespace();
2291  
2292          // parse the entire file
2293          $lastCount = $this->count;
2294          while (false !== $this->parseChunk());
2295  
2296          if ($this->count != strlen($this->buffer))
2297              $this->throwError();
2298  
2299          // TODO report where the block was opened
2300          if (!is_null($this->env->parent))
2301              throw new exception('parse error: unclosed block');
2302  
2303          return $this->env;
2304      }
2305  
2306      /**
2307       * Parse a single chunk off the head of the buffer and append it to the
2308       * current parse environment.
2309       * Returns false when the buffer is empty, or when there is an error.
2310       *
2311       * This function is called repeatedly until the entire document is
2312       * parsed.
2313       *
2314       * This parser is most similar to a recursive descent parser. Single
2315       * functions represent discrete grammatical rules for the language, and
2316       * they are able to capture the text that represents those rules.
2317       *
2318       * Consider the function lessc::keyword(). (all parse functions are
2319       * structured the same)
2320       *
2321       * The function takes a single reference argument. When calling the
2322       * function it will attempt to match a keyword on the head of the buffer.
2323       * If it is successful, it will place the keyword in the referenced
2324       * argument, advance the position in the buffer, and return true. If it
2325       * fails then it won't advance the buffer and it will return false.
2326       *
2327       * All of these parse functions are powered by lessc::match(), which behaves
2328       * the same way, but takes a literal regular expression. Sometimes it is
2329       * more convenient to use match instead of creating a new function.
2330       *
2331       * Because of the format of the functions, to parse an entire string of
2332       * grammatical rules, you can chain them together using &&.
2333       *
2334       * But, if some of the rules in the chain succeed before one fails, then
2335       * the buffer position will be left at an invalid state. In order to
2336       * avoid this, lessc::seek() is used to remember and set buffer positions.
2337       *
2338       * Before parsing a chain, use $s = $this->seek() to remember the current
2339       * position into $s. Then if a chain fails, use $this->seek($s) to
2340       * go back where we started.
2341       */
2342  	protected function parseChunk() {
2343          if (empty($this->buffer)) return false;
2344          $s = $this->seek();
2345  
2346          // setting a property
2347          if ($this->keyword($key) && $this->assign() &&
2348              $this->propertyValue($value, $key) && $this->end())
2349          {
2350              $this->append(array('assign', $key, $value), $s);
2351              return true;
2352          } else {
2353              $this->seek($s);
2354          }
2355  
2356  
2357          // look for special css blocks
2358          if ($this->literal('@', false)) {
2359              $this->count--;
2360  
2361              // media
2362              if ($this->literal('@media')) {
2363                  if (($this->mediaQueryList($mediaQueries) || true)
2364                      && $this->literal('{'))
2365                  {
2366                      $media = $this->pushSpecialBlock("media");
2367                      $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
2368                      return true;
2369                  } else {
2370                      $this->seek($s);
2371                      return false;
2372                  }
2373              }
2374  
2375              if ($this->literal("@", false) && $this->keyword($dirName)) {
2376                  if ($this->isDirective($dirName, $this->blockDirectives)) {
2377                      if (($this->openString("{", $dirValue, null, array(";")) || true) &&
2378                          $this->literal("{"))
2379                      {
2380                          $dir = $this->pushSpecialBlock("directive");
2381                          $dir->name = $dirName;
2382                          if (isset($dirValue)) $dir->value = $dirValue;
2383                          return true;
2384                      }
2385                  } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
2386                      if ($this->propertyValue($dirValue) && $this->end()) {
2387                          $this->append(array("directive", $dirName, $dirValue));
2388                          return true;
2389                      }
2390                  }
2391              }
2392  
2393              $this->seek($s);
2394          }
2395  
2396          // setting a variable
2397          if ($this->variable($var) && $this->assign() &&
2398              $this->propertyValue($value) && $this->end())
2399          {
2400              $this->append(array('assign', $var, $value), $s);
2401              return true;
2402          } else {
2403              $this->seek($s);
2404          }
2405  
2406          if ($this->import($importValue)) {
2407              $this->append($importValue, $s);
2408              return true;
2409          }
2410  
2411          // opening parametric mixin
2412          if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
2413              ($this->guards($guards) || true) &&
2414              $this->literal('{'))
2415          {
2416              $block = $this->pushBlock($this->fixTags(array($tag)));
2417              $block->args = $args;
2418              $block->isVararg = $isVararg;
2419              if (!empty($guards)) $block->guards = $guards;
2420              return true;
2421          } else {
2422              $this->seek($s);
2423          }
2424  
2425          // opening a simple block
2426          if ($this->tags($tags) && $this->literal('{')) {
2427              $tags = $this->fixTags($tags);
2428              $this->pushBlock($tags);
2429              return true;
2430          } else {
2431              $this->seek($s);
2432          }
2433  
2434          // closing a block
2435          if ($this->literal('}', false)) {
2436              try {
2437                  $block = $this->pop();
2438              } catch (exception $e) {
2439                  $this->seek($s);
2440                  $this->throwError($e->getMessage());
2441              }
2442  
2443              $hidden = false;
2444              if (is_null($block->type)) {
2445                  $hidden = true;
2446                  if (!isset($block->args)) {
2447                      foreach ($block->tags as $tag) {
2448                          if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) {
2449                              $hidden = false;
2450                              break;
2451                          }
2452                      }
2453                  }
2454  
2455                  foreach ($block->tags as $tag) {
2456                      if (is_string($tag)) {
2457                          $this->env->children[$tag][] = $block;
2458                      }
2459                  }
2460              }
2461  
2462              if (!$hidden) {
2463                  $this->append(array('block', $block), $s);
2464              }
2465  
2466              // this is done here so comments aren't bundled into he block that
2467              // was just closed
2468              $this->whitespace();
2469              return true;
2470          }
2471  
2472          // mixin
2473          if ($this->mixinTags($tags) &&
2474              ($this->argumentDef($argv, $isVararg) || true) &&
2475              ($this->keyword($suffix) || true) && $this->end())
2476          {
2477              $tags = $this->fixTags($tags);
2478              $this->append(array('mixin', $tags, $argv, $suffix), $s);
2479              return true;
2480          } else {
2481              $this->seek($s);
2482          }
2483  
2484          // spare ;
2485          if ($this->literal(';')) return true;
2486  
2487          return false; // got nothing, throw error
2488      }
2489  
2490  	protected function isDirective($dirname, $directives) {
2491          // TODO: cache pattern in parser
2492          $pattern = implode("|",
2493              array_map(array("lessc", "preg_quote"), $directives));
2494          $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
2495  
2496          return preg_match($pattern, $dirname);
2497      }
2498  
2499  	protected function fixTags($tags) {
2500          // move @ tags out of variable namespace
2501          foreach ($tags as &$tag) {
2502              if ($tag{0} == $this->lessc->vPrefix)
2503                  $tag[0] = $this->lessc->mPrefix;
2504          }
2505          return $tags;
2506      }
2507  
2508      // a list of expressions
2509  	protected function expressionList(&$exps) {
2510          $values = array();
2511  
2512          while ($this->expression($exp)) {
2513              $values[] = $exp;
2514          }
2515  
2516          if (count($values) == 0) return false;
2517  
2518          $exps = lessc::compressList($values, ' ');
2519          return true;
2520      }
2521  
2522      /**
2523       * Attempt to consume an expression.
2524       *
2525       * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
2526       *
2527       * @param array $out
2528       *
2529       * @return bool
2530       */
2531  	protected function expression(&$out) {
2532          if ($this->value($lhs)) {
2533              $out = $this->expHelper($lhs, 0);
2534  
2535              // look for / shorthand
2536              if (!empty($this->env->supressedDivision)) {
2537                  unset($this->env->supressedDivision);
2538                  $s = $this->seek();
2539                  if ($this->literal("/") && $this->value($rhs)) {
2540                      $out = array("list", "",
2541                          array($out, array("keyword", "/"), $rhs));
2542                  } else {
2543                      $this->seek($s);
2544                  }
2545              }
2546  
2547              return true;
2548          }
2549          return false;
2550      }
2551  
2552      /**
2553       * recursively parse infix equation with $lhs at precedence $minP
2554       *
2555       * @param array $lhs
2556       * @param mixed $minP
2557       *
2558       * @return array
2559       */
2560  	protected function expHelper($lhs, $minP) {
2561          $this->inExp = true;
2562          $ss = $this->seek();
2563  
2564          while (true) {
2565              $whiteBefore = isset($this->buffer[$this->count - 1]) &&
2566                  ctype_space($this->buffer[$this->count - 1]);
2567  
2568              // If there is whitespace before the operator, then we require
2569              // whitespace after the operator for it to be an expression
2570              $needWhite = $whiteBefore && !$this->inParens;
2571  
2572              if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
2573                  if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
2574                      foreach (self::$supressDivisionProps as $pattern) {
2575                          if (preg_match($pattern, $this->env->currentProperty)) {
2576                              $this->env->supressedDivision = true;
2577                              break 2;
2578                          }
2579                      }
2580                  }
2581  
2582  
2583                  $whiteAfter = isset($this->buffer[$this->count - 1]) &&
2584                      ctype_space($this->buffer[$this->count - 1]);
2585  
2586                  if (!$this->value($rhs)) break;
2587  
2588                  // peek for next operator to see what to do with rhs
2589                  if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
2590                      $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
2591                  }
2592  
2593                  $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
2594                  $ss = $this->seek();
2595  
2596                  continue;
2597              }
2598  
2599              break;
2600          }
2601  
2602          $this->seek($ss);
2603  
2604          return $lhs;
2605      }
2606  
2607      // consume a list of values for a property
2608  	public function propertyValue(&$value, $keyName = null) {
2609          $values = array();
2610  
2611          if ($keyName !== null) $this->env->currentProperty = $keyName;
2612  
2613          $s = null;
2614          while ($this->expressionList($v)) {
2615              $values[] = $v;
2616              $s = $this->seek();
2617              if (!$this->literal(',')) break;
2618          }
2619  
2620          if ($s) $this->seek($s);
2621  
2622          if ($keyName !== null) unset($this->env->currentProperty);
2623  
2624          if (count($values) == 0) return false;
2625  
2626          $value = lessc::compressList($values, ', ');
2627          return true;
2628      }
2629  
2630  	protected function parenValue(&$out) {
2631          $s = $this->seek();
2632  
2633          // speed shortcut
2634          if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
2635              return false;
2636          }
2637  
2638          $inParens = $this->inParens;
2639          if ($this->literal("(") &&
2640              ($this->inParens = true) && $this->expression($exp) &&
2641              $this->literal(")"))
2642          {
2643              $out = $exp;
2644              $this->inParens = $inParens;
2645              return true;
2646          } else {
2647              $this->inParens = $inParens;
2648              $this->seek($s);
2649          }
2650  
2651          return false;
2652      }
2653  
2654      // a single value
2655  	protected function value(&$value) {
2656          $s = $this->seek();
2657  
2658          // speed shortcut
2659          if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
2660              // negation
2661              if ($this->literal("-", false) &&
2662                  (($this->variable($inner) && $inner = array("variable", $inner)) ||
2663                  $this->unit($inner) ||
2664                  $this->parenValue($inner)))
2665              {
2666                  $value = array("unary", "-", $inner);
2667                  return true;
2668              } else {
2669                  $this->seek($s);
2670              }
2671          }
2672  
2673          if ($this->parenValue($value)) return true;
2674          if ($this->unit($value)) return true;
2675          if ($this->color($value)) return true;
2676          if ($this->func($value)) return true;
2677          if ($this->string($value)) return true;
2678  
2679          if ($this->keyword($word)) {
2680              $value = array('keyword', $word);
2681              return true;
2682          }
2683  
2684          // try a variable
2685          if ($this->variable($var)) {
2686              $value = array('variable', $var);
2687              return true;
2688          }
2689  
2690          // unquote string (should this work on any type?
2691          if ($this->literal("~") && $this->string($str)) {
2692              $value = array("escape", $str);
2693              return true;
2694          } else {
2695              $this->seek($s);
2696          }
2697  
2698          // css hack: \0
2699          if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
2700              $value = array('keyword', '\\'.$m[1]);
2701              return true;
2702          } else {
2703              $this->seek($s);
2704          }
2705  
2706          return false;
2707      }
2708  
2709      // an import statement
2710  	protected function import(&$out) {
2711          $s = $this->seek();
2712          if (!$this->literal('@import')) return false;
2713  
2714          // @import "something.css" media;
2715          // @import url("something.css") media;
2716          // @import url(something.css) media;
2717  
2718          if ($this->propertyValue($value)) {
2719              $out = array("import", $value);
2720              return true;
2721          }
2722      }
2723  
2724  	protected function mediaQueryList(&$out) {
2725          if ($this->genericList($list, "mediaQuery", ",", false)) {
2726              $out = $list[2];
2727              return true;
2728          }
2729          return false;
2730      }
2731  
2732  	protected function mediaQuery(&$out) {
2733          $s = $this->seek();
2734  
2735          $expressions = null;
2736          $parts = array();
2737  
2738          if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
2739              $prop = array("mediaType");
2740              if (isset($only)) $prop[] = "only";
2741              if (isset($not)) $prop[] = "not";
2742              $prop[] = $mediaType;
2743              $parts[] = $prop;
2744          } else {
2745              $this->seek($s);
2746          }
2747  
2748  
2749          if (!empty($mediaType) && !$this->literal("and")) {
2750              // ~
2751          } else {
2752              $this->genericList($expressions, "mediaExpression", "and", false);
2753              if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
2754          }
2755  
2756          if (count($parts) == 0) {
2757              $this->seek($s);
2758              return false;
2759          }
2760  
2761          $out = $parts;
2762          return true;
2763      }
2764  
2765  	protected function mediaExpression(&$out) {
2766          $s = $this->seek();
2767          $value = null;
2768          if ($this->literal("(") &&
2769              $this->keyword($feature) &&
2770              ($this->literal(":") && $this->expression($value) || true) &&
2771              $this->literal(")"))
2772          {
2773              $out = array("mediaExp", $feature);
2774              if ($value) $out[] = $value;
2775              return true;
2776          } elseif ($this->variable($variable)) {
2777              $out = array('variable', $variable);
2778              return true;
2779          }
2780  
2781          $this->seek($s);
2782          return false;
2783      }
2784  
2785      // an unbounded string stopped by $end
2786  	protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
2787          $oldWhite = $this->eatWhiteDefault;
2788          $this->eatWhiteDefault = false;
2789  
2790          $stop = array("'", '"', "@{", $end);
2791          $stop = array_map(array("lessc", "preg_quote"), $stop);
2792          // $stop[] = self::$commentMulti;
2793  
2794          if (!is_null($rejectStrs)) {
2795              $stop = array_merge($stop, $rejectStrs);
2796          }
2797  
2798          $patt = '(.*?)('.implode("|", $stop).')';
2799  
2800          $nestingLevel = 0;
2801  
2802          $content = array();
2803          while ($this->match($patt, $m, false)) {
2804              if (!empty($m[1])) {
2805                  $content[] = $m[1];
2806                  if ($nestingOpen) {
2807                      $nestingLevel += substr_count($m[1], $nestingOpen);
2808                  }
2809              }
2810  
2811              $tok = $m[2];
2812  
2813              $this->count-= strlen($tok);
2814              if ($tok == $end) {
2815                  if ($nestingLevel == 0) {
2816                      break;
2817                  } else {
2818                      $nestingLevel--;
2819                  }
2820              }
2821  
2822              if (($tok == "'" || $tok == '"') && $this->string($str)) {
2823                  $content[] = $str;
2824                  continue;
2825              }
2826  
2827              if ($tok == "@{" && $this->interpolation($inter)) {
2828                  $content[] = $inter;
2829                  continue;
2830              }
2831  
2832              if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
2833                  break;
2834              }
2835  
2836              $content[] = $tok;
2837              $this->count+= strlen($tok);
2838          }
2839  
2840          $this->eatWhiteDefault = $oldWhite;
2841  
2842          if (count($content) == 0) return false;
2843  
2844          // trim the end
2845          if (is_string(end($content))) {
2846              $content[count($content) - 1] = rtrim(end($content));
2847          }
2848  
2849          $out = array("string", "", $content);
2850          return true;
2851      }
2852  
2853  	protected function string(&$out) {
2854          $s = $this->seek();
2855          if ($this->literal('"', false)) {
2856              $delim = '"';
2857          } elseif ($this->literal("'", false)) {
2858              $delim = "'";
2859          } else {
2860              return false;
2861          }
2862  
2863          $content = array();
2864  
2865          // look for either ending delim , escape, or string interpolation
2866          $patt = '([^\n]*?)(@\{|\\\\|' .
2867              lessc::preg_quote($delim).')';
2868  
2869          $oldWhite = $this->eatWhiteDefault;
2870          $this->eatWhiteDefault = false;
2871  
2872          while ($this->match($patt, $m, false)) {
2873              $content[] = $m[1];
2874              if ($m[2] == "@{") {
2875                  $this->count -= strlen($m[2]);
2876                  if ($this->interpolation($inter, false)) {
2877                      $content[] = $inter;
2878                  } else {
2879                      $this->count += strlen($m[2]);
2880                      $content[] = "@{"; // ignore it
2881                  }
2882              } elseif ($m[2] == '\\') {
2883                  $content[] = $m[2];
2884                  if ($this->literal($delim, false)) {
2885                      $content[] = $delim;
2886                  }
2887              } else {
2888                  $this->count -= strlen($delim);
2889                  break; // delim
2890              }
2891          }
2892  
2893          $this->eatWhiteDefault = $oldWhite;
2894  
2895          if ($this->literal($delim)) {
2896              $out = array("string", $delim, $content);
2897              return true;
2898          }
2899  
2900          $this->seek($s);
2901          return false;
2902      }
2903  
2904  	protected function interpolation(&$out) {
2905          $oldWhite = $this->eatWhiteDefault;
2906          $this->eatWhiteDefault = true;
2907  
2908          $s = $this->seek();
2909          if ($this->literal("@{") &&
2910              $this->openString("}", $interp, null, array("'", '"', ";")) &&
2911              $this->literal("}", false))
2912          {
2913              $out = array("interpolate", $interp);
2914              $this->eatWhiteDefault = $oldWhite;
2915              if ($this->eatWhiteDefault) $this->whitespace();
2916              return true;
2917          }
2918  
2919          $this->eatWhiteDefault = $oldWhite;
2920          $this->seek($s);
2921          return false;
2922      }
2923  
2924  	protected function unit(&$unit) {
2925          // speed shortcut
2926          if (isset($this->buffer[$this->count])) {
2927              $char = $this->buffer[$this->count];
2928              if (!ctype_digit($char) && $char != ".") return false;
2929          }
2930  
2931          if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
2932              $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
2933              return true;
2934          }
2935          return false;
2936      }
2937  
2938      // a # color
2939  	protected function color(&$out) {
2940          if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
2941              if (strlen($m[1]) > 7) {
2942                  $out = array("string", "", array($m[1]));
2943              } else {
2944                  $out = array("raw_color", $m[1]);
2945              }
2946              return true;
2947          }
2948  
2949          return false;
2950      }
2951  
2952      // consume an argument definition list surrounded by ()
2953      // each argument is a variable name with optional value
2954      // or at the end a ... or a variable named followed by ...
2955      // arguments are separated by , unless a ; is in the list, then ; is the
2956      // delimiter.
2957  	protected function argumentDef(&$args, &$isVararg) {
2958          $s = $this->seek();
2959          if (!$this->literal('(')) return false;
2960  
2961          $values = array();
2962          $delim = ",";
2963          $method = "expressionList";
2964  
2965          $isVararg = false;
2966          while (true) {
2967              if ($this->literal("...")) {
2968                  $isVararg = true;
2969                  break;
2970              }
2971  
2972              if ($this->$method($value)) {
2973                  if ($value[0] == "variable") {
2974                      $arg = array("arg", $value[1]);
2975                      $ss = $this->seek();
2976  
2977                      if ($this->assign() && $this->$method($rhs)) {
2978                          $arg[] = $rhs;
2979                      } else {
2980                          $this->seek($ss);
2981                          if ($this->literal("...")) {
2982                              $arg[0] = "rest";
2983                              $isVararg = true;
2984                          }
2985                      }
2986  
2987                      $values[] = $arg;
2988                      if ($isVararg) break;
2989                      continue;
2990                  } else {
2991                      $values[] = array("lit", $value);
2992                  }
2993              }
2994  
2995  
2996              if (!$this->literal($delim)) {
2997                  if ($delim == "," && $this->literal(";")) {
2998                      // found new delim, convert existing args
2999                      $delim = ";";
3000                      $method = "propertyValue";
3001  
3002                      // transform arg list
3003                      if (isset($values[1])) { // 2 items
3004                          $newList = array();
3005                          foreach ($values as $i => $arg) {
3006                              switch($arg[0]) {
3007                              case "arg":
3008                                  if ($i) {
3009                                      $this->throwError("Cannot mix ; and , as delimiter types");
3010                                  }
3011                                  $newList[] = $arg[2];
3012                                  break;
3013                              case "lit":
3014                                  $newList[] = $arg[1];
3015                                  break;
3016                              case "rest":
3017                                  $this->throwError("Unexpected rest before semicolon");
3018                              }
3019                          }
3020  
3021                          $newList = array("list", ", ", $newList);
3022  
3023                          switch ($values[0][0]) {
3024                          case "arg":
3025                              $newArg = array("arg", $values[0][1], $newList);
3026                              break;
3027                          case "lit":
3028                              $newArg = array("lit", $newList);
3029                              break;
3030                          }
3031  
3032                      } elseif ($values) { // 1 item
3033                          $newArg = $values[0];
3034                      }
3035  
3036                      if ($newArg) {
3037                          $values = array($newArg);
3038                      }
3039                  } else {
3040                      break;
3041                  }
3042              }
3043          }
3044  
3045          if (!$this->literal(')')) {
3046              $this->seek($s);
3047              return false;
3048          }
3049  
3050          $args = $values;
3051  
3052          return true;
3053      }
3054  
3055      // consume a list of tags
3056      // this accepts a hanging delimiter
3057  	protected function tags(&$tags, $simple = false, $delim = ',') {
3058          $tags = array();
3059          while ($this->tag($tt, $simple)) {
3060              $tags[] = $tt;
3061              if (!$this->literal($delim)) break;
3062          }
3063          if (count($tags) == 0) return false;
3064  
3065          return true;
3066      }
3067  
3068      // list of tags of specifying mixin path
3069      // optionally separated by > (lazy, accepts extra >)
3070  	protected function mixinTags(&$tags) {
3071          $s = $this->seek();
3072          $tags = array();
3073          while ($this->tag($tt, true)) {
3074              $tags[] = $tt;
3075              $this->literal(">");
3076          }
3077  
3078          if (count($tags) == 0) return false;
3079  
3080          return true;
3081      }
3082  
3083      // a bracketed value (contained within in a tag definition)
3084  	protected function tagBracket(&$parts, &$hasExpression) {
3085          // speed shortcut
3086          if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
3087              return false;
3088          }
3089  
3090          $s = $this->seek();
3091  
3092          $hasInterpolation = false;
3093  
3094          if ($this->literal("[", false)) {
3095              $attrParts = array("[");
3096              // keyword, string, operator
3097              while (true) {
3098                  if ($this->literal("]", false)) {
3099                      $this->count--;
3100                      break; // get out early
3101                  }
3102  
3103                  if ($this->match('\s+', $m)) {
3104                      $attrParts[] = " ";
3105                      continue;
3106                  }
3107                  if ($this->string($str)) {
3108                      // escape parent selector, (yuck)
3109                      foreach ($str[2] as &$chunk) {
3110                          $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
3111                      }
3112  
3113                      $attrParts[] = $str;
3114                      $hasInterpolation = true;
3115                      continue;
3116                  }
3117  
3118                  if ($this->keyword($word)) {
3119                      $attrParts[] = $word;
3120                      continue;
3121                  }
3122  
3123                  if ($this->interpolation($inter, false)) {
3124                      $attrParts[] = $inter;
3125                      $hasInterpolation = true;
3126                      continue;
3127                  }
3128  
3129                  // operator, handles attr namespace too
3130                  if ($this->match('[|-~\$\*\^=]+', $m)) {
3131                      $attrParts[] = $m[0];
3132                      continue;
3133                  }
3134  
3135                  break;
3136              }
3137  
3138              if ($this->literal("]", false)) {
3139                  $attrParts[] = "]";
3140                  foreach ($attrParts as $part) {
3141                      $parts[] = $part;
3142                  }
3143                  $hasExpression = $hasExpression || $hasInterpolation;
3144                  return true;
3145              }
3146              $this->seek($s);
3147          }
3148  
3149          $this->seek($s);
3150          return false;
3151      }
3152  
3153      // a space separated list of selectors
3154  	protected function tag(&$tag, $simple = false) {
3155          if ($simple)
3156              $chars = '^@,:;{}\][>\(\) "\'';
3157          else
3158              $chars = '^@,;{}["\'';
3159  
3160          $s = $this->seek();
3161  
3162          $hasExpression = false;
3163          $parts = array();
3164          while ($this->tagBracket($parts, $hasExpression));
3165  
3166          $oldWhite = $this->eatWhiteDefault;
3167          $this->eatWhiteDefault = false;
3168  
3169          while (true) {
3170              if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
3171                  $parts[] = $m[1];
3172                  if ($simple) break;
3173  
3174                  while ($this->tagBracket($parts, $hasExpression));
3175                  continue;
3176              }
3177  
3178              if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
3179                  if ($this->interpolation($interp)) {
3180                      $hasExpression = true;
3181                      $interp[2] = true; // don't unescape
3182                      $parts[] = $interp;
3183                      continue;
3184                  }
3185  
3186                  if ($this->literal("@")) {
3187                      $parts[] = "@";
3188                      continue;
3189                  }
3190              }
3191  
3192              if ($this->unit($unit)) { // for keyframes
3193                  $parts[] = $unit[1];
3194                  $parts[] = $unit[2];
3195                  continue;
3196              }
3197  
3198              break;
3199          }
3200  
3201          $this->eatWhiteDefault = $oldWhite;
3202          if (!$parts) {
3203              $this->seek($s);
3204              return false;
3205          }
3206  
3207          if ($hasExpression) {
3208              $tag = array("exp", array("string", "", $parts));
3209          } else {
3210              $tag = trim(implode($parts));
3211          }
3212  
3213          $this->whitespace();
3214          return true;
3215      }
3216  
3217      // a css function
3218  	protected function func(&$func) {
3219          $s = $this->seek();
3220  
3221          if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
3222              $fname = $m[1];
3223  
3224              $sPreArgs = $this->seek();
3225  
3226              $args = array();
3227              while (true) {
3228                  $ss = $this->seek();
3229                  // this ugly nonsense is for ie filter properties
3230                  if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
3231                      $args[] = array("string", "", array($name, "=", $value));
3232                  } else {
3233                      $this->seek($ss);
3234                      if ($this->expressionList($value)) {
3235                          $args[] = $value;
3236                      }
3237                  }
3238  
3239                  if (!$this->literal(',')) break;
3240              }
3241              $args = array('list', ',', $args);
3242  
3243              if ($this->literal(')')) {
3244                  $func = array('function', $fname, $args);
3245                  return true;
3246              } elseif ($fname == 'url') {
3247                  // couldn't parse and in url? treat as string
3248                  $this->seek($sPreArgs);
3249                  if ($this->openString(")", $string) && $this->literal(")")) {
3250                      $func = array('function', $fname, $string);
3251                      return true;
3252                  }
3253              }
3254          }
3255  
3256          $this->seek($s);
3257          return false;
3258      }
3259  
3260      // consume a less variable
3261  	protected function variable(&$name) {
3262          $s = $this->seek();
3263          if ($this->literal($this->lessc->vPrefix, false) &&
3264              ($this->variable($sub) || $this->keyword($name)))
3265          {
3266              if (!empty($sub)) {
3267                  $name = array('variable', $sub);
3268              } else {
3269                  $name = $this->lessc->vPrefix.$name;
3270              }
3271              return true;
3272          }
3273  
3274          $name = null;
3275          $this->seek($s);
3276          return false;
3277      }
3278  
3279      /**
3280       * Consume an assignment operator
3281       * Can optionally take a name that will be set to the current property name
3282       *
3283       * @param null|string $name
3284       *
3285       * @return bool
3286       */
3287  	protected function assign($name = null) {
3288          if ($name) $this->currentProperty = $name;
3289          return $this->literal(':') || $this->literal('=');
3290      }
3291  
3292      // consume a keyword
3293  	protected function keyword(&$word) {
3294          if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
3295              $word = $m[1];
3296              return true;
3297          }
3298          return false;
3299      }
3300  
3301      // consume an end of statement delimiter
3302  	protected function end() {
3303          if ($this->literal(';')) {
3304              return true;
3305          } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3306              // if there is end of file or a closing block next then we don't need a ;
3307              return true;
3308          }
3309          return false;
3310      }
3311  
3312  	protected function guards(&$guards) {
3313          $s = $this->seek();
3314  
3315          if (!$this->literal("when")) {
3316              $this->seek($s);
3317              return false;
3318          }
3319  
3320          $guards = array();
3321  
3322          while ($this->guardGroup($g)) {
3323              $guards[] = $g;
3324              if (!$this->literal(",")) break;
3325          }
3326  
3327          if (count($guards) == 0) {
3328              $guards = null;
3329              $this->seek($s);
3330              return false;
3331          }
3332  
3333          return true;
3334      }
3335  
3336      // a bunch of guards that are and'd together
3337      // TODO rename to guardGroup
3338  	protected function guardGroup(&$guardGroup) {
3339          $s = $this->seek();
3340          $guardGroup = array();
3341          while ($this->guard($guard)) {
3342              $guardGroup[] = $guard;
3343              if (!$this->literal("and")) break;
3344          }
3345  
3346          if (count($guardGroup) == 0) {
3347              $guardGroup = null;
3348              $this->seek($s);
3349              return false;
3350          }
3351  
3352          return true;
3353      }
3354  
3355  	protected function guard(&$guard) {
3356          $s = $this->seek();
3357          $negate = $this->literal("not");
3358  
3359          if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
3360              $guard = $exp;
3361              if ($negate) $guard = array("negate", $guard);
3362              return true;
3363          }
3364  
3365          $this->seek($s);
3366          return false;
3367      }
3368  
3369      /* raw parsing functions */
3370  
3371  	protected function literal($what, $eatWhitespace = null) {
3372          if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3373  
3374          // shortcut on single letter
3375          if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3376              if ($this->buffer[$this->count] == $what) {
3377                  if (!$eatWhitespace) {
3378                      $this->count++;
3379                      return true;
3380                  }
3381                  // goes below...
3382              } else {
3383                  return false;
3384              }
3385          }
3386  
3387          if (!isset(self::$literalCache[$what])) {
3388              self::$literalCache[$what] = lessc::preg_quote($what);
3389          }
3390  
3391          return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
3392      }
3393  
3394  	protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3395          $s = $this->seek();
3396          $items = array();
3397          while ($this->$parseItem($value)) {
3398              $items[] = $value;
3399              if ($delim) {
3400                  if (!$this->literal($delim)) break;
3401              }
3402          }
3403  
3404          if (count($items) == 0) {
3405              $this->seek($s);
3406              return false;
3407          }
3408  
3409          if ($flatten && count($items) == 1) {
3410              $out = $items[0];
3411          } else {
3412              $out = array("list", $delim, $items);
3413          }
3414  
3415          return true;
3416      }
3417  
3418  
3419      // advance counter to next occurrence of $what
3420      // $until - don't include $what in advance
3421      // $allowNewline, if string, will be used as valid char set
3422      protected function to($what, &$out, $until = false, $allowNewline = false) {
3423          if (is_string($allowNewline)) {
3424              $validChars = $allowNewline;
3425          } else {
3426              $validChars = $allowNewline ? "." : "[^\n]";
3427          }
3428          if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
3429          if ($until) $this->count -= strlen($what); // give back $what
3430          $out = $m[1];
3431          return true;
3432      }
3433  
3434      // try to match something on head of buffer
3435  	protected function match($regex, &$out, $eatWhitespace = null) {
3436          if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3437  
3438          $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
3439          if (preg_match($r, $this->buffer, $out, null, $this->count)) {
3440              $this->count += strlen($out[0]);
3441              if ($eatWhitespace && $this->writeComments) $this->whitespace();
3442              return true;
3443          }
3444          return false;
3445      }
3446  
3447      // match some whitespace
3448  	protected function whitespace() {
3449          if ($this->writeComments) {
3450              $gotWhite = false;
3451              while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
3452                  if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
3453                      $this->append(array("comment", $m[1]));
3454                      $this->commentsSeen[$this->count] = true;
3455                  }
3456                  $this->count += strlen($m[0]);
3457                  $gotWhite = true;
3458              }
3459              return $gotWhite;
3460          } else {
3461              $this->match("", $m);
3462              return strlen($m[0]) > 0;
3463          }
3464      }
3465  
3466      // match something without consuming it
3467  	protected function peek($regex, &$out = null, $from=null) {
3468          if (is_null($from)) $from = $this->count;
3469          $r = '/'.$regex.'/Ais';
3470          $result = preg_match($r, $this->buffer, $out, null, $from);
3471  
3472          return $result;
3473      }
3474  
3475      // seek to a spot in the buffer or return where we are on no argument
3476  	protected function seek($where = null) {
3477          if ($where === null) return $this->count;
3478          else $this->count = $where;
3479          return true;
3480      }
3481  
3482      /* misc functions */
3483  
3484  	public function throwError($msg = "parse error", $count = null) {
3485          $count = is_null($count) ? $this->count : $count;
3486  
3487          $line = $this->line +
3488              substr_count(substr($this->buffer, 0, $count), "\n");
3489  
3490          if (!empty($this->sourceName)) {
3491              $loc = "$this->sourceName on line $line";
3492          } else {
3493              $loc = "line: $line";
3494          }
3495  
3496          // TODO this depends on $this->count
3497          if ($this->peek("(.*?)(\n|$)", $m, $count)) {
3498              throw new exception("$msg: failed at `$m[1]` $loc");
3499          } else {
3500              throw new exception("$msg: $loc");
3501          }
3502      }
3503  
3504  	protected function pushBlock($selectors=null, $type=null) {
3505          $b = new stdclass;
3506          $b->parent = $this->env;
3507  
3508          $b->type = $type;
3509          $b->id = self::$nextBlockId++;
3510  
3511          $b->isVararg = false; // TODO: kill me from here
3512          $b->tags = $selectors;
3513  
3514          $b->props = array();
3515          $b->children = array();
3516  
3517          $this->env = $b;
3518          return $b;
3519      }
3520  
3521      // push a block that doesn't multiply tags
3522  	protected function pushSpecialBlock($type) {
3523          return $this->pushBlock(null, $type);
3524      }
3525  
3526      // append a property to the current block
3527  	protected function append($prop, $pos = null) {
3528          if ($pos !== null) $prop[-1] = $pos;
3529          $this->env->props[] = $prop;
3530      }
3531  
3532      // pop something off the stack
3533  	protected function pop() {
3534          $old = $this->env;
3535          $this->env = $this->env->parent;
3536          return $old;
3537      }
3538  
3539      // remove comments from $text
3540      // todo: make it work for all functions, not just url
3541  	protected function removeComments($text) {
3542          $look = array(
3543              'url(', '//', '/*', '"', "'"
3544          );
3545  
3546          $out = '';
3547          $min = null;
3548          while (true) {
3549              // find the next item
3550              foreach ($look as $token) {
3551                  $pos = strpos($text, $token);
3552                  if ($pos !== false) {
3553                      if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
3554                  }
3555              }
3556  
3557              if (is_null($min)) break;
3558  
3559              $count = $min[1];
3560              $skip = 0;
3561              $newlines = 0;
3562              switch ($min[0]) {
3563              case 'url(':
3564                  if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
3565                      $count += strlen($m[0]) - strlen($min[0]);
3566                  break;
3567              case '"':
3568              case "'":
3569                  if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
3570                      $count += strlen($m[0]) - 1;
3571                  break;
3572              case '//':
3573                  $skip = strpos($text, "\n", $count);
3574                  if ($skip === false) $skip = strlen($text) - $count;
3575                  else $skip -= $count;
3576                  break;
3577              case '/*':
3578                  if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
3579                      $skip = strlen($m[0]);
3580                      $newlines = substr_count($m[0], "\n");
3581                  }
3582                  break;
3583              }
3584  
3585              if ($skip == 0) $count += strlen($min[0]);
3586  
3587              $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
3588              $text = substr($text, $count + $skip);
3589  
3590              $min = null;
3591          }
3592  
3593          return $out.$text;
3594      }
3595  
3596  }
3597  
3598  class lessc_formatter_classic {
3599      public $indentChar = "  ";
3600  
3601      public $break = "\n";
3602      public $open = " {";
3603      public $close = "}";
3604      public $selectorSeparator = ", ";
3605      public $assignSeparator = ":";
3606  
3607      public $openSingle = " { ";
3608      public $closeSingle = " }";
3609  
3610      public $disableSingle = false;
3611      public $breakSelectors = false;
3612  
3613      public $compressColors = false;
3614  
3615      public function __construct() {
3616          $this->indentLevel = 0;
3617      }
3618  
3619      public function indentStr($n = 0) {
3620          return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
3621      }
3622  
3623      public function property($name, $value) {
3624          return $name . $this->assignSeparator . $value . ";";
3625      }
3626  
3627      protected function isEmpty($block) {
3628          if (empty($block->lines)) {
3629              foreach ($block->children as $child) {
3630                  if (!$this->isEmpty($child)) return false;
3631              }
3632  
3633              return true;
3634          }
3635          return false;
3636      }
3637  
3638      public function block($block) {
3639          if ($this->isEmpty($block)) return;
3640  
3641          $inner = $pre = $this->indentStr();
3642  
3643          $isSingle = !$this->disableSingle &&
3644              is_null($block->type) && count($block->lines) == 1;
3645  
3646          if (!empty($block->selectors)) {
3647              $this->indentLevel++;
3648  
3649              if ($this->breakSelectors) {
3650                  $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
3651              } else {
3652                  $selectorSeparator = $this->selectorSeparator;
3653              }
3654  
3655              echo $pre .
3656                  implode($selectorSeparator, $block->selectors);
3657              if ($isSingle) {
3658                  echo $this->openSingle;
3659                  $inner = "";
3660              } else {
3661                  echo $this->open . $this->break;
3662                  $inner = $this->indentStr();
3663              }
3664  
3665          }
3666  
3667          if (!empty($block->lines)) {
3668              $glue = $this->break.$inner;
3669              echo $inner . implode($glue, $block->lines);
3670              if (!$isSingle && !empty($block->children)) {
3671                  echo $this->break;
3672              }
3673          }
3674  
3675          foreach ($block->children as $child) {
3676              $this->block($child);
3677          }
3678  
3679          if (!empty($block->selectors)) {
3680              if (!$isSingle && empty($block->children)) echo $this->break;
3681  
3682              if ($isSingle) {
3683                  echo $this->closeSingle . $this->break;
3684              } else {
3685                  echo $pre . $this->close . $this->break;
3686              }
3687  
3688              $this->indentLevel--;
3689          }
3690      }
3691  }
3692  
3693  class lessc_formatter_compressed extends lessc_formatter_classic {
3694      public $disableSingle = true;
3695      public $open = "{";
3696      public $selectorSeparator = ",";
3697      public $assignSeparator = ":";
3698      public $break = "";
3699      public $compressColors = true;
3700  
3701      public function indentStr($n = 0) {
3702          return "";
3703      }
3704  }
3705  
3706  class lessc_formatter_lessjs extends lessc_formatter_classic {
3707      public $disableSingle = true;
3708      public $breakSelectors = true;
3709      public $assignSeparator = ": ";
3710      public $selectorSeparator = ",";
3711  }
3712  
3713