File indexing completed on 2024-05-05 06:02:18

0001 <?php
0002 
0003 /**
0004  * lessphp v0.4.0
0005  * http://leafo.net/lessphp
0006  *
0007  * LESS css compiler, adapted from http://lesscss.org
0008  *
0009  * Copyright 2012, Leaf Corcoran <leafot@gmail.com>
0010  * Licensed under MIT or GPLv3, see LICENSE
0011  */
0012 
0013 
0014 /**
0015  * The less compiler and parser.
0016  *
0017  * Converting LESS to CSS is a three stage process. The incoming file is parsed
0018  * by `lessc_parser` into a syntax tree, then it is compiled into another tree
0019  * representing the CSS structure by `lessc`. The CSS tree is fed into a
0020  * formatter, like `lessc_formatter` which then outputs CSS as a string.
0021  *
0022  * During the first compile, all values are *reduced*, which means that their
0023  * types are brought to the lowest form before being dump as strings. This
0024  * handles math equations, variable dereferences, and the like.
0025  *
0026  * The `parse` function of `lessc` is the entry point.
0027  *
0028  * In summary:
0029  *
0030  * The `lessc` class creates an intstance of the parser, feeds it LESS code,
0031  * then transforms the resulting tree to a CSS tree. This class also holds the
0032  * evaluation context, such as all available mixins and variables at any given
0033  * time.
0034  *
0035  * The `lessc_parser` class is only concerned with parsing its input.
0036  *
0037  * The `lessc_formatter` takes a CSS tree, and dumps it to a formatted string,
0038  * handling things like indentation.
0039  */
0040 class lessc {
0041   static public $VERSION = "v0.4.0";
0042   static protected $TRUE = array("keyword", "true");
0043   static protected $FALSE = array("keyword", "false");
0044 
0045   protected $libFunctions = array();
0046   protected $registeredVars = array();
0047   protected $preserveComments = false;
0048 
0049   public $vPrefix = '@'; // prefix of abstract properties
0050   public $mPrefix = '$'; // prefix of abstract blocks
0051   public $parentSelector = '&';
0052 
0053   public $importDisabled = false;
0054   public $importDir = '';
0055 
0056   protected $numberPrecision = null;
0057 
0058   protected $allParsedFiles = array();
0059 
0060   // set to the parser that generated the current line when compiling
0061   // so we know how to create error messages
0062   protected $sourceParser = null;
0063   protected $sourceLoc = null;
0064 
0065   static public $defaultValue = array("keyword", "");
0066 
0067   static protected $nextImportId = 0; // uniquely identify imports
0068 
0069   // attempts to find the path of an import url, returns null for css files
0070   protected function findImport($url) {
0071     foreach ((array)$this->importDir as $dir) {
0072       $full = $dir.(substr($dir, -1) != '/' ? '/' : '').$url;
0073       if ($this->fileExists($file = $full.'.less') || $this->fileExists($file = $full)) {
0074         return $file;
0075       }
0076     }
0077 
0078     return null;
0079   }
0080 
0081   protected function fileExists($name) {
0082     return is_file($name);
0083   }
0084 
0085   static public function compressList($items, $delim) {
0086     if (!isset($items[1]) && isset($items[0])) return $items[0];
0087     else return array('list', $delim, $items);
0088   }
0089 
0090   static public function preg_quote($what) {
0091     return preg_quote($what, '/');
0092   }
0093 
0094   protected function tryImport($importPath, $parentBlock, $out) {
0095     if ($importPath[0] == "function" && $importPath[1] == "url") {
0096       $importPath = $this->flattenList($importPath[2]);
0097     }
0098 
0099     $str = $this->coerceString($importPath);
0100     if ($str === null) return false;
0101 
0102     $url = $this->compileValue($this->lib_e($str));
0103 
0104     // don't import if it ends in css
0105     if (substr_compare($url, '.css', -4, 4) === 0) return false;
0106 
0107     $realPath = $this->findImport($url);
0108 
0109     if ($realPath === null) return false;
0110 
0111     if ($this->importDisabled) {
0112       return array(false, "/* import disabled */");
0113     }
0114 
0115     if (isset($this->allParsedFiles[realpath($realPath)])) {
0116       return array(false, null);
0117     }
0118 
0119     $this->addParsedFile($realPath);
0120     $parser = $this->makeParser($realPath);
0121     $root = $parser->parse(file_get_contents($realPath));
0122 
0123     // set the parents of all the block props
0124     foreach ($root->props as $prop) {
0125       if ($prop[0] == "block") {
0126         $prop[1]->parent = $parentBlock;
0127       }
0128     }
0129 
0130     // copy mixins into scope, set their parents
0131     // bring blocks from import into current block
0132     // TODO: need to mark the source parser these came from this file
0133     foreach ($root->children as $childName => $child) {
0134       if (isset($parentBlock->children[$childName])) {
0135         $parentBlock->children[$childName] = array_merge(
0136           $parentBlock->children[$childName],
0137           $child);
0138       } else {
0139         $parentBlock->children[$childName] = $child;
0140       }
0141     }
0142 
0143     $pi = pathinfo($realPath);
0144     $dir = $pi["dirname"];
0145 
0146     list($top, $bottom) = $this->sortProps($root->props, true);
0147     $this->compileImportedProps($top, $parentBlock, $out, $parser, $dir);
0148 
0149     return array(true, $bottom, $parser, $dir);
0150   }
0151 
0152   protected function compileImportedProps($props, $block, $out, $sourceParser, $importDir) {
0153     $oldSourceParser = $this->sourceParser;
0154 
0155     $oldImport = $this->importDir;
0156 
0157     // TODO: this is because the importDir api is stupid
0158     $this->importDir = (array)$this->importDir;
0159     array_unshift($this->importDir, $importDir);
0160 
0161     foreach ($props as $prop) {
0162       $this->compileProp($prop, $block, $out);
0163     }
0164 
0165     $this->importDir = $oldImport;
0166     $this->sourceParser = $oldSourceParser;
0167   }
0168 
0169   /**
0170    * Recursively compiles a block.
0171    *
0172    * A block is analogous to a CSS block in most cases. A single LESS document
0173    * is encapsulated in a block when parsed, but it does not have parent tags
0174    * so all of it's children appear on the root level when compiled.
0175    *
0176    * Blocks are made up of props and children.
0177    *
0178    * Props are property instructions, array tuples which describe an action
0179    * to be taken, eg. write a property, set a variable, mixin a block.
0180    *
0181    * The children of a block are just all the blocks that are defined within.
0182    * This is used to look up mixins when performing a mixin.
0183    *
0184    * Compiling the block involves pushing a fresh environment on the stack,
0185    * and iterating through the props, compiling each one.
0186    *
0187    * See lessc::compileProp()
0188    *
0189    */
0190   protected function compileBlock($block) {
0191     switch ($block->type) {
0192     case "root":
0193       $this->compileRoot($block);
0194       break;
0195     case null:
0196       $this->compileCSSBlock($block);
0197       break;
0198     case "media":
0199       $this->compileMedia($block);
0200       break;
0201     case "directive":
0202       $name = "@" . $block->name;
0203       if (!empty($block->value)) {
0204         $name .= " " . $this->compileValue($this->reduce($block->value));
0205       }
0206 
0207       $this->compileNestedBlock($block, array($name));
0208       break;
0209     default:
0210       $this->throwError("unknown block type: $block->type\n");
0211     }
0212   }
0213 
0214   protected function compileCSSBlock($block) {
0215     $env = $this->pushEnv();
0216 
0217     $selectors = $this->compileSelectors($block->tags);
0218     $env->selectors = $this->multiplySelectors($selectors);
0219     $out = $this->makeOutputBlock(null, $env->selectors);
0220 
0221     $this->scope->children[] = $out;
0222     $this->compileProps($block, $out);
0223 
0224     $block->scope = $env; // mixins carry scope with them!
0225     $this->popEnv();
0226   }
0227 
0228   protected function compileMedia($media) {
0229     $env = $this->pushEnv($media);
0230     $parentScope = $this->mediaParent($this->scope);
0231 
0232     $query = $this->compileMediaQuery($this->multiplyMedia($env));
0233 
0234     $this->scope = $this->makeOutputBlock($media->type, array($query));
0235     $parentScope->children[] = $this->scope;
0236 
0237     $this->compileProps($media, $this->scope);
0238 
0239     if (count($this->scope->lines) > 0) {
0240       $orphanSelelectors = $this->findClosestSelectors();
0241       if (!is_null($orphanSelelectors)) {
0242         $orphan = $this->makeOutputBlock(null, $orphanSelelectors);
0243         $orphan->lines = $this->scope->lines;
0244         array_unshift($this->scope->children, $orphan);
0245         $this->scope->lines = array();
0246       }
0247     }
0248 
0249     $this->scope = $this->scope->parent;
0250     $this->popEnv();
0251   }
0252 
0253   protected function mediaParent($scope) {
0254     while (!empty($scope->parent)) {
0255       if (!empty($scope->type) && $scope->type != "media") {
0256         break;
0257       }
0258       $scope = $scope->parent;
0259     }
0260 
0261     return $scope;
0262   }
0263 
0264   protected function compileNestedBlock($block, $selectors) {
0265     $this->pushEnv($block);
0266     $this->scope = $this->makeOutputBlock($block->type, $selectors);
0267     $this->scope->parent->children[] = $this->scope;
0268 
0269     $this->compileProps($block, $this->scope);
0270 
0271     $this->scope = $this->scope->parent;
0272     $this->popEnv();
0273   }
0274 
0275   protected function compileRoot($root) {
0276     $this->pushEnv();
0277     $this->scope = $this->makeOutputBlock($root->type);
0278     $this->compileProps($root, $this->scope);
0279     $this->popEnv();
0280   }
0281 
0282   protected function compileProps($block, $out) {
0283     foreach ($this->sortProps($block->props) as $prop) {
0284       $this->compileProp($prop, $block, $out);
0285     }
0286 
0287     $out->lines = array_values(array_unique($out->lines));
0288   }
0289 
0290   protected function sortProps($props, $split = false) {
0291     $vars = array();
0292     $imports = array();
0293     $other = array();
0294 
0295     foreach ($props as $prop) {
0296       switch ($prop[0]) {
0297       case "assign":
0298         if (isset($prop[1][0]) && $prop[1][0] == $this->vPrefix) {
0299           $vars[] = $prop;
0300         } else {
0301           $other[] = $prop;
0302         }
0303         break;
0304       case "import":
0305         $id = self::$nextImportId++;
0306         $prop[] = $id;
0307         $imports[] = $prop;
0308         $other[] = array("import_mixin", $id);
0309         break;
0310       default:
0311         $other[] = $prop;
0312       }
0313     }
0314 
0315     if ($split) {
0316       return array(array_merge($vars, $imports), $other);
0317     } else {
0318       return array_merge($vars, $imports, $other);
0319     }
0320   }
0321 
0322   protected function compileMediaQuery($queries) {
0323     $compiledQueries = array();
0324     foreach ($queries as $query) {
0325       $parts = array();
0326       foreach ($query as $q) {
0327         switch ($q[0]) {
0328         case "mediaType":
0329           $parts[] = implode(" ", array_slice($q, 1));
0330           break;
0331         case "mediaExp":
0332           if (isset($q[2])) {
0333             $parts[] = "($q[1]: " .
0334               $this->compileValue($this->reduce($q[2])) . ")";
0335           } else {
0336             $parts[] = "($q[1])";
0337           }
0338           break;
0339         case "variable":
0340           $parts[] = $this->compileValue($this->reduce($q));
0341         break;
0342         }
0343       }
0344 
0345       if (count($parts) > 0) {
0346         $compiledQueries[] =  implode(" and ", $parts);
0347       }
0348     }
0349 
0350     $out = "@media";
0351     if (!empty($parts)) {
0352       $out .= " " .
0353         implode($this->formatter->selectorSeparator, $compiledQueries);
0354     }
0355     return $out;
0356   }
0357 
0358   protected function multiplyMedia($env, $childQueries = null) {
0359     if (is_null($env) ||
0360       !empty($env->block->type) && $env->block->type != "media")
0361     {
0362       return $childQueries;
0363     }
0364 
0365     // plain old block, skip
0366     if (empty($env->block->type)) {
0367       return $this->multiplyMedia($env->parent, $childQueries);
0368     }
0369 
0370     $out = array();
0371     $queries = $env->block->queries;
0372     if (is_null($childQueries)) {
0373       $out = $queries;
0374     } else {
0375       foreach ($queries as $parent) {
0376         foreach ($childQueries as $child) {
0377           $out[] = array_merge($parent, $child);
0378         }
0379       }
0380     }
0381 
0382     return $this->multiplyMedia($env->parent, $out);
0383   }
0384 
0385   protected function expandParentSelectors(&$tag, $replace) {
0386     $parts = explode("$&$", $tag);
0387     $count = 0;
0388     foreach ($parts as &$part) {
0389       $part = str_replace($this->parentSelector, $replace, $part, $c);
0390       $count += $c;
0391     }
0392     $tag = implode($this->parentSelector, $parts);
0393     return $count;
0394   }
0395 
0396   protected function findClosestSelectors() {
0397     $env = $this->env;
0398     $selectors = null;
0399     while ($env !== null) {
0400       if (isset($env->selectors)) {
0401         $selectors = $env->selectors;
0402         break;
0403       }
0404       $env = $env->parent;
0405     }
0406 
0407     return $selectors;
0408   }
0409 
0410 
0411   // multiply $selectors against the nearest selectors in env
0412   protected function multiplySelectors($selectors) {
0413     // find parent selectors
0414 
0415     $parentSelectors = $this->findClosestSelectors();
0416     if (is_null($parentSelectors)) {
0417       // kill parent reference in top level selector
0418       foreach ($selectors as &$s) {
0419         $this->expandParentSelectors($s, "");
0420       }
0421 
0422       return $selectors;
0423     }
0424 
0425     $out = array();
0426     foreach ($parentSelectors as $parent) {
0427       foreach ($selectors as $child) {
0428         $count = $this->expandParentSelectors($child, $parent);
0429 
0430         // don't prepend the parent tag if & was used
0431         if ($count > 0) {
0432           $out[] = trim($child);
0433         } else {
0434           $out[] = trim($parent . ' ' . $child);
0435         }
0436       }
0437     }
0438 
0439     return $out;
0440   }
0441 
0442   // reduces selector expressions
0443   protected function compileSelectors($selectors) {
0444     $out = array();
0445 
0446     foreach ($selectors as $s) {
0447       if (is_array($s)) {
0448         list(, $value) = $s;
0449         $out[] = trim($this->compileValue($this->reduce($value)));
0450       } else {
0451         $out[] = $s;
0452       }
0453     }
0454 
0455     return $out;
0456   }
0457 
0458   protected function eq($left, $right) {
0459     return $left == $right;
0460   }
0461 
0462   protected function patternMatch($block, $orderedArgs, $keywordArgs) {
0463     // match the guards if it has them
0464     // any one of the groups must have all its guards pass for a match
0465     if (!empty($block->guards)) {
0466       $groupPassed = false;
0467       foreach ($block->guards as $guardGroup) {
0468         foreach ($guardGroup as $guard) {
0469           $this->pushEnv();
0470           $this->zipSetArgs($block->args, $orderedArgs, $keywordArgs);
0471 
0472           $negate = false;
0473           if ($guard[0] == "negate") {
0474             $guard = $guard[1];
0475             $negate = true;
0476           }
0477 
0478           $passed = $this->reduce($guard) == self::$TRUE;
0479           if ($negate) $passed = !$passed;
0480 
0481           $this->popEnv();
0482 
0483           if ($passed) {
0484             $groupPassed = true;
0485           } else {
0486             $groupPassed = false;
0487             break;
0488           }
0489         }
0490 
0491         if ($groupPassed) break;
0492       }
0493 
0494       if (!$groupPassed) {
0495         return false;
0496       }
0497     }
0498 
0499     if (empty($block->args)) {
0500       return $block->isVararg || empty($orderedArgs) && empty($keywordArgs);
0501     }
0502 
0503     $remainingArgs = $block->args;
0504     if ($keywordArgs) {
0505       $remainingArgs = array();
0506       foreach ($block->args as $arg) {
0507         if ($arg[0] == "arg" && isset($keywordArgs[$arg[1]])) {
0508           continue;
0509         }
0510 
0511         $remainingArgs[] = $arg;
0512       }
0513     }
0514 
0515     $i = -1; // no args
0516     // try to match by arity or by argument literal
0517     foreach ($remainingArgs as $i => $arg) {
0518       switch ($arg[0]) {
0519       case "lit":
0520         if (empty($orderedArgs[$i]) || !$this->eq($arg[1], $orderedArgs[$i])) {
0521           return false;
0522         }
0523         break;
0524       case "arg":
0525         // no arg and no default value
0526         if (!isset($orderedArgs[$i]) && !isset($arg[2])) {
0527           return false;
0528         }
0529         break;
0530       case "rest":
0531         $i--; // rest can be empty
0532         break 2;
0533       }
0534     }
0535 
0536     if ($block->isVararg) {
0537       return true; // not having enough is handled above
0538     } else {
0539       $numMatched = $i + 1;
0540       // greater than becuase default values always match
0541       return $numMatched >= count($orderedArgs);
0542     }
0543   }
0544 
0545   protected function patternMatchAll($blocks, $orderedArgs, $keywordArgs, $skip=array()) {
0546     $matches = null;
0547     foreach ($blocks as $block) {
0548       // skip seen blocks that don't have arguments
0549       if (isset($skip[$block->id]) && !isset($block->args)) {
0550         continue;
0551       }
0552 
0553       if ($this->patternMatch($block, $orderedArgs, $keywordArgs)) {
0554         $matches[] = $block;
0555       }
0556     }
0557 
0558     return $matches;
0559   }
0560 
0561   // attempt to find blocks matched by path and args
0562   protected function findBlocks($searchIn, $path, $orderedArgs, $keywordArgs, $seen=array()) {
0563     if ($searchIn == null) return null;
0564     if (isset($seen[$searchIn->id])) return null;
0565     $seen[$searchIn->id] = true;
0566 
0567     $name = $path[0];
0568 
0569     if (isset($searchIn->children[$name])) {
0570       $blocks = $searchIn->children[$name];
0571       if (count($path) == 1) {
0572         $matches = $this->patternMatchAll($blocks, $orderedArgs, $keywordArgs, $seen);
0573         if (!empty($matches)) {
0574           // This will return all blocks that match in the closest
0575           // scope that has any matching block, like lessjs
0576           return $matches;
0577         }
0578       } else {
0579         $matches = array();
0580         foreach ($blocks as $subBlock) {
0581           $subMatches = $this->findBlocks($subBlock,
0582             array_slice($path, 1), $orderedArgs, $keywordArgs, $seen);
0583 
0584           if (!is_null($subMatches)) {
0585             foreach ($subMatches as $sm) {
0586               $matches[] = $sm;
0587             }
0588           }
0589         }
0590 
0591         return count($matches) > 0 ? $matches : null;
0592       }
0593     }
0594     if ($searchIn->parent === $searchIn) return null;
0595     return $this->findBlocks($searchIn->parent, $path, $orderedArgs, $keywordArgs, $seen);
0596   }
0597 
0598   // sets all argument names in $args to either the default value
0599   // or the one passed in through $values
0600   protected function zipSetArgs($args, $orderedValues, $keywordValues) {
0601     $assignedValues = array();
0602 
0603     $i = 0;
0604     foreach ($args as  $a) {
0605       if ($a[0] == "arg") {
0606         if (isset($keywordValues[$a[1]])) {
0607           // has keyword arg
0608           $value = $keywordValues[$a[1]];
0609         } elseif (isset($orderedValues[$i])) {
0610           // has ordered arg
0611           $value = $orderedValues[$i];
0612           $i++;
0613         } elseif (isset($a[2])) {
0614           // has default value
0615           $value = $a[2];
0616         } else {
0617           $this->throwError("Failed to assign arg " . $a[1]);
0618           $value = null; // :(
0619         }
0620 
0621         $value = $this->reduce($value);
0622         $this->set($a[1], $value);
0623         $assignedValues[] = $value;
0624       } else {
0625         // a lit
0626         $i++;
0627       }
0628     }
0629 
0630     // check for a rest
0631     $last = end($args);
0632     if ($last[0] == "rest") {
0633       $rest = array_slice($orderedValues, count($args) - 1);
0634       $this->set($last[1], $this->reduce(array("list", " ", $rest)));
0635     }
0636 
0637     // wow is this the only true use of PHP's + operator for arrays?
0638     $this->env->arguments = $assignedValues + $orderedValues;
0639   }
0640 
0641   // compile a prop and update $lines or $blocks appropriately
0642   protected function compileProp($prop, $block, $out) {
0643     // set error position context
0644     $this->sourceLoc = isset($prop[-1]) ? $prop[-1] : -1;
0645 
0646     switch ($prop[0]) {
0647     case 'assign':
0648       list(, $name, $value) = $prop;
0649       if ($name[0] == $this->vPrefix) {
0650         $this->set($name, $value);
0651       } else {
0652         $out->lines[] = $this->formatter->property($name,
0653             $this->compileValue($this->reduce($value)));
0654       }
0655       break;
0656     case 'block':
0657       list(, $child) = $prop;
0658       $this->compileBlock($child);
0659       break;
0660     case 'mixin':
0661       list(, $path, $args, $suffix) = $prop;
0662 
0663       $orderedArgs = array();
0664       $keywordArgs = array();
0665       foreach ((array)$args as $arg) {
0666         $argval = null;
0667         switch ($arg[0]) {
0668         case "arg":
0669           if (!isset($arg[2])) {
0670             $orderedArgs[] = $this->reduce(array("variable", $arg[1]));
0671           } else {
0672             $keywordArgs[$arg[1]] = $this->reduce($arg[2]);
0673           }
0674           break;
0675 
0676         case "lit":
0677           $orderedArgs[] = $this->reduce($arg[1]);
0678           break;
0679         default:
0680           $this->throwError("Unknown arg type: " . $arg[0]);
0681         }
0682       }
0683 
0684       $mixins = $this->findBlocks($block, $path, $orderedArgs, $keywordArgs);
0685 
0686       if ($mixins === null) {
0687         // fwrite(STDERR,"failed to find block: ".implode(" > ", $path)."\n");
0688         break; // throw error here??
0689       }
0690 
0691       foreach ($mixins as $mixin) {
0692         if ($mixin === $block && !$orderedArgs) {
0693           continue;
0694         }
0695 
0696         $haveScope = false;
0697         if (isset($mixin->parent->scope)) {
0698           $haveScope = true;
0699           $mixinParentEnv = $this->pushEnv();
0700           $mixinParentEnv->storeParent = $mixin->parent->scope;
0701         }
0702 
0703         $haveArgs = false;
0704         if (isset($mixin->args)) {
0705           $haveArgs = true;
0706           $this->pushEnv();
0707           $this->zipSetArgs($mixin->args, $orderedArgs, $keywordArgs);
0708         }
0709 
0710         $oldParent = $mixin->parent;
0711         if ($mixin != $block) $mixin->parent = $block;
0712 
0713         foreach ($this->sortProps($mixin->props) as $subProp) {
0714           if ($suffix !== null &&
0715             $subProp[0] == "assign" &&
0716             is_string($subProp[1]) &&
0717             $subProp[1]{0} != $this->vPrefix)
0718           {
0719             $subProp[2] = array(
0720               'list', ' ',
0721               array($subProp[2], array('keyword', $suffix))
0722             );
0723           }
0724 
0725           $this->compileProp($subProp, $mixin, $out);
0726         }
0727 
0728         $mixin->parent = $oldParent;
0729 
0730         if ($haveArgs) $this->popEnv();
0731         if ($haveScope) $this->popEnv();
0732       }
0733 
0734       break;
0735     case 'raw':
0736       $out->lines[] = $prop[1];
0737       break;
0738     case "directive":
0739       list(, $name, $value) = $prop;
0740       $out->lines[] = "@$name " . $this->compileValue($this->reduce($value)).';';
0741       break;
0742     case "comment":
0743       $out->lines[] = $prop[1];
0744       break;
0745     case "import";
0746       list(, $importPath, $importId) = $prop;
0747       $importPath = $this->reduce($importPath);
0748 
0749       if (!isset($this->env->imports)) {
0750         $this->env->imports = array();
0751       }
0752 
0753       $result = $this->tryImport($importPath, $block, $out);
0754 
0755       $this->env->imports[$importId] = $result === false ?
0756         array(false, "@import " . $this->compileValue($importPath).";") :
0757         $result;
0758 
0759       break;
0760     case "import_mixin":
0761       list(,$importId) = $prop;
0762       $import = $this->env->imports[$importId];
0763       if ($import[0] === false) {
0764         if (isset($import[1])) {
0765           $out->lines[] = $import[1];
0766         }
0767       } else {
0768         list(, $bottom, $parser, $importDir) = $import;
0769         $this->compileImportedProps($bottom, $block, $out, $parser, $importDir);
0770       }
0771 
0772       break;
0773     default:
0774       $this->throwError("unknown op: {$prop[0]}\n");
0775     }
0776   }
0777 
0778 
0779   /**
0780    * Compiles a primitive value into a CSS property value.
0781    *
0782    * Values in lessphp are typed by being wrapped in arrays, their format is
0783    * typically:
0784    *
0785    *     array(type, contents [, additional_contents]*)
0786    *
0787    * The input is expected to be reduced. This function will not work on
0788    * things like expressions and variables.
0789    */
0790   protected function compileValue($value) {
0791     switch ($value[0]) {
0792     case 'list':
0793       // [1] - delimiter
0794       // [2] - array of values
0795       return implode($value[1], array_map(array($this, 'compileValue'), $value[2]));
0796     case 'raw_color':
0797       if (!empty($this->formatter->compressColors)) {
0798         return $this->compileValue($this->coerceColor($value));
0799       }
0800       return $value[1];
0801     case 'keyword':
0802       // [1] - the keyword
0803       return $value[1];
0804     case 'number':
0805       list(, $num, $unit) = $value;
0806       // [1] - the number
0807       // [2] - the unit
0808       if ($this->numberPrecision !== null) {
0809         $num = round($num, $this->numberPrecision);
0810       }
0811       return $num . $unit;
0812     case 'string':
0813       // [1] - contents of string (includes quotes)
0814       list(, $delim, $content) = $value;
0815       foreach ($content as &$part) {
0816         if (is_array($part)) {
0817           $part = $this->compileValue($part);
0818         }
0819       }
0820       return $delim . implode($content) . $delim;
0821     case 'color':
0822       // [1] - red component (either number or a %)
0823       // [2] - green component
0824       // [3] - blue component
0825       // [4] - optional alpha component
0826       list(, $r, $g, $b) = $value;
0827       $r = round($r);
0828       $g = round($g);
0829       $b = round($b);
0830 
0831       if (count($value) == 5 && $value[4] != 1) { // rgba
0832         return 'rgba('.$r.','.$g.','.$b.','.$value[4].')';
0833       }
0834 
0835       $h = sprintf("#%02x%02x%02x", $r, $g, $b);
0836 
0837       if (!empty($this->formatter->compressColors)) {
0838         // Converting hex color to short notation (e.g. #003399 to #039)
0839         if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
0840           $h = '#' . $h[1] . $h[3] . $h[5];
0841         }
0842       }
0843 
0844       return $h;
0845 
0846     case 'function':
0847       list(, $name, $args) = $value;
0848       return $name.'('.$this->compileValue($args).')';
0849     default: // assumed to be unit
0850       $this->throwError("unknown value type: $value[0]");
0851     }
0852   }
0853 
0854   protected function lib_pow($args) {
0855     list($base, $exp) = $this->assertArgs($args, 2, "pow");
0856     return pow($this->assertNumber($base), $this->assertNumber($exp));
0857   }
0858 
0859   protected function lib_pi() {
0860     return pi();
0861   }
0862 
0863   protected function lib_mod($args) {
0864     list($a, $b) = $this->assertArgs($args, 2, "mod");
0865     return $this->assertNumber($a) % $this->assertNumber($b);
0866   }
0867 
0868   protected function lib_tan($num) {
0869     return tan($this->assertNumber($num));
0870   }
0871 
0872   protected function lib_sin($num) {
0873     return sin($this->assertNumber($num));
0874   }
0875 
0876   protected function lib_cos($num) {
0877     return cos($this->assertNumber($num));
0878   }
0879 
0880   protected function lib_atan($num) {
0881     $num = atan($this->assertNumber($num));
0882     return array("number", $num, "rad");
0883   }
0884 
0885   protected function lib_asin($num) {
0886     $num = asin($this->assertNumber($num));
0887     return array("number", $num, "rad");
0888   }
0889 
0890   protected function lib_acos($num) {
0891     $num = acos($this->assertNumber($num));
0892     return array("number", $num, "rad");
0893   }
0894 
0895   protected function lib_sqrt($num) {
0896     return sqrt($this->assertNumber($num));
0897   }
0898 
0899   protected function lib_extract($value) {
0900     list($list, $idx) = $this->assertArgs($value, 2, "extract");
0901     $idx = $this->assertNumber($idx);
0902     // 1 indexed
0903     if ($list[0] == "list" && isset($list[2][$idx - 1])) {
0904       return $list[2][$idx - 1];
0905     }
0906   }
0907 
0908   protected function lib_isnumber($value) {
0909     return $this->toBool($value[0] == "number");
0910   }
0911 
0912   protected function lib_isstring($value) {
0913     return $this->toBool($value[0] == "string");
0914   }
0915 
0916   protected function lib_iscolor($value) {
0917     return $this->toBool($this->coerceColor($value));
0918   }
0919 
0920   protected function lib_iskeyword($value) {
0921     return $this->toBool($value[0] == "keyword");
0922   }
0923 
0924   protected function lib_ispixel($value) {
0925     return $this->toBool($value[0] == "number" && $value[2] == "px");
0926   }
0927 
0928   protected function lib_ispercentage($value) {
0929     return $this->toBool($value[0] == "number" && $value[2] == "%");
0930   }
0931 
0932   protected function lib_isem($value) {
0933     return $this->toBool($value[0] == "number" && $value[2] == "em");
0934   }
0935 
0936   protected function lib_isrem($value) {
0937     return $this->toBool($value[0] == "number" && $value[2] == "rem");
0938   }
0939 
0940   protected function lib_rgbahex($color) {
0941     $color = $this->coerceColor($color);
0942     if (is_null($color))
0943       $this->throwError("color expected for rgbahex");
0944 
0945     return sprintf("#%02x%02x%02x%02x",
0946       isset($color[4]) ? $color[4]*255 : 255,
0947       $color[1],$color[2], $color[3]);
0948   }
0949 
0950   protected function lib_argb($color){
0951     return $this->lib_rgbahex($color);
0952   }
0953 
0954   // utility func to unquote a string
0955   protected function lib_e($arg) {
0956     switch ($arg[0]) {
0957       case "list":
0958         $items = $arg[2];
0959         if (isset($items[0])) {
0960           return $this->lib_e($items[0]);
0961         }
0962         return self::$defaultValue;
0963       case "string":
0964         $arg[1] = "";
0965         return $arg;
0966       case "keyword":
0967         return $arg;
0968       default:
0969         return array("keyword", $this->compileValue($arg));
0970     }
0971   }
0972 
0973   protected function lib__sprintf($args) {
0974     if ($args[0] != "list") return $args;
0975     $values = $args[2];
0976     $string = array_shift($values);
0977     $template = $this->compileValue($this->lib_e($string));
0978 
0979     $i = 0;
0980     if (preg_match_all('/%[dsa]/', $template, $m)) {
0981       foreach ($m[0] as $match) {
0982         $val = isset($values[$i]) ?
0983           $this->reduce($values[$i]) : array('keyword', '');
0984 
0985         // lessjs compat, renders fully expanded color, not raw color
0986         if ($color = $this->coerceColor($val)) {
0987           $val = $color;
0988         }
0989 
0990         $i++;
0991         $rep = $this->compileValue($this->lib_e($val));
0992         $template = preg_replace('/'.self::preg_quote($match).'/',
0993           $rep, $template, 1);
0994       }
0995     }
0996 
0997     $d = $string[0] == "string" ? $string[1] : '"';
0998     return array("string", $d, array($template));
0999   }
1000 
1001   protected function lib_floor($arg) {
1002     $value = $this->assertNumber($arg);
1003     return array("number", floor($value), $arg[2]);
1004   }
1005 
1006   protected function lib_ceil($arg) {
1007     $value = $this->assertNumber($arg);
1008     return array("number", ceil($value), $arg[2]);
1009   }
1010 
1011   protected function lib_round($arg) {
1012     $value = $this->assertNumber($arg);
1013     return array("number", round($value), $arg[2]);
1014   }
1015 
1016   protected function lib_unit($arg) {
1017     if ($arg[0] == "list") {
1018       list($number, $newUnit) = $arg[2];
1019       return array("number", $this->assertNumber($number),
1020         $this->compileValue($this->lib_e($newUnit)));
1021     } else {
1022       return array("number", $this->assertNumber($arg), "");
1023     }
1024   }
1025 
1026   /**
1027    * Helper function to get arguments for color manipulation functions.
1028    * takes a list that contains a color like thing and a percentage
1029    */
1030   protected function colorArgs($args) {
1031     if ($args[0] != 'list' || count($args[2]) < 2) {
1032       return array(array('color', 0, 0, 0), 0);
1033     }
1034     list($color, $delta) = $args[2];
1035     $color = $this->assertColor($color);
1036     $delta = floatval($delta[1]);
1037 
1038     return array($color, $delta);
1039   }
1040 
1041   protected function lib_darken($args) {
1042     list($color, $delta) = $this->colorArgs($args);
1043 
1044     $hsl = $this->toHSL($color);
1045     $hsl[3] = $this->clamp($hsl[3] - $delta, 100);
1046     return $this->toRGB($hsl);
1047   }
1048 
1049   protected function lib_lighten($args) {
1050     list($color, $delta) = $this->colorArgs($args);
1051 
1052     $hsl = $this->toHSL($color);
1053     $hsl[3] = $this->clamp($hsl[3] + $delta, 100);
1054     return $this->toRGB($hsl);
1055   }
1056 
1057   protected function lib_saturate($args) {
1058     list($color, $delta) = $this->colorArgs($args);
1059 
1060     $hsl = $this->toHSL($color);
1061     $hsl[2] = $this->clamp($hsl[2] + $delta, 100);
1062     return $this->toRGB($hsl);
1063   }
1064 
1065   protected function lib_desaturate($args) {
1066     list($color, $delta) = $this->colorArgs($args);
1067 
1068     $hsl = $this->toHSL($color);
1069     $hsl[2] = $this->clamp($hsl[2] - $delta, 100);
1070     return $this->toRGB($hsl);
1071   }
1072 
1073   protected function lib_spin($args) {
1074     list($color, $delta) = $this->colorArgs($args);
1075 
1076     $hsl = $this->toHSL($color);
1077 
1078     $hsl[1] = $hsl[1] + $delta % 360;
1079     if ($hsl[1] < 0) $hsl[1] += 360;
1080 
1081     return $this->toRGB($hsl);
1082   }
1083 
1084   protected function lib_fadeout($args) {
1085     list($color, $delta) = $this->colorArgs($args);
1086     $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) - $delta/100);
1087     return $color;
1088   }
1089 
1090   protected function lib_fadein($args) {
1091     list($color, $delta) = $this->colorArgs($args);
1092     $color[4] = $this->clamp((isset($color[4]) ? $color[4] : 1) + $delta/100);
1093     return $color;
1094   }
1095 
1096   protected function lib_hue($color) {
1097     $hsl = $this->toHSL($this->assertColor($color));
1098     return round($hsl[1]);
1099   }
1100 
1101   protected function lib_saturation($color) {
1102     $hsl = $this->toHSL($this->assertColor($color));
1103     return round($hsl[2]);
1104   }
1105 
1106   protected function lib_lightness($color) {
1107     $hsl = $this->toHSL($this->assertColor($color));
1108     return round($hsl[3]);
1109   }
1110 
1111   // get the alpha of a color
1112   // defaults to 1 for non-colors or colors without an alpha
1113   protected function lib_alpha($value) {
1114     if (!is_null($color = $this->coerceColor($value))) {
1115       return isset($color[4]) ? $color[4] : 1;
1116     }
1117   }
1118 
1119   // set the alpha of the color
1120   protected function lib_fade($args) {
1121     list($color, $alpha) = $this->colorArgs($args);
1122     $color[4] = $this->clamp($alpha / 100.0);
1123     return $color;
1124   }
1125 
1126   protected function lib_percentage($arg) {
1127     $num = $this->assertNumber($arg);
1128     return array("number", $num*100, "%");
1129   }
1130 
1131   // mixes two colors by weight
1132   // mix(@color1, @color2, [@weight: 50%]);
1133   // http://sass-lang.com/docs/yardoc/Sass/Script/Functions.html#mix-instance_method
1134   protected function lib_mix($args) {
1135     if ($args[0] != "list" || count($args[2]) < 2)
1136       $this->throwError("mix expects (color1, color2, weight)");
1137 
1138     list($first, $second) = $args[2];
1139     $first = $this->assertColor($first);
1140     $second = $this->assertColor($second);
1141 
1142     $first_a = $this->lib_alpha($first);
1143     $second_a = $this->lib_alpha($second);
1144 
1145     if (isset($args[2][2])) {
1146       $weight = $args[2][2][1] / 100.0;
1147     } else {
1148       $weight = 0.5;
1149     }
1150 
1151     $w = $weight * 2 - 1;
1152     $a = $first_a - $second_a;
1153 
1154     $w1 = (($w * $a == -1 ? $w : ($w + $a)/(1 + $w * $a)) + 1) / 2.0;
1155     $w2 = 1.0 - $w1;
1156 
1157     $new = array('color',
1158       $w1 * $first[1] + $w2 * $second[1],
1159       $w1 * $first[2] + $w2 * $second[2],
1160       $w1 * $first[3] + $w2 * $second[3],
1161     );
1162 
1163     if ($first_a != 1.0 || $second_a != 1.0) {
1164       $new[] = $first_a * $weight + $second_a * ($weight - 1);
1165     }
1166 
1167     return $this->fixColor($new);
1168   }
1169 
1170   protected function lib_contrast($args) {
1171     if ($args[0] != 'list' || count($args[2]) < 3) {
1172       return array(array('color', 0, 0, 0), 0);
1173     }
1174 
1175     list($inputColor, $darkColor, $lightColor) = $args[2];
1176 
1177     $inputColor = $this->assertColor($inputColor);
1178     $darkColor = $this->assertColor($darkColor);
1179     $lightColor = $this->assertColor($lightColor);
1180     $hsl = $this->toHSL($inputColor);
1181 
1182     if ($hsl[3] > 50) {
1183       return $darkColor;
1184     }
1185 
1186     return $lightColor;
1187   }
1188 
1189   protected function assertColor($value, $error = "expected color value") {
1190     $color = $this->coerceColor($value);
1191     if (is_null($color)) $this->throwError($error);
1192     return $color;
1193   }
1194 
1195   protected function assertNumber($value, $error = "expecting number") {
1196     if ($value[0] == "number") return $value[1];
1197     $this->throwError($error);
1198   }
1199 
1200   protected function assertArgs($value, $expectedArgs, $name="") {
1201     if ($expectedArgs == 1) {
1202       return $value;
1203     } else {
1204       if ($value[0] !== "list" || $value[1] != ",") $this->throwError("expecting list");
1205       $values = $value[2];
1206       $numValues = count($values);
1207       if ($expectedArgs != $numValues) {
1208         if ($name) {
1209           $name = $name . ": ";
1210         }
1211 
1212         $this->throwError("${name}expecting $expectedArgs arguments, got $numValues");
1213       }
1214 
1215       return $values;
1216     }
1217   }
1218 
1219   protected function toHSL($color) {
1220     if ($color[0] == 'hsl') return $color;
1221 
1222     $r = $color[1] / 255;
1223     $g = $color[2] / 255;
1224     $b = $color[3] / 255;
1225 
1226     $min = min($r, $g, $b);
1227     $max = max($r, $g, $b);
1228 
1229     $L = ($min + $max) / 2;
1230     if ($min == $max) {
1231       $S = $H = 0;
1232     } else {
1233       if ($L < 0.5)
1234         $S = ($max - $min)/($max + $min);
1235       else
1236         $S = ($max - $min)/(2.0 - $max - $min);
1237 
1238       if ($r == $max) $H = ($g - $b)/($max - $min);
1239       elseif ($g == $max) $H = 2.0 + ($b - $r)/($max - $min);
1240       elseif ($b == $max) $H = 4.0 + ($r - $g)/($max - $min);
1241 
1242     }
1243 
1244     $out = array('hsl',
1245       ($H < 0 ? $H + 6 : $H)*60,
1246       $S*100,
1247       $L*100,
1248     );
1249 
1250     if (count($color) > 4) $out[] = $color[4]; // copy alpha
1251     return $out;
1252   }
1253 
1254   protected function toRGB_helper($comp, $temp1, $temp2) {
1255     if ($comp < 0) $comp += 1.0;
1256     elseif ($comp > 1) $comp -= 1.0;
1257 
1258     if (6 * $comp < 1) return $temp1 + ($temp2 - $temp1) * 6 * $comp;
1259     if (2 * $comp < 1) return $temp2;
1260     if (3 * $comp < 2) return $temp1 + ($temp2 - $temp1)*((2/3) - $comp) * 6;
1261 
1262     return $temp1;
1263   }
1264 
1265   /**
1266    * Converts a hsl array into a color value in rgb.
1267    * Expects H to be in range of 0 to 360, S and L in 0 to 100
1268    */
1269   protected function toRGB($color) {
1270     if ($color[0] == 'color') return $color;
1271 
1272     $H = $color[1] / 360;
1273     $S = $color[2] / 100;
1274     $L = $color[3] / 100;
1275 
1276     if ($S == 0) {
1277       $r = $g = $b = $L;
1278     } else {
1279       $temp2 = $L < 0.5 ?
1280         $L*(1.0 + $S) :
1281         $L + $S - $L * $S;
1282 
1283       $temp1 = 2.0 * $L - $temp2;
1284 
1285       $r = $this->toRGB_helper($H + 1/3, $temp1, $temp2);
1286       $g = $this->toRGB_helper($H, $temp1, $temp2);
1287       $b = $this->toRGB_helper($H - 1/3, $temp1, $temp2);
1288     }
1289 
1290     // $out = array('color', round($r*255), round($g*255), round($b*255));
1291     $out = array('color', $r*255, $g*255, $b*255);
1292     if (count($color) > 4) $out[] = $color[4]; // copy alpha
1293     return $out;
1294   }
1295 
1296   protected function clamp($v, $max = 1, $min = 0) {
1297     return min($max, max($min, $v));
1298   }
1299 
1300   /**
1301    * Convert the rgb, rgba, hsl color literals of function type
1302    * as returned by the parser into values of color type.
1303    */
1304   protected function funcToColor($func) {
1305     $fname = $func[1];
1306     if ($func[2][0] != 'list') return false; // need a list of arguments
1307     $rawComponents = $func[2][2];
1308 
1309     if ($fname == 'hsl' || $fname == 'hsla') {
1310       $hsl = array('hsl');
1311       $i = 0;
1312       foreach ($rawComponents as $c) {
1313         $val = $this->reduce($c);
1314         $val = isset($val[1]) ? floatval($val[1]) : 0;
1315 
1316         if ($i == 0) $clamp = 360;
1317         elseif ($i < 3) $clamp = 100;
1318         else $clamp = 1;
1319 
1320         $hsl[] = $this->clamp($val, $clamp);
1321         $i++;
1322       }
1323 
1324       while (count($hsl) < 4) $hsl[] = 0;
1325       return $this->toRGB($hsl);
1326 
1327     } elseif ($fname == 'rgb' || $fname == 'rgba') {
1328       $components = array();
1329       $i = 1;
1330       foreach ($rawComponents as $c) {
1331         $c = $this->reduce($c);
1332         if ($i < 4) {
1333           if ($c[0] == "number" && $c[2] == "%") {
1334             $components[] = 255 * ($c[1] / 100);
1335           } else {
1336             $components[] = floatval($c[1]);
1337           }
1338         } elseif ($i == 4) {
1339           if ($c[0] == "number" && $c[2] == "%") {
1340             $components[] = 1.0 * ($c[1] / 100);
1341           } else {
1342             $components[] = floatval($c[1]);
1343           }
1344         } else break;
1345 
1346         $i++;
1347       }
1348       while (count($components) < 3) $components[] = 0;
1349       array_unshift($components, 'color');
1350       return $this->fixColor($components);
1351     }
1352 
1353     return false;
1354   }
1355 
1356   protected function reduce($value, $forExpression = false) {
1357     switch ($value[0]) {
1358     case "interpolate":
1359       $reduced = $this->reduce($value[1]);
1360       $var = $this->compileValue($reduced);
1361       $res = $this->reduce(array("variable", $this->vPrefix . $var));
1362 
1363       if ($res[0] == "raw_color") {
1364         $res = $this->coerceColor($res);
1365       }
1366 
1367       if (empty($value[2])) $res = $this->lib_e($res);
1368 
1369       return $res;
1370     case "variable":
1371       $key = $value[1];
1372       if (is_array($key)) {
1373         $key = $this->reduce($key);
1374         $key = $this->vPrefix . $this->compileValue($this->lib_e($key));
1375       }
1376 
1377       $seen =& $this->env->seenNames;
1378 
1379       if (!empty($seen[$key])) {
1380         $this->throwError("infinite loop detected: $key");
1381       }
1382 
1383       $seen[$key] = true;
1384       $out = $this->reduce($this->get($key, self::$defaultValue));
1385       $seen[$key] = false;
1386       return $out;
1387     case "list":
1388       foreach ($value[2] as &$item) {
1389         $item = $this->reduce($item, $forExpression);
1390       }
1391       return $value;
1392     case "expression":
1393       return $this->evaluate($value);
1394     case "string":
1395       foreach ($value[2] as &$part) {
1396         if (is_array($part)) {
1397           $strip = $part[0] == "variable";
1398           $part = $this->reduce($part);
1399           if ($strip) $part = $this->lib_e($part);
1400         }
1401       }
1402       return $value;
1403     case "escape":
1404       list(,$inner) = $value;
1405       return $this->lib_e($this->reduce($inner));
1406     case "function":
1407       $color = $this->funcToColor($value);
1408       if ($color) return $color;
1409 
1410       list(, $name, $args) = $value;
1411       if ($name == "%") $name = "_sprintf";
1412       $f = isset($this->libFunctions[$name]) ?
1413         $this->libFunctions[$name] : array($this, 'lib_'.$name);
1414 
1415       if (is_callable($f)) {
1416         if ($args[0] == 'list')
1417           $args = self::compressList($args[2], $args[1]);
1418 
1419         $ret = call_user_func($f, $this->reduce($args, true), $this);
1420 
1421         if (is_null($ret)) {
1422           return array("string", "", array(
1423             $name, "(", $args, ")"
1424           ));
1425         }
1426 
1427         // convert to a typed value if the result is a php primitive
1428         if (is_numeric($ret)) $ret = array('number', $ret, "");
1429         elseif (!is_array($ret)) $ret = array('keyword', $ret);
1430 
1431         return $ret;
1432       }
1433 
1434       // plain function, reduce args
1435       $value[2] = $this->reduce($value[2]);
1436       return $value;
1437     case "unary":
1438       list(, $op, $exp) = $value;
1439       $exp = $this->reduce($exp);
1440 
1441       if ($exp[0] == "number") {
1442         switch ($op) {
1443         case "+":
1444           return $exp;
1445         case "-":
1446           $exp[1] *= -1;
1447           return $exp;
1448         }
1449       }
1450       return array("string", "", array($op, $exp));
1451     }
1452 
1453     if ($forExpression) {
1454       switch ($value[0]) {
1455       case "keyword":
1456         if ($color = $this->coerceColor($value)) {
1457           return $color;
1458         }
1459         break;
1460       case "raw_color":
1461         return $this->coerceColor($value);
1462       }
1463     }
1464 
1465     return $value;
1466   }
1467 
1468 
1469   // coerce a value for use in color operation
1470   protected function coerceColor($value) {
1471     switch($value[0]) {
1472       case 'color': return $value;
1473       case 'raw_color':
1474         $c = array("color", 0, 0, 0);
1475         $colorStr = substr($value[1], 1);
1476         $num = hexdec($colorStr);
1477         $width = strlen($colorStr) == 3 ? 16 : 256;
1478 
1479         for ($i = 3; $i > 0; $i--) { // 3 2 1
1480           $t = $num % $width;
1481           $num /= $width;
1482 
1483           $c[$i] = $t * (256/$width) + $t * floor(16/$width);
1484         }
1485 
1486         return $c;
1487       case 'keyword':
1488         $name = $value[1];
1489         if (isset(self::$cssColors[$name])) {
1490           $rgba = explode(',', self::$cssColors[$name]);
1491 
1492           if(isset($rgba[3]))
1493             return array('color', $rgba[0], $rgba[1], $rgba[2], $rgba[3]);
1494 
1495           return array('color', $rgba[0], $rgba[1], $rgba[2]);
1496         }
1497         return null;
1498     }
1499   }
1500 
1501   // make something string like into a string
1502   protected function coerceString($value) {
1503     switch ($value[0]) {
1504     case "string":
1505       return $value;
1506     case "keyword":
1507       return array("string", "", array($value[1]));
1508     }
1509     return null;
1510   }
1511 
1512   // turn list of length 1 into value type
1513   protected function flattenList($value) {
1514     if ($value[0] == "list" && count($value[2]) == 1) {
1515       return $this->flattenList($value[2][0]);
1516     }
1517     return $value;
1518   }
1519 
1520   protected function toBool($a) {
1521     if ($a) return self::$TRUE;
1522     else return self::$FALSE;
1523   }
1524 
1525   // evaluate an expression
1526   protected function evaluate($exp) {
1527     list(, $op, $left, $right, $whiteBefore, $whiteAfter) = $exp;
1528 
1529     $left = $this->reduce($left, true);
1530     $right = $this->reduce($right, true);
1531 
1532     if ($leftColor = $this->coerceColor($left)) {
1533       $left = $leftColor;
1534     }
1535 
1536     if ($rightColor = $this->coerceColor($right)) {
1537       $right = $rightColor;
1538     }
1539 
1540     $ltype = $left[0];
1541     $rtype = $right[0];
1542 
1543     // operators that work on all types
1544     if ($op == "and") {
1545       return $this->toBool($left == self::$TRUE && $right == self::$TRUE);
1546     }
1547 
1548     if ($op == "=") {
1549       return $this->toBool($this->eq($left, $right) );
1550     }
1551 
1552     if ($op == "+" && !is_null($str = $this->stringConcatenate($left, $right))) {
1553       return $str;
1554     }
1555 
1556     // type based operators
1557     $fname = "op_${ltype}_${rtype}";
1558     if (is_callable(array($this, $fname))) {
1559       $out = $this->$fname($op, $left, $right);
1560       if (!is_null($out)) return $out;
1561     }
1562 
1563     // make the expression look it did before being parsed
1564     $paddedOp = $op;
1565     if ($whiteBefore) $paddedOp = " " . $paddedOp;
1566     if ($whiteAfter) $paddedOp .= " ";
1567 
1568     return array("string", "", array($left, $paddedOp, $right));
1569   }
1570 
1571   protected function stringConcatenate($left, $right) {
1572     if ($strLeft = $this->coerceString($left)) {
1573       if ($right[0] == "string") {
1574         $right[1] = "";
1575       }
1576       $strLeft[2][] = $right;
1577       return $strLeft;
1578     }
1579 
1580     if ($strRight = $this->coerceString($right)) {
1581       array_unshift($strRight[2], $left);
1582       return $strRight;
1583     }
1584   }
1585 
1586 
1587   // make sure a color's components don't go out of bounds
1588   protected function fixColor($c) {
1589     foreach (range(1, 3) as $i) {
1590       if ($c[$i] < 0) $c[$i] = 0;
1591       if ($c[$i] > 255) $c[$i] = 255;
1592     }
1593 
1594     return $c;
1595   }
1596 
1597   protected function op_number_color($op, $lft, $rgt) {
1598     if ($op == '+' || $op == '*') {
1599       return $this->op_color_number($op, $rgt, $lft);
1600     }
1601   }
1602 
1603   protected function op_color_number($op, $lft, $rgt) {
1604     if ($rgt[0] == '%') $rgt[1] /= 100;
1605 
1606     return $this->op_color_color($op, $lft,
1607       array_fill(1, count($lft) - 1, $rgt[1]));
1608   }
1609 
1610   protected function op_color_color($op, $left, $right) {
1611     $out = array('color');
1612     $max = count($left) > count($right) ? count($left) : count($right);
1613     foreach (range(1, $max - 1) as $i) {
1614       $lval = isset($left[$i]) ? $left[$i] : 0;
1615       $rval = isset($right[$i]) ? $right[$i] : 0;
1616       switch ($op) {
1617       case '+':
1618         $out[] = $lval + $rval;
1619         break;
1620       case '-':
1621         $out[] = $lval - $rval;
1622         break;
1623       case '*':
1624         $out[] = $lval * $rval;
1625         break;
1626       case '%':
1627         $out[] = $lval % $rval;
1628         break;
1629       case '/':
1630         if ($rval == 0) $this->throwError("evaluate error: can't divide by zero");
1631         $out[] = $lval / $rval;
1632         break;
1633       default:
1634         $this->throwError('evaluate error: color op number failed on op '.$op);
1635       }
1636     }
1637     return $this->fixColor($out);
1638   }
1639 
1640   function lib_red($color){
1641     $color = $this->coerceColor($color);
1642     if (is_null($color)) {
1643       $this->throwError('color expected for red()');
1644     }
1645 
1646     return $color[1];
1647   }
1648 
1649   function lib_green($color){
1650     $color = $this->coerceColor($color);
1651     if (is_null($color)) {
1652       $this->throwError('color expected for green()');
1653     }
1654 
1655     return $color[2];
1656   }
1657 
1658   function lib_blue($color){
1659     $color = $this->coerceColor($color);
1660     if (is_null($color)) {
1661       $this->throwError('color expected for blue()');
1662     }
1663 
1664     return $color[3];
1665   }
1666 
1667 
1668   // operator on two numbers
1669   protected function op_number_number($op, $left, $right) {
1670     $unit = empty($left[2]) ? $right[2] : $left[2];
1671 
1672     $value = 0;
1673     switch ($op) {
1674     case '+':
1675       $value = $left[1] + $right[1];
1676       break;
1677     case '*':
1678       $value = $left[1] * $right[1];
1679       break;
1680     case '-':
1681       $value = $left[1] - $right[1];
1682       break;
1683     case '%':
1684       $value = $left[1] % $right[1];
1685       break;
1686     case '/':
1687       if ($right[1] == 0) $this->throwError('parse error: divide by zero');
1688       $value = $left[1] / $right[1];
1689       break;
1690     case '<':
1691       return $this->toBool($left[1] < $right[1]);
1692     case '>':
1693       return $this->toBool($left[1] > $right[1]);
1694     case '>=':
1695       return $this->toBool($left[1] >= $right[1]);
1696     case '=<':
1697       return $this->toBool($left[1] <= $right[1]);
1698     default:
1699       $this->throwError('parse error: unknown number operator: '.$op);
1700     }
1701 
1702     return array("number", $value, $unit);
1703   }
1704 
1705 
1706   /* environment functions */
1707 
1708   protected function makeOutputBlock($type, $selectors = null) {
1709     $b = new stdclass;
1710     $b->lines = array();
1711     $b->children = array();
1712     $b->selectors = $selectors;
1713     $b->type = $type;
1714     $b->parent = $this->scope;
1715     return $b;
1716   }
1717 
1718   // the state of execution
1719   protected function pushEnv($block = null) {
1720     $e = new stdclass;
1721     $e->parent = $this->env;
1722     $e->store = array();
1723     $e->block = $block;
1724 
1725     $this->env = $e;
1726     return $e;
1727   }
1728 
1729   // pop something off the stack
1730   protected function popEnv() {
1731     $old = $this->env;
1732     $this->env = $this->env->parent;
1733     return $old;
1734   }
1735 
1736   // set something in the current env
1737   protected function set($name, $value) {
1738     $this->env->store[$name] = $value;
1739   }
1740 
1741 
1742   // get the highest occurrence entry for a name
1743   protected function get($name, $default=null) {
1744     $current = $this->env;
1745 
1746     $isArguments = $name == $this->vPrefix . 'arguments';
1747     while ($current) {
1748       if ($isArguments && isset($current->arguments)) {
1749         return array('list', ' ', $current->arguments);
1750       }
1751 
1752       if (isset($current->store[$name]))
1753         return $current->store[$name];
1754       else {
1755         $current = isset($current->storeParent) ?
1756           $current->storeParent : $current->parent;
1757       }
1758     }
1759 
1760     return $default;
1761   }
1762 
1763   // inject array of unparsed strings into environment as variables
1764   protected function injectVariables($args) {
1765     $this->pushEnv();
1766     $parser = new lessc_parser($this, __METHOD__);
1767     foreach ($args as $name => $strValue) {
1768       if ($name{0} != '@') $name = '@'.$name;
1769       $parser->count = 0;
1770       $parser->buffer = (string)$strValue;
1771       if (!$parser->propertyValue($value)) {
1772         throw new Exception("failed to parse passed in variable $name: $strValue");
1773       }
1774 
1775       $this->set($name, $value);
1776     }
1777   }
1778 
1779   /**
1780    * Initialize any static state, can initialize parser for a file
1781    * $opts isn't used yet
1782    */
1783   public function __construct($fname = null) {
1784     if ($fname !== null) {
1785       // used for deprecated parse method
1786       $this->_parseFile = $fname;
1787     }
1788   }
1789 
1790   public function compile($string, $name = null) {
1791     $locale = setlocale(LC_NUMERIC, 0);
1792     setlocale(LC_NUMERIC, "C");
1793 
1794     $this->parser = $this->makeParser($name);
1795     $root = $this->parser->parse($string);
1796 
1797     $this->env = null;
1798     $this->scope = null;
1799 
1800     $this->formatter = $this->newFormatter();
1801 
1802     if (!empty($this->registeredVars)) {
1803       $this->injectVariables($this->registeredVars);
1804     }
1805 
1806     $this->sourceParser = $this->parser; // used for error messages
1807     $this->compileBlock($root);
1808 
1809     ob_start();
1810     $this->formatter->block($this->scope);
1811     $out = ob_get_clean();
1812     setlocale(LC_NUMERIC, $locale);
1813     return $out;
1814   }
1815 
1816   public function compileFile($fname, $outFname = null) {
1817     if (!is_readable($fname)) {
1818       throw new Exception('load error: failed to find '.$fname);
1819     }
1820 
1821     $pi = pathinfo($fname);
1822 
1823     $oldImport = $this->importDir;
1824 
1825     $this->importDir = (array)$this->importDir;
1826     $this->importDir[] = $pi['dirname'].'/';
1827 
1828     $this->addParsedFile($fname);
1829 
1830     $out = $this->compile(file_get_contents($fname), $fname);
1831 
1832     $this->importDir = $oldImport;
1833 
1834     if ($outFname !== null) {
1835       return file_put_contents($outFname, $out);
1836     }
1837 
1838     return $out;
1839   }
1840 
1841   // compile only if changed input has changed or output doesn't exist
1842   public function checkedCompile($in, $out) {
1843     if (!is_file($out) || filemtime($in) > filemtime($out)) {
1844       $this->compileFile($in, $out);
1845       return true;
1846     }
1847     return false;
1848   }
1849 
1850   /**
1851    * Execute lessphp on a .less file or a lessphp cache structure
1852    *
1853    * The lessphp cache structure contains information about a specific
1854    * less file having been parsed. It can be used as a hint for future
1855    * calls to determine whether or not a rebuild is required.
1856    *
1857    * The cache structure contains two important keys that may be used
1858    * externally:
1859    *
1860    * compiled: The final compiled CSS
1861    * updated: The time (in seconds) the CSS was last compiled
1862    *
1863    * The cache structure is a plain-ol' PHP associative array and can
1864    * be serialized and unserialized without a hitch.
1865    *
1866    * @param mixed $in Input
1867    * @param bool $force Force rebuild?
1868    * @return array lessphp cache structure
1869    */
1870   public function cachedCompile($in, $force = false) {
1871     // assume no root
1872     $root = null;
1873 
1874     if (is_string($in)) {
1875       $root = $in;
1876     } elseif (is_array($in) and isset($in['root'])) {
1877       if ($force or ! isset($in['files'])) {
1878         // If we are forcing a recompile or if for some reason the
1879         // structure does not contain any file information we should
1880         // specify the root to trigger a rebuild.
1881         $root = $in['root'];
1882       } elseif (isset($in['files']) and is_array($in['files'])) {
1883         foreach ($in['files'] as $fname => $ftime ) {
1884           if (!file_exists($fname) or filemtime($fname) > $ftime) {
1885             // One of the files we knew about previously has changed
1886             // so we should look at our incoming root again.
1887             $root = $in['root'];
1888             break;
1889           }
1890         }
1891       }
1892     } else {
1893       // TODO: Throw an exception? We got neither a string nor something
1894       // that looks like a compatible lessphp cache structure.
1895       return null;
1896     }
1897 
1898     if ($root !== null) {
1899       // If we have a root value which means we should rebuild.
1900       $out = array();
1901       $out['root'] = $root;
1902       $out['compiled'] = $this->compileFile($root);
1903       $out['files'] = $this->allParsedFiles();
1904       $out['updated'] = time();
1905       return $out;
1906     } else {
1907       // No changes, pass back the structure
1908       // we were given initially.
1909       return $in;
1910     }
1911 
1912   }
1913 
1914   // parse and compile buffer
1915   // This is deprecated
1916   public function parse($str = null, $initialVariables = null) {
1917     if (is_array($str)) {
1918       $initialVariables = $str;
1919       $str = null;
1920     }
1921 
1922     $oldVars = $this->registeredVars;
1923     if ($initialVariables !== null) {
1924       $this->setVariables($initialVariables);
1925     }
1926 
1927     if ($str == null) {
1928       if (empty($this->_parseFile)) {
1929         throw new exception("nothing to parse");
1930       }
1931 
1932       $out = $this->compileFile($this->_parseFile);
1933     } else {
1934       $out = $this->compile($str);
1935     }
1936 
1937     $this->registeredVars = $oldVars;
1938     return $out;
1939   }
1940 
1941   protected function makeParser($name) {
1942     $parser = new lessc_parser($this, $name);
1943     $parser->writeComments = $this->preserveComments;
1944 
1945     return $parser;
1946   }
1947 
1948   public function setFormatter($name) {
1949     $this->formatterName = $name;
1950   }
1951 
1952   protected function newFormatter() {
1953     $className = "lessc_formatter_lessjs";
1954     if (!empty($this->formatterName)) {
1955       if (!is_string($this->formatterName))
1956         return $this->formatterName;
1957       $className = "lessc_formatter_$this->formatterName";
1958     }
1959 
1960     return new $className;
1961   }
1962 
1963   public function setPreserveComments($preserve) {
1964     $this->preserveComments = $preserve;
1965   }
1966 
1967   public function registerFunction($name, $func) {
1968     $this->libFunctions[$name] = $func;
1969   }
1970 
1971   public function unregisterFunction($name) {
1972     unset($this->libFunctions[$name]);
1973   }
1974 
1975   public function setVariables($variables) {
1976     $this->registeredVars = array_merge($this->registeredVars, $variables);
1977   }
1978 
1979   public function unsetVariable($name) {
1980     unset($this->registeredVars[$name]);
1981   }
1982 
1983   public function setImportDir($dirs) {
1984     $this->importDir = (array)$dirs;
1985   }
1986 
1987   public function addImportDir($dir) {
1988     $this->importDir = (array)$this->importDir;
1989     $this->importDir[] = $dir;
1990   }
1991 
1992   public function allParsedFiles() {
1993     return $this->allParsedFiles;
1994   }
1995 
1996   protected function addParsedFile($file) {
1997     $this->allParsedFiles[realpath($file)] = filemtime($file);
1998   }
1999 
2000   /**
2001    * Uses the current value of $this->count to show line and line number
2002    */
2003   protected function throwError($msg = null) {
2004     if ($this->sourceLoc >= 0) {
2005       $this->sourceParser->throwError($msg, $this->sourceLoc);
2006     }
2007     throw new exception($msg);
2008   }
2009 
2010   // compile file $in to file $out if $in is newer than $out
2011   // returns true when it compiles, false otherwise
2012   public static function ccompile($in, $out, $less = null) {
2013     if ($less === null) {
2014       $less = new self;
2015     }
2016     return $less->checkedCompile($in, $out);
2017   }
2018 
2019   public static function cexecute($in, $force = false, $less = null) {
2020     if ($less === null) {
2021       $less = new self;
2022     }
2023     return $less->cachedCompile($in, $force);
2024   }
2025 
2026   static protected $cssColors = array(
2027     'aliceblue' => '240,248,255',
2028     'antiquewhite' => '250,235,215',
2029     'aqua' => '0,255,255',
2030     'aquamarine' => '127,255,212',
2031     'azure' => '240,255,255',
2032     'beige' => '245,245,220',
2033     'bisque' => '255,228,196',
2034     'black' => '0,0,0',
2035     'blanchedalmond' => '255,235,205',
2036     'blue' => '0,0,255',
2037     'blueviolet' => '138,43,226',
2038     'brown' => '165,42,42',
2039     'burlywood' => '222,184,135',
2040     'cadetblue' => '95,158,160',
2041     'chartreuse' => '127,255,0',
2042     'chocolate' => '210,105,30',
2043     'coral' => '255,127,80',
2044     'cornflowerblue' => '100,149,237',
2045     'cornsilk' => '255,248,220',
2046     'crimson' => '220,20,60',
2047     'cyan' => '0,255,255',
2048     'darkblue' => '0,0,139',
2049     'darkcyan' => '0,139,139',
2050     'darkgoldenrod' => '184,134,11',
2051     'darkgray' => '169,169,169',
2052     'darkgreen' => '0,100,0',
2053     'darkgrey' => '169,169,169',
2054     'darkkhaki' => '189,183,107',
2055     'darkmagenta' => '139,0,139',
2056     'darkolivegreen' => '85,107,47',
2057     'darkorange' => '255,140,0',
2058     'darkorchid' => '153,50,204',
2059     'darkred' => '139,0,0',
2060     'darksalmon' => '233,150,122',
2061     'darkseagreen' => '143,188,143',
2062     'darkslateblue' => '72,61,139',
2063     'darkslategray' => '47,79,79',
2064     'darkslategrey' => '47,79,79',
2065     'darkturquoise' => '0,206,209',
2066     'darkviolet' => '148,0,211',
2067     'deeppink' => '255,20,147',
2068     'deepskyblue' => '0,191,255',
2069     'dimgray' => '105,105,105',
2070     'dimgrey' => '105,105,105',
2071     'dodgerblue' => '30,144,255',
2072     'firebrick' => '178,34,34',
2073     'floralwhite' => '255,250,240',
2074     'forestgreen' => '34,139,34',
2075     'fuchsia' => '255,0,255',
2076     'gainsboro' => '220,220,220',
2077     'ghostwhite' => '248,248,255',
2078     'gold' => '255,215,0',
2079     'goldenrod' => '218,165,32',
2080     'gray' => '128,128,128',
2081     'green' => '0,128,0',
2082     'greenyellow' => '173,255,47',
2083     'grey' => '128,128,128',
2084     'honeydew' => '240,255,240',
2085     'hotpink' => '255,105,180',
2086     'indianred' => '205,92,92',
2087     'indigo' => '75,0,130',
2088     'ivory' => '255,255,240',
2089     'khaki' => '240,230,140',
2090     'lavender' => '230,230,250',
2091     'lavenderblush' => '255,240,245',
2092     'lawngreen' => '124,252,0',
2093     'lemonchiffon' => '255,250,205',
2094     'lightblue' => '173,216,230',
2095     'lightcoral' => '240,128,128',
2096     'lightcyan' => '224,255,255',
2097     'lightgoldenrodyellow' => '250,250,210',
2098     'lightgray' => '211,211,211',
2099     'lightgreen' => '144,238,144',
2100     'lightgrey' => '211,211,211',
2101     'lightpink' => '255,182,193',
2102     'lightsalmon' => '255,160,122',
2103     'lightseagreen' => '32,178,170',
2104     'lightskyblue' => '135,206,250',
2105     'lightslategray' => '119,136,153',
2106     'lightslategrey' => '119,136,153',
2107     'lightsteelblue' => '176,196,222',
2108     'lightyellow' => '255,255,224',
2109     'lime' => '0,255,0',
2110     'limegreen' => '50,205,50',
2111     'linen' => '250,240,230',
2112     'magenta' => '255,0,255',
2113     'maroon' => '128,0,0',
2114     'mediumaquamarine' => '102,205,170',
2115     'mediumblue' => '0,0,205',
2116     'mediumorchid' => '186,85,211',
2117     'mediumpurple' => '147,112,219',
2118     'mediumseagreen' => '60,179,113',
2119     'mediumslateblue' => '123,104,238',
2120     'mediumspringgreen' => '0,250,154',
2121     'mediumturquoise' => '72,209,204',
2122     'mediumvioletred' => '199,21,133',
2123     'midnightblue' => '25,25,112',
2124     'mintcream' => '245,255,250',
2125     'mistyrose' => '255,228,225',
2126     'moccasin' => '255,228,181',
2127     'navajowhite' => '255,222,173',
2128     'navy' => '0,0,128',
2129     'oldlace' => '253,245,230',
2130     'olive' => '128,128,0',
2131     'olivedrab' => '107,142,35',
2132     'orange' => '255,165,0',
2133     'orangered' => '255,69,0',
2134     'orchid' => '218,112,214',
2135     'palegoldenrod' => '238,232,170',
2136     'palegreen' => '152,251,152',
2137     'paleturquoise' => '175,238,238',
2138     'palevioletred' => '219,112,147',
2139     'papayawhip' => '255,239,213',
2140     'peachpuff' => '255,218,185',
2141     'peru' => '205,133,63',
2142     'pink' => '255,192,203',
2143     'plum' => '221,160,221',
2144     'powderblue' => '176,224,230',
2145     'purple' => '128,0,128',
2146     'red' => '255,0,0',
2147     'rosybrown' => '188,143,143',
2148     'royalblue' => '65,105,225',
2149     'saddlebrown' => '139,69,19',
2150     'salmon' => '250,128,114',
2151     'sandybrown' => '244,164,96',
2152     'seagreen' => '46,139,87',
2153     'seashell' => '255,245,238',
2154     'sienna' => '160,82,45',
2155     'silver' => '192,192,192',
2156     'skyblue' => '135,206,235',
2157     'slateblue' => '106,90,205',
2158     'slategray' => '112,128,144',
2159     'slategrey' => '112,128,144',
2160     'snow' => '255,250,250',
2161     'springgreen' => '0,255,127',
2162     'steelblue' => '70,130,180',
2163     'tan' => '210,180,140',
2164     'teal' => '0,128,128',
2165     'thistle' => '216,191,216',
2166     'tomato' => '255,99,71',
2167     'transparent' => '0,0,0,0',
2168     'turquoise' => '64,224,208',
2169     'violet' => '238,130,238',
2170     'wheat' => '245,222,179',
2171     'white' => '255,255,255',
2172     'whitesmoke' => '245,245,245',
2173     'yellow' => '255,255,0',
2174     'yellowgreen' => '154,205,50'
2175   );
2176 }
2177 
2178 // responsible for taking a string of LESS code and converting it into a
2179 // syntax tree
2180 class lessc_parser {
2181   static protected $nextBlockId = 0; // used to uniquely identify blocks
2182 
2183   static protected $precedence = array(
2184     '=<' => 0,
2185     '>=' => 0,
2186     '=' => 0,
2187     '<' => 0,
2188     '>' => 0,
2189 
2190     '+' => 1,
2191     '-' => 1,
2192     '*' => 2,
2193     '/' => 2,
2194     '%' => 2,
2195   );
2196 
2197   static protected $whitePattern;
2198   static protected $commentMulti;
2199 
2200   static protected $commentSingle = "//";
2201   static protected $commentMultiLeft = "/*";
2202   static protected $commentMultiRight = "*/";
2203 
2204   // regex string to match any of the operators
2205   static protected $operatorString;
2206 
2207   // these properties will supress division unless it's inside parenthases
2208   static protected $supressDivisionProps =
2209     array('/border-radius$/i', '/^font$/i');
2210 
2211   protected $blockDirectives = array("font-face", "keyframes", "page", "-moz-document", "viewport", "-moz-viewport", "-o-viewport", "-ms-viewport");
2212   protected $lineDirectives = array("charset");
2213 
2214   /**
2215    * if we are in parens we can be more liberal with whitespace around
2216    * operators because it must evaluate to a single value and thus is less
2217    * ambiguous.
2218    *
2219    * Consider:
2220    *     property1: 10 -5; // is two numbers, 10 and -5
2221    *     property2: (10 -5); // should evaluate to 5
2222    */
2223   protected $inParens = false;
2224 
2225   // caches preg escaped literals
2226   static protected $literalCache = array();
2227 
2228   public function __construct($lessc, $sourceName = null) {
2229     $this->eatWhiteDefault = true;
2230     // reference to less needed for vPrefix, mPrefix, and parentSelector
2231     $this->lessc = $lessc;
2232 
2233     $this->sourceName = $sourceName; // name used for error messages
2234 
2235     $this->writeComments = false;
2236 
2237     if (!self::$operatorString) {
2238       self::$operatorString =
2239         '('.implode('|', array_map(array('lessc', 'preg_quote'),
2240           array_keys(self::$precedence))).')';
2241 
2242       $commentSingle = lessc::preg_quote(self::$commentSingle);
2243       $commentMultiLeft = lessc::preg_quote(self::$commentMultiLeft);
2244       $commentMultiRight = lessc::preg_quote(self::$commentMultiRight);
2245 
2246       self::$commentMulti = $commentMultiLeft.'.*?'.$commentMultiRight;
2247       self::$whitePattern = '/'.$commentSingle.'[^\n]*\s*|('.self::$commentMulti.')\s*|\s+/Ais';
2248     }
2249   }
2250 
2251   public function parse($buffer) {
2252     $this->count = 0;
2253     $this->line = 1;
2254 
2255     $this->env = null; // block stack
2256     $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
2257     $this->pushSpecialBlock("root");
2258     $this->eatWhiteDefault = true;
2259     $this->seenComments = array();
2260 
2261     // trim whitespace on head
2262     // if (preg_match('/^\s+/', $this->buffer, $m)) {
2263     //  $this->line += substr_count($m[0], "\n");
2264     //  $this->buffer = ltrim($this->buffer);
2265     // }
2266     $this->whitespace();
2267 
2268     // parse the entire file
2269     $lastCount = $this->count;
2270     while (false !== $this->parseChunk());
2271 
2272     if ($this->count != strlen($this->buffer))
2273       $this->throwError();
2274 
2275     // TODO report where the block was opened
2276     if (!is_null($this->env->parent))
2277       throw new exception('parse error: unclosed block');
2278 
2279     return $this->env;
2280   }
2281 
2282   /**
2283    * Parse a single chunk off the head of the buffer and append it to the
2284    * current parse environment.
2285    * Returns false when the buffer is empty, or when there is an error.
2286    *
2287    * This function is called repeatedly until the entire document is
2288    * parsed.
2289    *
2290    * This parser is most similar to a recursive descent parser. Single
2291    * functions represent discrete grammatical rules for the language, and
2292    * they are able to capture the text that represents those rules.
2293    *
2294    * Consider the function lessc::keyword(). (all parse functions are
2295    * structured the same)
2296    *
2297    * The function takes a single reference argument. When calling the
2298    * function it will attempt to match a keyword on the head of the buffer.
2299    * If it is successful, it will place the keyword in the referenced
2300    * argument, advance the position in the buffer, and return true. If it
2301    * fails then it won't advance the buffer and it will return false.
2302    *
2303    * All of these parse functions are powered by lessc::match(), which behaves
2304    * the same way, but takes a literal regular expression. Sometimes it is
2305    * more convenient to use match instead of creating a new function.
2306    *
2307    * Because of the format of the functions, to parse an entire string of
2308    * grammatical rules, you can chain them together using &&.
2309    *
2310    * But, if some of the rules in the chain succeed before one fails, then
2311    * the buffer position will be left at an invalid state. In order to
2312    * avoid this, lessc::seek() is used to remember and set buffer positions.
2313    *
2314    * Before parsing a chain, use $s = $this->seek() to remember the current
2315    * position into $s. Then if a chain fails, use $this->seek($s) to
2316    * go back where we started.
2317    */
2318   protected function parseChunk() {
2319     if (empty($this->buffer)) return false;
2320     $s = $this->seek();
2321 
2322     // setting a property
2323     if ($this->keyword($key) && $this->assign() &&
2324       $this->propertyValue($value, $key) && $this->end())
2325     {
2326       $this->append(array('assign', $key, $value), $s);
2327       return true;
2328     } else {
2329       $this->seek($s);
2330     }
2331 
2332 
2333     // look for special css blocks
2334     if ($this->literal('@', false)) {
2335       $this->count--;
2336 
2337       // media
2338       if ($this->literal('@media')) {
2339         if (($this->mediaQueryList($mediaQueries) || true)
2340           && $this->literal('{'))
2341         {
2342           $media = $this->pushSpecialBlock("media");
2343           $media->queries = is_null($mediaQueries) ? array() : $mediaQueries;
2344           return true;
2345         } else {
2346           $this->seek($s);
2347           return false;
2348         }
2349       }
2350 
2351       if ($this->literal("@", false) && $this->keyword($dirName)) {
2352         if ($this->isDirective($dirName, $this->blockDirectives)) {
2353           if (($this->openString("{", $dirValue, null, array(";")) || true) &&
2354             $this->literal("{"))
2355           {
2356             $dir = $this->pushSpecialBlock("directive");
2357             $dir->name = $dirName;
2358             if (isset($dirValue)) $dir->value = $dirValue;
2359             return true;
2360           }
2361         } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
2362           if ($this->propertyValue($dirValue) && $this->end()) {
2363             $this->append(array("directive", $dirName, $dirValue));
2364             return true;
2365           }
2366         }
2367       }
2368 
2369       $this->seek($s);
2370     }
2371 
2372     // setting a variable
2373     if ($this->variable($var) && $this->assign() &&
2374       $this->propertyValue($value) && $this->end())
2375     {
2376       $this->append(array('assign', $var, $value), $s);
2377       return true;
2378     } else {
2379       $this->seek($s);
2380     }
2381 
2382     if ($this->import($importValue)) {
2383       $this->append($importValue, $s);
2384       return true;
2385     }
2386 
2387     // opening parametric mixin
2388     if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
2389       ($this->guards($guards) || true) &&
2390       $this->literal('{'))
2391     {
2392       $block = $this->pushBlock($this->fixTags(array($tag)));
2393       $block->args = $args;
2394       $block->isVararg = $isVararg;
2395       if (!empty($guards)) $block->guards = $guards;
2396       return true;
2397     } else {
2398       $this->seek($s);
2399     }
2400 
2401     // opening a simple block
2402     if ($this->tags($tags) && $this->literal('{')) {
2403       $tags = $this->fixTags($tags);
2404       $this->pushBlock($tags);
2405       return true;
2406     } else {
2407       $this->seek($s);
2408     }
2409 
2410     // closing a block
2411     if ($this->literal('}', false)) {
2412       try {
2413         $block = $this->pop();
2414       } catch (exception $e) {
2415         $this->seek($s);
2416         $this->throwError($e->getMessage());
2417       }
2418 
2419       $hidden = false;
2420       if (is_null($block->type)) {
2421         $hidden = true;
2422         if (!isset($block->args)) {
2423           foreach ($block->tags as $tag) {
2424             if (!is_string($tag) || $tag{0} != $this->lessc->mPrefix) {
2425               $hidden = false;
2426               break;
2427             }
2428           }
2429         }
2430 
2431         foreach ($block->tags as $tag) {
2432           if (is_string($tag)) {
2433             $this->env->children[$tag][] = $block;
2434           }
2435         }
2436       }
2437 
2438       if (!$hidden) {
2439         $this->append(array('block', $block), $s);
2440       }
2441 
2442       // this is done here so comments aren't bundled into he block that
2443       // was just closed
2444       $this->whitespace();
2445       return true;
2446     }
2447 
2448     // mixin
2449     if ($this->mixinTags($tags) &&
2450       ($this->argumentDef($argv, $isVararg) || true) &&
2451       ($this->keyword($suffix) || true) && $this->end())
2452     {
2453       $tags = $this->fixTags($tags);
2454       $this->append(array('mixin', $tags, $argv, $suffix), $s);
2455       return true;
2456     } else {
2457       $this->seek($s);
2458     }
2459 
2460     // spare ;
2461     if ($this->literal(';')) return true;
2462 
2463     return false; // got nothing, throw error
2464   }
2465 
2466   protected function isDirective($dirname, $directives) {
2467     // TODO: cache pattern in parser
2468     $pattern = implode("|",
2469       array_map(array("lessc", "preg_quote"), $directives));
2470     $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
2471 
2472     return preg_match($pattern, $dirname);
2473   }
2474 
2475   protected function fixTags($tags) {
2476     // move @ tags out of variable namespace
2477     foreach ($tags as &$tag) {
2478       if ($tag{0} == $this->lessc->vPrefix)
2479         $tag[0] = $this->lessc->mPrefix;
2480     }
2481     return $tags;
2482   }
2483 
2484   // a list of expressions
2485   protected function expressionList(&$exps) {
2486     $values = array();
2487 
2488     while ($this->expression($exp)) {
2489       $values[] = $exp;
2490     }
2491 
2492     if (count($values) == 0) return false;
2493 
2494     $exps = lessc::compressList($values, ' ');
2495     return true;
2496   }
2497 
2498   /**
2499    * Attempt to consume an expression.
2500    * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
2501    */
2502   protected function expression(&$out) {
2503     if ($this->value($lhs)) {
2504       $out = $this->expHelper($lhs, 0);
2505 
2506       // look for / shorthand
2507       if (!empty($this->env->supressedDivision)) {
2508         unset($this->env->supressedDivision);
2509         $s = $this->seek();
2510         if ($this->literal("/") && $this->value($rhs)) {
2511           $out = array("list", "",
2512             array($out, array("keyword", "/"), $rhs));
2513         } else {
2514           $this->seek($s);
2515         }
2516       }
2517 
2518       return true;
2519     }
2520     return false;
2521   }
2522 
2523   /**
2524    * recursively parse infix equation with $lhs at precedence $minP
2525    */
2526   protected function expHelper($lhs, $minP) {
2527     $this->inExp = true;
2528     $ss = $this->seek();
2529 
2530     while (true) {
2531       $whiteBefore = isset($this->buffer[$this->count - 1]) &&
2532         ctype_space($this->buffer[$this->count - 1]);
2533 
2534       // If there is whitespace before the operator, then we require
2535       // whitespace after the operator for it to be an expression
2536       $needWhite = $whiteBefore && !$this->inParens;
2537 
2538       if ($this->match(self::$operatorString.($needWhite ? '\s' : ''), $m) && self::$precedence[$m[1]] >= $minP) {
2539         if (!$this->inParens && isset($this->env->currentProperty) && $m[1] == "/" && empty($this->env->supressedDivision)) {
2540           foreach (self::$supressDivisionProps as $pattern) {
2541             if (preg_match($pattern, $this->env->currentProperty)) {
2542               $this->env->supressedDivision = true;
2543               break 2;
2544             }
2545           }
2546         }
2547 
2548 
2549         $whiteAfter = isset($this->buffer[$this->count - 1]) &&
2550           ctype_space($this->buffer[$this->count - 1]);
2551 
2552         if (!$this->value($rhs)) break;
2553 
2554         // peek for next operator to see what to do with rhs
2555         if ($this->peek(self::$operatorString, $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]) {
2556           $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
2557         }
2558 
2559         $lhs = array('expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter);
2560         $ss = $this->seek();
2561 
2562         continue;
2563       }
2564 
2565       break;
2566     }
2567 
2568     $this->seek($ss);
2569 
2570     return $lhs;
2571   }
2572 
2573   // consume a list of values for a property
2574   public function propertyValue(&$value, $keyName = null) {
2575     $values = array();
2576 
2577     if ($keyName !== null) $this->env->currentProperty = $keyName;
2578 
2579     $s = null;
2580     while ($this->expressionList($v)) {
2581       $values[] = $v;
2582       $s = $this->seek();
2583       if (!$this->literal(',')) break;
2584     }
2585 
2586     if ($s) $this->seek($s);
2587 
2588     if ($keyName !== null) unset($this->env->currentProperty);
2589 
2590     if (count($values) == 0) return false;
2591 
2592     $value = lessc::compressList($values, ', ');
2593     return true;
2594   }
2595 
2596   protected function parenValue(&$out) {
2597     $s = $this->seek();
2598 
2599     // speed shortcut
2600     if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "(") {
2601       return false;
2602     }
2603 
2604     $inParens = $this->inParens;
2605     if ($this->literal("(") &&
2606       ($this->inParens = true) && $this->expression($exp) &&
2607       $this->literal(")"))
2608     {
2609       $out = $exp;
2610       $this->inParens = $inParens;
2611       return true;
2612     } else {
2613       $this->inParens = $inParens;
2614       $this->seek($s);
2615     }
2616 
2617     return false;
2618   }
2619 
2620   // a single value
2621   protected function value(&$value) {
2622     $s = $this->seek();
2623 
2624     // speed shortcut
2625     if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "-") {
2626       // negation
2627       if ($this->literal("-", false) &&
2628         (($this->variable($inner) && $inner = array("variable", $inner)) ||
2629         $this->unit($inner) ||
2630         $this->parenValue($inner)))
2631       {
2632         $value = array("unary", "-", $inner);
2633         return true;
2634       } else {
2635         $this->seek($s);
2636       }
2637     }
2638 
2639     if ($this->parenValue($value)) return true;
2640     if ($this->unit($value)) return true;
2641     if ($this->color($value)) return true;
2642     if ($this->func($value)) return true;
2643     if ($this->string($value)) return true;
2644 
2645     if ($this->keyword($word)) {
2646       $value = array('keyword', $word);
2647       return true;
2648     }
2649 
2650     // try a variable
2651     if ($this->variable($var)) {
2652       $value = array('variable', $var);
2653       return true;
2654     }
2655 
2656     // unquote string (should this work on any type?
2657     if ($this->literal("~") && $this->string($str)) {
2658       $value = array("escape", $str);
2659       return true;
2660     } else {
2661       $this->seek($s);
2662     }
2663 
2664     // css hack: \0
2665     if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
2666       $value = array('keyword', '\\'.$m[1]);
2667       return true;
2668     } else {
2669       $this->seek($s);
2670     }
2671 
2672     return false;
2673   }
2674 
2675   // an import statement
2676   protected function import(&$out) {
2677     $s = $this->seek();
2678     if (!$this->literal('@import')) return false;
2679 
2680     // @import "something.css" media;
2681     // @import url("something.css") media;
2682     // @import url(something.css) media;
2683 
2684     if ($this->propertyValue($value)) {
2685       $out = array("import", $value);
2686       return true;
2687     }
2688   }
2689 
2690   protected function mediaQueryList(&$out) {
2691     if ($this->genericList($list, "mediaQuery", ",", false)) {
2692       $out = $list[2];
2693       return true;
2694     }
2695     return false;
2696   }
2697 
2698   protected function mediaQuery(&$out) {
2699     $s = $this->seek();
2700 
2701     $expressions = null;
2702     $parts = array();
2703 
2704     if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) && $this->keyword($mediaType)) {
2705       $prop = array("mediaType");
2706       if (isset($only)) $prop[] = "only";
2707       if (isset($not)) $prop[] = "not";
2708       $prop[] = $mediaType;
2709       $parts[] = $prop;
2710     } else {
2711       $this->seek($s);
2712     }
2713 
2714 
2715     if (!empty($mediaType) && !$this->literal("and")) {
2716       // ~
2717     } else {
2718       $this->genericList($expressions, "mediaExpression", "and", false);
2719       if (is_array($expressions)) $parts = array_merge($parts, $expressions[2]);
2720     }
2721 
2722     if (count($parts) == 0) {
2723       $this->seek($s);
2724       return false;
2725     }
2726 
2727     $out = $parts;
2728     return true;
2729   }
2730 
2731   protected function mediaExpression(&$out) {
2732     $s = $this->seek();
2733     $value = null;
2734     if ($this->literal("(") &&
2735       $this->keyword($feature) &&
2736       ($this->literal(":") && $this->expression($value) || true) &&
2737       $this->literal(")"))
2738     {
2739       $out = array("mediaExp", $feature);
2740       if ($value) $out[] = $value;
2741       return true;
2742     } elseif ($this->variable($variable)) {
2743       $out = array('variable', $variable);
2744       return true;
2745     }
2746 
2747     $this->seek($s);
2748     return false;
2749   }
2750 
2751   // an unbounded string stopped by $end
2752   protected function openString($end, &$out, $nestingOpen=null, $rejectStrs = null) {
2753     $oldWhite = $this->eatWhiteDefault;
2754     $this->eatWhiteDefault = false;
2755 
2756     $stop = array("'", '"', "@{", $end);
2757     $stop = array_map(array("lessc", "preg_quote"), $stop);
2758     // $stop[] = self::$commentMulti;
2759 
2760     if (!is_null($rejectStrs)) {
2761       $stop = array_merge($stop, $rejectStrs);
2762     }
2763 
2764     $patt = '(.*?)('.implode("|", $stop).')';
2765 
2766     $nestingLevel = 0;
2767 
2768     $content = array();
2769     while ($this->match($patt, $m, false)) {
2770       if (!empty($m[1])) {
2771         $content[] = $m[1];
2772         if ($nestingOpen) {
2773           $nestingLevel += substr_count($m[1], $nestingOpen);
2774         }
2775       }
2776 
2777       $tok = $m[2];
2778 
2779       $this->count-= strlen($tok);
2780       if ($tok == $end) {
2781         if ($nestingLevel == 0) {
2782           break;
2783         } else {
2784           $nestingLevel--;
2785         }
2786       }
2787 
2788       if (($tok == "'" || $tok == '"') && $this->string($str)) {
2789         $content[] = $str;
2790         continue;
2791       }
2792 
2793       if ($tok == "@{" && $this->interpolation($inter)) {
2794         $content[] = $inter;
2795         continue;
2796       }
2797 
2798       if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
2799         break;
2800       }
2801 
2802       $content[] = $tok;
2803       $this->count+= strlen($tok);
2804     }
2805 
2806     $this->eatWhiteDefault = $oldWhite;
2807 
2808     if (count($content) == 0) return false;
2809 
2810     // trim the end
2811     if (is_string(end($content))) {
2812       $content[count($content) - 1] = rtrim(end($content));
2813     }
2814 
2815     $out = array("string", "", $content);
2816     return true;
2817   }
2818 
2819   protected function string(&$out) {
2820     $s = $this->seek();
2821     if ($this->literal('"', false)) {
2822       $delim = '"';
2823     } elseif ($this->literal("'", false)) {
2824       $delim = "'";
2825     } else {
2826       return false;
2827     }
2828 
2829     $content = array();
2830 
2831     // look for either ending delim , escape, or string interpolation
2832     $patt = '([^\n]*?)(@\{|\\\\|' .
2833       lessc::preg_quote($delim).')';
2834 
2835     $oldWhite = $this->eatWhiteDefault;
2836     $this->eatWhiteDefault = false;
2837 
2838     while ($this->match($patt, $m, false)) {
2839       $content[] = $m[1];
2840       if ($m[2] == "@{") {
2841         $this->count -= strlen($m[2]);
2842         if ($this->interpolation($inter, false)) {
2843           $content[] = $inter;
2844         } else {
2845           $this->count += strlen($m[2]);
2846           $content[] = "@{"; // ignore it
2847         }
2848       } elseif ($m[2] == '\\') {
2849         $content[] = $m[2];
2850         if ($this->literal($delim, false)) {
2851           $content[] = $delim;
2852         }
2853       } else {
2854         $this->count -= strlen($delim);
2855         break; // delim
2856       }
2857     }
2858 
2859     $this->eatWhiteDefault = $oldWhite;
2860 
2861     if ($this->literal($delim)) {
2862       $out = array("string", $delim, $content);
2863       return true;
2864     }
2865 
2866     $this->seek($s);
2867     return false;
2868   }
2869 
2870   protected function interpolation(&$out) {
2871     $oldWhite = $this->eatWhiteDefault;
2872     $this->eatWhiteDefault = true;
2873 
2874     $s = $this->seek();
2875     if ($this->literal("@{") &&
2876       $this->openString("}", $interp, null, array("'", '"', ";")) &&
2877       $this->literal("}", false))
2878     {
2879       $out = array("interpolate", $interp);
2880       $this->eatWhiteDefault = $oldWhite;
2881       if ($this->eatWhiteDefault) $this->whitespace();
2882       return true;
2883     }
2884 
2885     $this->eatWhiteDefault = $oldWhite;
2886     $this->seek($s);
2887     return false;
2888   }
2889 
2890   protected function unit(&$unit) {
2891     // speed shortcut
2892     if (isset($this->buffer[$this->count])) {
2893       $char = $this->buffer[$this->count];
2894       if (!ctype_digit($char) && $char != ".") return false;
2895     }
2896 
2897     if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
2898       $unit = array("number", $m[1], empty($m[2]) ? "" : $m[2]);
2899       return true;
2900     }
2901     return false;
2902   }
2903 
2904   // a # color
2905   protected function color(&$out) {
2906     if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
2907       if (strlen($m[1]) > 7) {
2908         $out = array("string", "", array($m[1]));
2909       } else {
2910         $out = array("raw_color", $m[1]);
2911       }
2912       return true;
2913     }
2914 
2915     return false;
2916   }
2917 
2918   // consume an argument definition list surrounded by ()
2919   // each argument is a variable name with optional value
2920   // or at the end a ... or a variable named followed by ...
2921   // arguments are separated by , unless a ; is in the list, then ; is the
2922   // delimiter.
2923   protected function argumentDef(&$args, &$isVararg) {
2924     $s = $this->seek();
2925     if (!$this->literal('(')) return false;
2926 
2927     $values = array();
2928     $delim = ",";
2929     $method = "expressionList";
2930 
2931     $isVararg = false;
2932     while (true) {
2933       if ($this->literal("...")) {
2934         $isVararg = true;
2935         break;
2936       }
2937 
2938       if ($this->$method($value)) {
2939         if ($value[0] == "variable") {
2940           $arg = array("arg", $value[1]);
2941           $ss = $this->seek();
2942 
2943           if ($this->assign() && $this->$method($rhs)) {
2944             $arg[] = $rhs;
2945           } else {
2946             $this->seek($ss);
2947             if ($this->literal("...")) {
2948               $arg[0] = "rest";
2949               $isVararg = true;
2950             }
2951           }
2952 
2953           $values[] = $arg;
2954           if ($isVararg) break;
2955           continue;
2956         } else {
2957           $values[] = array("lit", $value);
2958         }
2959       }
2960 
2961 
2962       if (!$this->literal($delim)) {
2963         if ($delim == "," && $this->literal(";")) {
2964           // found new delim, convert existing args
2965           $delim = ";";
2966           $method = "propertyValue";
2967 
2968           // transform arg list
2969           if (isset($values[1])) { // 2 items
2970             $newList = array();
2971             foreach ($values as $i => $arg) {
2972               switch($arg[0]) {
2973               case "arg":
2974                 if ($i) {
2975                   $this->throwError("Cannot mix ; and , as delimiter types");
2976                 }
2977                 $newList[] = $arg[2];
2978                 break;
2979               case "lit":
2980                 $newList[] = $arg[1];
2981                 break;
2982               case "rest":
2983                 $this->throwError("Unexpected rest before semicolon");
2984               }
2985             }
2986 
2987             $newList = array("list", ", ", $newList);
2988 
2989             switch ($values[0][0]) {
2990             case "arg":
2991               $newArg = array("arg", $values[0][1], $newList);
2992               break;
2993             case "lit":
2994               $newArg = array("lit", $newList);
2995               break;
2996             }
2997 
2998           } elseif ($values) { // 1 item
2999             $newArg = $values[0];
3000           }
3001 
3002           if ($newArg) {
3003             $values = array($newArg);
3004           }
3005         } else {
3006           break;
3007         }
3008       }
3009     }
3010 
3011     if (!$this->literal(')')) {
3012       $this->seek($s);
3013       return false;
3014     }
3015 
3016     $args = $values;
3017 
3018     return true;
3019   }
3020 
3021   // consume a list of tags
3022   // this accepts a hanging delimiter
3023   protected function tags(&$tags, $simple = false, $delim = ',') {
3024     $tags = array();
3025     while ($this->tag($tt, $simple)) {
3026       $tags[] = $tt;
3027       if (!$this->literal($delim)) break;
3028     }
3029     if (count($tags) == 0) return false;
3030 
3031     return true;
3032   }
3033 
3034   // list of tags of specifying mixin path
3035   // optionally separated by > (lazy, accepts extra >)
3036   protected function mixinTags(&$tags) {
3037     $s = $this->seek();
3038     $tags = array();
3039     while ($this->tag($tt, true)) {
3040       $tags[] = $tt;
3041       $this->literal(">");
3042     }
3043 
3044     if (count($tags) == 0) return false;
3045 
3046     return true;
3047   }
3048 
3049   // a bracketed value (contained within in a tag definition)
3050   protected function tagBracket(&$parts, &$hasExpression) {
3051     // speed shortcut
3052     if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] != "[") {
3053       return false;
3054     }
3055 
3056     $s = $this->seek();
3057 
3058     $hasInterpolation = false;
3059 
3060     if ($this->literal("[", false)) {
3061       $attrParts = array("[");
3062       // keyword, string, operator
3063       while (true) {
3064         if ($this->literal("]", false)) {
3065           $this->count--;
3066           break; // get out early
3067         }
3068 
3069         if ($this->match('\s+', $m)) {
3070           $attrParts[] = " ";
3071           continue;
3072         }
3073         if ($this->string($str)) {
3074           // escape parent selector, (yuck)
3075           foreach ($str[2] as &$chunk) {
3076             $chunk = str_replace($this->lessc->parentSelector, "$&$", $chunk);
3077           }
3078 
3079           $attrParts[] = $str;
3080           $hasInterpolation = true;
3081           continue;
3082         }
3083 
3084         if ($this->keyword($word)) {
3085           $attrParts[] = $word;
3086           continue;
3087         }
3088 
3089         if ($this->interpolation($inter, false)) {
3090           $attrParts[] = $inter;
3091           $hasInterpolation = true;
3092           continue;
3093         }
3094 
3095         // operator, handles attr namespace too
3096         if ($this->match('[|-~\$\*\^=]+', $m)) {
3097           $attrParts[] = $m[0];
3098           continue;
3099         }
3100 
3101         break;
3102       }
3103 
3104       if ($this->literal("]", false)) {
3105         $attrParts[] = "]";
3106         foreach ($attrParts as $part) {
3107           $parts[] = $part;
3108         }
3109         $hasExpression = $hasExpression || $hasInterpolation;
3110         return true;
3111       }
3112       $this->seek($s);
3113     }
3114 
3115     $this->seek($s);
3116     return false;
3117   }
3118 
3119   // a space separated list of selectors
3120   protected function tag(&$tag, $simple = false) {
3121     if ($simple)
3122       $chars = '^@,:;{}\][>\(\) "\'';
3123     else
3124       $chars = '^@,;{}["\'';
3125 
3126     $s = $this->seek();
3127 
3128     $hasExpression = false;
3129     $parts = array();
3130     while ($this->tagBracket($parts, $hasExpression));
3131 
3132     $oldWhite = $this->eatWhiteDefault;
3133     $this->eatWhiteDefault = false;
3134 
3135     while (true) {
3136       if ($this->match('(['.$chars.'0-9]['.$chars.']*)', $m)) {
3137         $parts[] = $m[1];
3138         if ($simple) break;
3139 
3140         while ($this->tagBracket($parts, $hasExpression));
3141         continue;
3142       }
3143 
3144       if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] == "@") {
3145         if ($this->interpolation($interp)) {
3146           $hasExpression = true;
3147           $interp[2] = true; // don't unescape
3148           $parts[] = $interp;
3149           continue;
3150         }
3151 
3152         if ($this->literal("@")) {
3153           $parts[] = "@";
3154           continue;
3155         }
3156       }
3157 
3158       if ($this->unit($unit)) { // for keyframes
3159         $parts[] = $unit[1];
3160         $parts[] = $unit[2];
3161         continue;
3162       }
3163 
3164       break;
3165     }
3166 
3167     $this->eatWhiteDefault = $oldWhite;
3168     if (!$parts) {
3169       $this->seek($s);
3170       return false;
3171     }
3172 
3173     if ($hasExpression) {
3174       $tag = array("exp", array("string", "", $parts));
3175     } else {
3176       $tag = trim(implode($parts));
3177     }
3178 
3179     $this->whitespace();
3180     return true;
3181   }
3182 
3183   // a css function
3184   protected function func(&$func) {
3185     $s = $this->seek();
3186 
3187     if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
3188       $fname = $m[1];
3189 
3190       $sPreArgs = $this->seek();
3191 
3192       $args = array();
3193       while (true) {
3194         $ss = $this->seek();
3195         // this ugly nonsense is for ie filter properties
3196         if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
3197           $args[] = array("string", "", array($name, "=", $value));
3198         } else {
3199           $this->seek($ss);
3200           if ($this->expressionList($value)) {
3201             $args[] = $value;
3202           }
3203         }
3204 
3205         if (!$this->literal(',')) break;
3206       }
3207       $args = array('list', ',', $args);
3208 
3209       if ($this->literal(')')) {
3210         $func = array('function', $fname, $args);
3211         return true;
3212       } elseif ($fname == 'url') {
3213         // couldn't parse and in url? treat as string
3214         $this->seek($sPreArgs);
3215         if ($this->openString(")", $string) && $this->literal(")")) {
3216           $func = array('function', $fname, $string);
3217           return true;
3218         }
3219       }
3220     }
3221 
3222     $this->seek($s);
3223     return false;
3224   }
3225 
3226   // consume a less variable
3227   protected function variable(&$name) {
3228     $s = $this->seek();
3229     if ($this->literal($this->lessc->vPrefix, false) &&
3230       ($this->variable($sub) || $this->keyword($name)))
3231     {
3232       if (!empty($sub)) {
3233         $name = array('variable', $sub);
3234       } else {
3235         $name = $this->lessc->vPrefix.$name;
3236       }
3237       return true;
3238     }
3239 
3240     $name = null;
3241     $this->seek($s);
3242     return false;
3243   }
3244 
3245   /**
3246    * Consume an assignment operator
3247    * Can optionally take a name that will be set to the current property name
3248    */
3249   protected function assign($name = null) {
3250     if ($name) $this->currentProperty = $name;
3251     return $this->literal(':') || $this->literal('=');
3252   }
3253 
3254   // consume a keyword
3255   protected function keyword(&$word) {
3256     if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
3257       $word = $m[1];
3258       return true;
3259     }
3260     return false;
3261   }
3262 
3263   // consume an end of statement delimiter
3264   protected function end() {
3265     if ($this->literal(';')) {
3266       return true;
3267     } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] == '}') {
3268       // if there is end of file or a closing block next then we don't need a ;
3269       return true;
3270     }
3271     return false;
3272   }
3273 
3274   protected function guards(&$guards) {
3275     $s = $this->seek();
3276 
3277     if (!$this->literal("when")) {
3278       $this->seek($s);
3279       return false;
3280     }
3281 
3282     $guards = array();
3283 
3284     while ($this->guardGroup($g)) {
3285       $guards[] = $g;
3286       if (!$this->literal(",")) break;
3287     }
3288 
3289     if (count($guards) == 0) {
3290       $guards = null;
3291       $this->seek($s);
3292       return false;
3293     }
3294 
3295     return true;
3296   }
3297 
3298   // a bunch of guards that are and'd together
3299   // TODO rename to guardGroup
3300   protected function guardGroup(&$guardGroup) {
3301     $s = $this->seek();
3302     $guardGroup = array();
3303     while ($this->guard($guard)) {
3304       $guardGroup[] = $guard;
3305       if (!$this->literal("and")) break;
3306     }
3307 
3308     if (count($guardGroup) == 0) {
3309       $guardGroup = null;
3310       $this->seek($s);
3311       return false;
3312     }
3313 
3314     return true;
3315   }
3316 
3317   protected function guard(&$guard) {
3318     $s = $this->seek();
3319     $negate = $this->literal("not");
3320 
3321     if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
3322       $guard = $exp;
3323       if ($negate) $guard = array("negate", $guard);
3324       return true;
3325     }
3326 
3327     $this->seek($s);
3328     return false;
3329   }
3330 
3331   /* raw parsing functions */
3332 
3333   protected function literal($what, $eatWhitespace = null) {
3334     if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3335 
3336     // shortcut on single letter
3337     if (!isset($what[1]) && isset($this->buffer[$this->count])) {
3338       if ($this->buffer[$this->count] == $what) {
3339         if (!$eatWhitespace) {
3340           $this->count++;
3341           return true;
3342         }
3343         // goes below...
3344       } else {
3345         return false;
3346       }
3347     }
3348 
3349     if (!isset(self::$literalCache[$what])) {
3350       self::$literalCache[$what] = lessc::preg_quote($what);
3351     }
3352 
3353     return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
3354   }
3355 
3356   protected function genericList(&$out, $parseItem, $delim="", $flatten=true) {
3357     $s = $this->seek();
3358     $items = array();
3359     while ($this->$parseItem($value)) {
3360       $items[] = $value;
3361       if ($delim) {
3362         if (!$this->literal($delim)) break;
3363       }
3364     }
3365 
3366     if (count($items) == 0) {
3367       $this->seek($s);
3368       return false;
3369     }
3370 
3371     if ($flatten && count($items) == 1) {
3372       $out = $items[0];
3373     } else {
3374       $out = array("list", $delim, $items);
3375     }
3376 
3377     return true;
3378   }
3379 
3380 
3381   // advance counter to next occurrence of $what
3382   // $until - don't include $what in advance
3383   // $allowNewline, if string, will be used as valid char set
3384   protected function to($what, &$out, $until = false, $allowNewline = false) {
3385     if (is_string($allowNewline)) {
3386       $validChars = $allowNewline;
3387     } else {
3388       $validChars = $allowNewline ? "." : "[^\n]";
3389     }
3390     if (!$this->match('('.$validChars.'*?)'.lessc::preg_quote($what), $m, !$until)) return false;
3391     if ($until) $this->count -= strlen($what); // give back $what
3392     $out = $m[1];
3393     return true;
3394   }
3395 
3396   // try to match something on head of buffer
3397   protected function match($regex, &$out, $eatWhitespace = null) {
3398     if ($eatWhitespace === null) $eatWhitespace = $this->eatWhiteDefault;
3399 
3400     $r = '/'.$regex.($eatWhitespace && !$this->writeComments ? '\s*' : '').'/Ais';
3401     if (preg_match($r, $this->buffer, $out, null, $this->count)) {
3402       $this->count += strlen($out[0]);
3403       if ($eatWhitespace && $this->writeComments) $this->whitespace();
3404       return true;
3405     }
3406     return false;
3407   }
3408 
3409   // match some whitespace
3410   protected function whitespace() {
3411     if ($this->writeComments) {
3412       $gotWhite = false;
3413       while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
3414         if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
3415           $this->append(array("comment", $m[1]));
3416           $this->commentsSeen[$this->count] = true;
3417         }
3418         $this->count += strlen($m[0]);
3419         $gotWhite = true;
3420       }
3421       return $gotWhite;
3422     } else {
3423       $this->match("", $m);
3424       return strlen($m[0]) > 0;
3425     }
3426   }
3427 
3428   // match something without consuming it
3429   protected function peek($regex, &$out = null, $from=null) {
3430     if (is_null($from)) $from = $this->count;
3431     $r = '/'.$regex.'/Ais';
3432     $result = preg_match($r, $this->buffer, $out, null, $from);
3433 
3434     return $result;
3435   }
3436 
3437   // seek to a spot in the buffer or return where we are on no argument
3438   protected function seek($where = null) {
3439     if ($where === null) return $this->count;
3440     else $this->count = $where;
3441     return true;
3442   }
3443 
3444   /* misc functions */
3445 
3446   public function throwError($msg = "parse error", $count = null) {
3447     $count = is_null($count) ? $this->count : $count;
3448 
3449     $line = $this->line +
3450       substr_count(substr($this->buffer, 0, $count), "\n");
3451 
3452     if (!empty($this->sourceName)) {
3453       $loc = "$this->sourceName on line $line";
3454     } else {
3455       $loc = "line: $line";
3456     }
3457 
3458     // TODO this depends on $this->count
3459     if ($this->peek("(.*?)(\n|$)", $m, $count)) {
3460       throw new exception("$msg: failed at `$m[1]` $loc");
3461     } else {
3462       throw new exception("$msg: $loc");
3463     }
3464   }
3465 
3466   protected function pushBlock($selectors=null, $type=null) {
3467     $b = new stdclass;
3468     $b->parent = $this->env;
3469 
3470     $b->type = $type;
3471     $b->id = self::$nextBlockId++;
3472 
3473     $b->isVararg = false; // TODO: kill me from here
3474     $b->tags = $selectors;
3475 
3476     $b->props = array();
3477     $b->children = array();
3478 
3479     $this->env = $b;
3480     return $b;
3481   }
3482 
3483   // push a block that doesn't multiply tags
3484   protected function pushSpecialBlock($type) {
3485     return $this->pushBlock(null, $type);
3486   }
3487 
3488   // append a property to the current block
3489   protected function append($prop, $pos = null) {
3490     if ($pos !== null) $prop[-1] = $pos;
3491     $this->env->props[] = $prop;
3492   }
3493 
3494   // pop something off the stack
3495   protected function pop() {
3496     $old = $this->env;
3497     $this->env = $this->env->parent;
3498     return $old;
3499   }
3500 
3501   // remove comments from $text
3502   // todo: make it work for all functions, not just url
3503   protected function removeComments($text) {
3504     $look = array(
3505       'url(', '//', '/*', '"', "'"
3506     );
3507 
3508     $out = '';
3509     $min = null;
3510     while (true) {
3511       // find the next item
3512       foreach ($look as $token) {
3513         $pos = strpos($text, $token);
3514         if ($pos !== false) {
3515           if (!isset($min) || $pos < $min[1]) $min = array($token, $pos);
3516         }
3517       }
3518 
3519       if (is_null($min)) break;
3520 
3521       $count = $min[1];
3522       $skip = 0;
3523       $newlines = 0;
3524       switch ($min[0]) {
3525       case 'url(':
3526         if (preg_match('/url\(.*?\)/', $text, $m, 0, $count))
3527           $count += strlen($m[0]) - strlen($min[0]);
3528         break;
3529       case '"':
3530       case "'":
3531         if (preg_match('/'.$min[0].'.*?(?<!\\\\)'.$min[0].'/', $text, $m, 0, $count))
3532           $count += strlen($m[0]) - 1;
3533         break;
3534       case '//':
3535         $skip = strpos($text, "\n", $count);
3536         if ($skip === false) $skip = strlen($text) - $count;
3537         else $skip -= $count;
3538         break;
3539       case '/*':
3540         if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
3541           $skip = strlen($m[0]);
3542           $newlines = substr_count($m[0], "\n");
3543         }
3544         break;
3545       }
3546 
3547       if ($skip == 0) $count += strlen($min[0]);
3548 
3549       $out .= substr($text, 0, $count).str_repeat("\n", $newlines);
3550       $text = substr($text, $count + $skip);
3551 
3552       $min = null;
3553     }
3554 
3555     return $out.$text;
3556   }
3557 
3558 }
3559 
3560 class lessc_formatter_classic {
3561   public $indentChar = "  ";
3562 
3563   public $break = "\n";
3564   public $open = " {";
3565   public $close = "}";
3566   public $selectorSeparator = ", ";
3567   public $assignSeparator = ":";
3568 
3569   public $openSingle = " { ";
3570   public $closeSingle = " }";
3571 
3572   public $disableSingle = false;
3573   public $breakSelectors = false;
3574 
3575   public $compressColors = false;
3576 
3577   public function __construct() {
3578     $this->indentLevel = 0;
3579   }
3580 
3581   public function indentStr($n = 0) {
3582     return str_repeat($this->indentChar, max($this->indentLevel + $n, 0));
3583   }
3584 
3585   public function property($name, $value) {
3586     return $name . $this->assignSeparator . $value . ";";
3587   }
3588 
3589   protected function isEmpty($block) {
3590     if (empty($block->lines)) {
3591       foreach ($block->children as $child) {
3592         if (!$this->isEmpty($child)) return false;
3593       }
3594 
3595       return true;
3596     }
3597     return false;
3598   }
3599 
3600   public function block($block) {
3601     if ($this->isEmpty($block)) return;
3602 
3603     $inner = $pre = $this->indentStr();
3604 
3605     $isSingle = !$this->disableSingle &&
3606       is_null($block->type) && count($block->lines) == 1;
3607 
3608     if (!empty($block->selectors)) {
3609       $this->indentLevel++;
3610 
3611       if ($this->breakSelectors) {
3612         $selectorSeparator = $this->selectorSeparator . $this->break . $pre;
3613       } else {
3614         $selectorSeparator = $this->selectorSeparator;
3615       }
3616 
3617       echo $pre .
3618         implode($selectorSeparator, $block->selectors);
3619       if ($isSingle) {
3620         echo $this->openSingle;
3621         $inner = "";
3622       } else {
3623         echo $this->open . $this->break;
3624         $inner = $this->indentStr();
3625       }
3626 
3627     }
3628 
3629     if (!empty($block->lines)) {
3630       $glue = $this->break.$inner;
3631       echo $inner . implode($glue, $block->lines);
3632       if (!$isSingle && !empty($block->children)) {
3633         echo $this->break;
3634       }
3635     }
3636 
3637     foreach ($block->children as $child) {
3638       $this->block($child);
3639     }
3640 
3641     if (!empty($block->selectors)) {
3642       if (!$isSingle && empty($block->children)) echo $this->break;
3643 
3644       if ($isSingle) {
3645         echo $this->closeSingle . $this->break;
3646       } else {
3647         echo $pre . $this->close . $this->break;
3648       }
3649 
3650       $this->indentLevel--;
3651     }
3652   }
3653 }
3654 
3655 class lessc_formatter_compressed extends lessc_formatter_classic {
3656   public $disableSingle = true;
3657   public $open = "{";
3658   public $selectorSeparator = ",";
3659   public $assignSeparator = ":";
3660   public $break = "";
3661   public $compressColors = true;
3662 
3663   public function indentStr($n = 0) {
3664     return "";
3665   }
3666 }
3667 
3668 class lessc_formatter_lessjs extends lessc_formatter_classic {
3669   public $disableSingle = true;
3670   public $breakSelectors = true;
3671   public $assignSeparator = ": ";
3672   public $selectorSeparator = ",";
3673 }
3674 
3675