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