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