1<?php
2/**
3 * SCSSPHP
4 *
5 * @copyright 2012-2015 Leaf Corcoran
6 *
7 * @license http://opensource.org/licenses/MIT MIT
8 *
9 * @link http://leafo.github.io/scssphp
10 */
11namespace Leafo\ScssPhp;
12
13use Leafo\ScssPhp\Base\Range;
14use Leafo\ScssPhp\Block;
15use Leafo\ScssPhp\Colors;
16use Leafo\ScssPhp\Compiler\Environment;
17use Leafo\ScssPhp\Exception\CompilerException;
18use Leafo\ScssPhp\Formatter\OutputBlock;
19use Leafo\ScssPhp\Node;
20use Leafo\ScssPhp\Type;
21use Leafo\ScssPhp\Parser;
22use Leafo\ScssPhp\Util;
23/**
24 * The scss compiler and parser.
25 *
26 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
27 * by `Parser` into a syntax tree, then it is compiled into another tree
28 * representing the CSS structure by `Compiler`. The CSS tree is fed into a
29 * formatter, like `Formatter` which then outputs CSS as a string.
30 *
31 * During the first compile, all values are *reduced*, which means that their
32 * types are brought to the lowest form before being dump as strings. This
33 * handles math equations, variable dereferences, and the like.
34 *
35 * The `compile` function of `Compiler` is the entry point.
36 *
37 * In summary:
38 *
39 * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
40 * then transforms the resulting tree to a CSS tree. This class also holds the
41 * evaluation context, such as all available mixins and variables at any given
42 * time.
43 *
44 * The `Parser` class is only concerned with parsing its input.
45 *
46 * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
47 * handling things like indentation.
48 */
49/**
50 * SCSS compiler
51 *
52 * @author Leaf Corcoran <leafot@gmail.com>
53 */
54class Compiler
55{
56    const LINE_COMMENTS = 1;
57    const DEBUG_INFO = 2;
58    const WITH_RULE = 1;
59    const WITH_MEDIA = 2;
60    const WITH_SUPPORTS = 4;
61    const WITH_ALL = 7;
62    /**
63     * @var array
64     */
65    protected static $operatorNames = array('+' => 'add', '-' => 'sub', '*' => 'mul', '/' => 'div', '%' => 'mod', '==' => 'eq', '!=' => 'neq', '<' => 'lt', '>' => 'gt', '<=' => 'lte', '>=' => 'gte', '<=>' => 'cmp');
66    /**
67     * @var array
68     */
69    protected static $namespaces = array('special' => '%', 'mixin' => '@', 'function' => '^');
70    public static $true = array(Type::T_KEYWORD, 'true');
71    public static $false = array(Type::T_KEYWORD, 'false');
72    public static $null = array(Type::T_NULL);
73    public static $nullString = array(Type::T_STRING, '', array());
74    public static $defaultValue = array(Type::T_KEYWORD, '');
75    public static $selfSelector = array(Type::T_SELF);
76    public static $emptyList = array(Type::T_LIST, '', array());
77    public static $emptyMap = array(Type::T_MAP, array(), array());
78    public static $emptyString = array(Type::T_STRING, '"', array());
79    public static $with = array(Type::T_KEYWORD, 'with');
80    public static $without = array(Type::T_KEYWORD, 'without');
81    protected $importPaths = array('');
82    protected $importCache = array();
83    protected $importedFiles = array();
84    protected $userFunctions = array();
85    protected $registeredVars = array();
86    protected $registeredFeatures = array('extend-selector-pseudoclass' => false, 'at-error' => true, 'units-level-3' => false, 'global-variable-shadowing' => false);
87    protected $encoding = null;
88    protected $lineNumberStyle = null;
89    protected $formatter = 'Leafo\\ScssPhp\\Formatter\\Nested';
90    protected $rootEnv;
91    protected $rootBlock;
92    protected $env;
93    protected $scope;
94    protected $storeEnv;
95    protected $charsetSeen;
96    protected $sourceNames;
97    private $indentLevel;
98    private $commentsSeen;
99    private $extends;
100    private $extendsMap;
101    private $parsedFiles;
102    private $parser;
103    private $sourceIndex;
104    private $sourceLine;
105    private $sourceColumn;
106    private $stderr;
107    private $shouldEvaluate;
108    private $ignoreErrors;
109    /**
110     * Constructor
111     */
112    public function __construct()
113    {
114        $this->parsedFiles = array();
115        $this->sourceNames = array();
116    }
117    /**
118     * Compile scss
119     *
120     * @api
121     *
122     * @param string $code
123     * @param string $path
124     *
125     * @return string
126     */
127    public function compile($code, $path = null)
128    {
129        $locale = setlocale(LC_NUMERIC, 0);
130        setlocale(LC_NUMERIC, 'C');
131        $this->indentLevel = -1;
132        $this->commentsSeen = array();
133        $this->extends = array();
134        $this->extendsMap = array();
135        $this->sourceIndex = null;
136        $this->sourceLine = null;
137        $this->sourceColumn = null;
138        $this->env = null;
139        $this->scope = null;
140        $this->storeEnv = null;
141        $this->charsetSeen = null;
142        $this->shouldEvaluate = null;
143        $this->stderr = fopen('php://stderr', 'w');
144        $this->parser = $this->parserFactory($path);
145        $tree = $this->parser->parse($code);
146        $this->parser = null;
147        $this->formatter = new $this->formatter();
148        $this->rootBlock = null;
149        $this->rootEnv = $this->pushEnv($tree);
150        $this->injectVariables($this->registeredVars);
151        $this->compileRoot($tree);
152        $this->popEnv();
153        $out = $this->formatter->format($this->scope);
154        setlocale(LC_NUMERIC, $locale);
155        return $out;
156    }
157    /**
158     * Instantiate parser
159     *
160     * @param string $path
161     *
162     * @return \Leafo\ScssPhp\Parser
163     */
164    protected function parserFactory($path)
165    {
166        $parser = new Parser($path, count($this->sourceNames), $this->encoding);
167        $this->sourceNames[] = $path;
168        $this->addParsedFile($path);
169        return $parser;
170    }
171    /**
172     * Is self extend?
173     *
174     * @param array $target
175     * @param array $origin
176     *
177     * @return boolean
178     */
179    protected function isSelfExtend($target, $origin)
180    {
181        foreach ($origin as $sel) {
182            if (in_array($target, $sel)) {
183                return true;
184            }
185        }
186        return false;
187    }
188    /**
189     * Push extends
190     *
191     * @param array     $target
192     * @param array     $origin
193     * @param \stdClass $block
194     */
195    protected function pushExtends($target, $origin, $block)
196    {
197        if ($this->isSelfExtend($target, $origin)) {
198            return;
199        }
200        $i = count($this->extends);
201        $this->extends[] = array($target, $origin, $block);
202        foreach ($target as $part) {
203            if (isset($this->extendsMap[$part])) {
204                $this->extendsMap[$part][] = $i;
205            } else {
206                $this->extendsMap[$part] = array($i);
207            }
208        }
209    }
210    /**
211     * Make output block
212     *
213     * @param string $type
214     * @param array  $selectors
215     *
216     * @return \Leafo\ScssPhp\Formatter\OutputBlock
217     */
218    protected function makeOutputBlock($type, $selectors = null)
219    {
220        $out = new OutputBlock();
221        $out->type = $type;
222        $out->lines = array();
223        $out->children = array();
224        $out->parent = $this->scope;
225        $out->selectors = $selectors;
226        $out->depth = $this->env->depth;
227        return $out;
228    }
229    /**
230     * Compile root
231     *
232     * @param \Leafo\ScssPhp\Block $rootBlock
233     */
234    protected function compileRoot(Block $rootBlock)
235    {
236        $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
237        $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
238        $this->flattenSelectors($this->scope);
239        $this->missingSelectors();
240    }
241    /**
242     * Report missing selectors
243     */
244    protected function missingSelectors()
245    {
246        foreach ($this->extends as $extend) {
247            if (isset($extend[3])) {
248                continue;
249            }
250            list($target, $origin, $block) = $extend;
251            // ignore if !optional
252            if ($block[2]) {
253                continue;
254            }
255            $target = implode(' ', $target);
256            $origin = $this->collapseSelectors($origin);
257            $this->sourceLine = $block[Parser::SOURCE_LINE];
258            $this->throwError("\"{$origin}\" failed to @extend \"{$target}\". The selector \"{$target}\" was not found.");
259        }
260    }
261    /**
262     * Flatten selectors
263     *
264     * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
265     * @param string                               $parentKey
266     */
267    protected function flattenSelectors(OutputBlock $block, $parentKey = null)
268    {
269        if ($block->selectors) {
270            $selectors = array();
271            foreach ($block->selectors as $s) {
272                $selectors[] = $s;
273                if (!is_array($s)) {
274                    continue;
275                }
276                // check extends
277                if (!empty($this->extendsMap)) {
278                    $this->matchExtends($s, $selectors);
279                    // remove duplicates
280                    array_walk($selectors, function (&$value) {
281                        $value = serialize($value);
282                    });
283                    $selectors = array_unique($selectors);
284                    array_walk($selectors, function (&$value) {
285                        $value = unserialize($value);
286                    });
287                }
288            }
289            $block->selectors = array();
290            $placeholderSelector = false;
291            foreach ($selectors as $selector) {
292                if ($this->hasSelectorPlaceholder($selector)) {
293                    $placeholderSelector = true;
294                    continue;
295                }
296                $block->selectors[] = $this->compileSelector($selector);
297            }
298            if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
299                unset($block->parent->children[$parentKey]);
300                return;
301            }
302        }
303        foreach ($block->children as $key => $child) {
304            $this->flattenSelectors($child, $key);
305        }
306    }
307    /**
308     * Match extends
309     *
310     * @param array   $selector
311     * @param array   $out
312     * @param integer $from
313     * @param boolean $initial
314     */
315    protected function matchExtends($selector, &$out, $from = 0, $initial = true)
316    {
317        foreach ($selector as $i => $part) {
318            if ($i < $from) {
319                continue;
320            }
321            if ($this->matchExtendsSingle($part, $origin)) {
322                $before = array_slice($selector, 0, $i);
323                $after = array_slice($selector, $i + 1);
324                $s = count($before);
325                foreach ($origin as $new) {
326                    $k = 0;
327                    // remove shared parts
328                    if ($initial) {
329                        while ($k < $s && isset($new[$k]) && $before[$k] === $new[$k]) {
330                            $k++;
331                        }
332                    }
333                    $result = array_merge($before, $k > 0 ? array_slice($new, $k) : $new, $after);
334                    if ($result === $selector) {
335                        continue;
336                    }
337                    $out[] = $result;
338                    // recursively check for more matches
339                    $this->matchExtends($result, $out, $i, false);
340                    // selector sequence merging
341                    if (!empty($before) && count($new) > 1) {
342                        $result2 = array_merge(array_slice($new, 0, -1), $k > 0 ? array_slice($before, $k) : $before, array_slice($new, -1), $after);
343                        $out[] = $result2;
344                    }
345                }
346            }
347        }
348    }
349    /**
350     * Match extends single
351     *
352     * @param array $rawSingle
353     * @param array $outOrigin
354     *
355     * @return boolean
356     */
357    protected function matchExtendsSingle($rawSingle, &$outOrigin)
358    {
359        $counts = array();
360        $single = array();
361        foreach ($rawSingle as $part) {
362            // matches Number
363            if (!is_string($part)) {
364                return false;
365            }
366            if (!preg_match('/^[\\[.:#%]/', $part) && count($single)) {
367                $single[count($single) - 1] .= $part;
368            } else {
369                $single[] = $part;
370            }
371        }
372        foreach ($single as $part) {
373            if (isset($this->extendsMap[$part])) {
374                foreach ($this->extendsMap[$part] as $idx) {
375                    $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
376                }
377            }
378        }
379        $outOrigin = array();
380        $found = false;
381        foreach ($counts as $idx => $count) {
382            list($target, $origin, ) = $this->extends[$idx];
383            // check count
384            if ($count !== count($target)) {
385                continue;
386            }
387            $this->extends[$idx][3] = true;
388            $rem = array_diff($single, $target);
389            foreach ($origin as $j => $new) {
390                // prevent infinite loop when target extends itself
391                if ($this->isSelfExtend($single, $origin)) {
392                    return false;
393                }
394                $combined = $this->combineSelectorSingle(end($new), $rem);
395                if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
396                    $origin[$j][count($origin[$j]) - 1] = $combined;
397                }
398            }
399            $outOrigin = array_merge($outOrigin, $origin);
400            $found = true;
401        }
402        return $found;
403    }
404    /**
405     * Combine selector single
406     *
407     * @param array $base
408     * @param array $other
409     *
410     * @return array
411     */
412    protected function combineSelectorSingle($base, $other)
413    {
414        $tag = array();
415        $out = array();
416        $wasTag = true;
417        foreach (array($base, $other) as $single) {
418            foreach ($single as $part) {
419                if (preg_match('/^[\\[.:#]/', $part)) {
420                    $out[] = $part;
421                    $wasTag = false;
422                } elseif (preg_match('/^[^_-]/', $part)) {
423                    $tag[] = $part;
424                    $wasTag = true;
425                } elseif ($wasTag) {
426                    $tag[count($tag) - 1] .= $part;
427                } else {
428                    $out[count($out) - 1] .= $part;
429                }
430            }
431        }
432        if (count($tag)) {
433            array_unshift($out, $tag[0]);
434        }
435        return $out;
436    }
437    /**
438     * Compile media
439     *
440     * @param \Leafo\ScssPhp\Block $media
441     */
442    protected function compileMedia(Block $media)
443    {
444        $this->pushEnv($media);
445        $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
446        if (!empty($mediaQuery)) {
447            $this->scope = $this->makeOutputBlock(Type::T_MEDIA, array($mediaQuery));
448            $parentScope = $this->mediaParent($this->scope);
449            $parentScope->children[] = $this->scope;
450            // top level properties in a media cause it to be wrapped
451            $needsWrap = false;
452            foreach ($media->children as $child) {
453                $type = $child[0];
454                if ($type !== Type::T_BLOCK && $type !== Type::T_MEDIA && $type !== Type::T_DIRECTIVE && $type !== Type::T_IMPORT) {
455                    $needsWrap = true;
456                    break;
457                }
458            }
459            if ($needsWrap) {
460                $wrapped = new Block();
461                $wrapped->sourceIndex = $media->sourceIndex;
462                $wrapped->sourceLine = $media->sourceLine;
463                $wrapped->sourceColumn = $media->sourceColumn;
464                $wrapped->selectors = array();
465                $wrapped->comments = array();
466                $wrapped->parent = $media;
467                $wrapped->children = $media->children;
468                $media->children = array(array(Type::T_BLOCK, $wrapped));
469            }
470            $this->compileChildrenNoReturn($media->children, $this->scope);
471            $this->scope = $this->scope->parent;
472        }
473        $this->popEnv();
474    }
475    /**
476     * Media parent
477     *
478     * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
479     *
480     * @return \Leafo\ScssPhp\Formatter\OutputBlock
481     */
482    protected function mediaParent(OutputBlock $scope)
483    {
484        while (!empty($scope->parent)) {
485            if (!empty($scope->type) && $scope->type !== Type::T_MEDIA) {
486                break;
487            }
488            $scope = $scope->parent;
489        }
490        return $scope;
491    }
492    /**
493     * Compile directive
494     *
495     * @param \Leafo\ScssPhp\Block $block
496     */
497    protected function compileDirective(Block $block)
498    {
499        $s = '@' . $block->name;
500        if (!empty($block->value)) {
501            $s .= ' ' . $this->compileValue($block->value);
502        }
503        if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
504            $this->compileKeyframeBlock($block, array($s));
505        } else {
506            $this->compileNestedBlock($block, array($s));
507        }
508    }
509    /**
510     * Compile at-root
511     *
512     * @param \Leafo\ScssPhp\Block $block
513     */
514    protected function compileAtRoot(Block $block)
515    {
516        $env = $this->pushEnv($block);
517        $envs = $this->compactEnv($env);
518        $without = isset($block->with) ? $this->compileWith($block->with) : self::WITH_RULE;
519        // wrap inline selector
520        if ($block->selector) {
521            $wrapped = new Block();
522            $wrapped->sourceIndex = $block->sourceIndex;
523            $wrapped->sourceLine = $block->sourceLine;
524            $wrapped->sourceColumn = $block->sourceColumn;
525            $wrapped->selectors = $block->selector;
526            $wrapped->comments = array();
527            $wrapped->parent = $block;
528            $wrapped->children = $block->children;
529            $block->children = array(array(Type::T_BLOCK, $wrapped));
530        }
531        $this->env = $this->filterWithout($envs, $without);
532        $newBlock = $this->spliceTree($envs, $block, $without);
533        $saveScope = $this->scope;
534        $this->scope = $this->rootBlock;
535        $this->compileChild($newBlock, $this->scope);
536        $this->scope = $saveScope;
537        $this->env = $this->extractEnv($envs);
538        $this->popEnv();
539    }
540    /**
541     * Splice parse tree
542     *
543     * @param array                $envs
544     * @param \Leafo\ScssPhp\Block $block
545     * @param integer              $without
546     *
547     * @return array
548     */
549    private function spliceTree($envs, Block $block, $without)
550    {
551        $newBlock = null;
552        foreach ($envs as $e) {
553            if (!isset($e->block)) {
554                continue;
555            }
556            if ($e->block === $block) {
557                continue;
558            }
559            if (isset($e->block->type) && $e->block->type === Type::T_AT_ROOT) {
560                continue;
561            }
562            if ($e->block && $this->isWithout($without, $e->block)) {
563                continue;
564            }
565            $b = new Block();
566            $b->sourceIndex = $e->block->sourceIndex;
567            $b->sourceLine = $e->block->sourceLine;
568            $b->sourceColumn = $e->block->sourceColumn;
569            $b->selectors = array();
570            $b->comments = $e->block->comments;
571            $b->parent = null;
572            if ($newBlock) {
573                $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
574                $b->children = array(array($type, $newBlock));
575                $newBlock->parent = $b;
576            } elseif (count($block->children)) {
577                foreach ($block->children as $child) {
578                    if ($child[0] === Type::T_BLOCK) {
579                        $child[1]->parent = $b;
580                    }
581                }
582                $b->children = $block->children;
583            }
584            if (isset($e->block->type)) {
585                $b->type = $e->block->type;
586            }
587            if (isset($e->block->name)) {
588                $b->name = $e->block->name;
589            }
590            if (isset($e->block->queryList)) {
591                $b->queryList = $e->block->queryList;
592            }
593            if (isset($e->block->value)) {
594                $b->value = $e->block->value;
595            }
596            $newBlock = $b;
597        }
598        $type = isset($newBlock->type) ? $newBlock->type : Type::T_BLOCK;
599        return array($type, $newBlock);
600    }
601    /**
602     * Compile @at-root's with: inclusion / without: exclusion into filter flags
603     *
604     * @param array $with
605     *
606     * @return integer
607     */
608    private function compileWith($with)
609    {
610        static $mapping = array('rule' => self::WITH_RULE, 'media' => self::WITH_MEDIA, 'supports' => self::WITH_SUPPORTS, 'all' => self::WITH_ALL);
611        // exclude selectors by default
612        $without = self::WITH_RULE;
613        if ($this->libMapHasKey(array($with, self::$with))) {
614            $without = self::WITH_ALL;
615            $list = $this->coerceList($this->libMapGet(array($with, self::$with)));
616            foreach ($list[2] as $item) {
617                $keyword = $this->compileStringContent($this->coerceString($item));
618                if (array_key_exists($keyword, $mapping)) {
619                    $without &= ~$mapping[$keyword];
620                }
621            }
622        }
623        if ($this->libMapHasKey(array($with, self::$without))) {
624            $without = 0;
625            $list = $this->coerceList($this->libMapGet(array($with, self::$without)));
626            foreach ($list[2] as $item) {
627                $keyword = $this->compileStringContent($this->coerceString($item));
628                if (array_key_exists($keyword, $mapping)) {
629                    $without |= $mapping[$keyword];
630                }
631            }
632        }
633        return $without;
634    }
635    /**
636     * Filter env stack
637     *
638     * @param array   $envs
639     * @param integer $without
640     *
641     * @return \Leafo\ScssPhp\Compiler\Environment
642     */
643    private function filterWithout($envs, $without)
644    {
645        $filtered = array();
646        foreach ($envs as $e) {
647            if ($e->block && $this->isWithout($without, $e->block)) {
648                continue;
649            }
650            $filtered[] = $e;
651        }
652        return $this->extractEnv($filtered);
653    }
654    /**
655     * Filter WITH rules
656     *
657     * @param integer              $without
658     * @param \Leafo\ScssPhp\Block $block
659     *
660     * @return boolean
661     */
662    private function isWithout($without, Block $block)
663    {
664        if ($without & self::WITH_RULE && isset($block->selectors) || $without & self::WITH_MEDIA && isset($block->type) && $block->type === Type::T_MEDIA || $without & self::WITH_SUPPORTS && isset($block->type) && $block->type === Type::T_DIRECTIVE && isset($block->name) && $block->name === 'supports') {
665            return true;
666        }
667        return false;
668    }
669    /**
670     * Compile keyframe block
671     *
672     * @param \Leafo\ScssPhp\Block $block
673     * @param array                $selectors
674     */
675    protected function compileKeyframeBlock(Block $block, $selectors)
676    {
677        $env = $this->pushEnv($block);
678        $envs = $this->compactEnv($env);
679        $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
680            return !isset($e->block->selectors);
681        }));
682        $this->scope = $this->makeOutputBlock($block->type, $selectors);
683        $this->scope->depth = 1;
684        $this->scope->parent->children[] = $this->scope;
685        $this->compileChildrenNoReturn($block->children, $this->scope);
686        $this->scope = $this->scope->parent;
687        $this->env = $this->extractEnv($envs);
688        $this->popEnv();
689    }
690    /**
691     * Compile nested block
692     *
693     * @param \Leafo\ScssPhp\Block $block
694     * @param array                $selectors
695     */
696    protected function compileNestedBlock(Block $block, $selectors)
697    {
698        $this->pushEnv($block);
699        $this->scope = $this->makeOutputBlock($block->type, $selectors);
700        $this->scope->parent->children[] = $this->scope;
701        $this->compileChildrenNoReturn($block->children, $this->scope);
702        $this->scope = $this->scope->parent;
703        $this->popEnv();
704    }
705    /**
706     * Recursively compiles a block.
707     *
708     * A block is analogous to a CSS block in most cases. A single SCSS document
709     * is encapsulated in a block when parsed, but it does not have parent tags
710     * so all of its children appear on the root level when compiled.
711     *
712     * Blocks are made up of selectors and children.
713     *
714     * The children of a block are just all the blocks that are defined within.
715     *
716     * Compiling the block involves pushing a fresh environment on the stack,
717     * and iterating through the props, compiling each one.
718     *
719     * @see Compiler::compileChild()
720     *
721     * @param \Leafo\ScssPhp\Block $block
722     */
723    protected function compileBlock(Block $block)
724    {
725        $env = $this->pushEnv($block);
726        $env->selectors = $this->evalSelectors($block->selectors);
727        $out = $this->makeOutputBlock(null);
728        if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
729            $annotation = $this->makeOutputBlock(Type::T_COMMENT);
730            $annotation->depth = 0;
731            $file = $this->sourceNames[$block->sourceIndex];
732            $line = $block->sourceLine;
733            switch ($this->lineNumberStyle) {
734                case self::LINE_COMMENTS:
735                    $annotation->lines[] = '/* line ' . $line . ', ' . $file . ' */';
736                    break;
737                case self::DEBUG_INFO:
738                    $annotation->lines[] = '@media -sass-debug-info{filename{font-family:"' . $file . '"}line{font-family:' . $line . '}}';
739                    break;
740            }
741            $this->scope->children[] = $annotation;
742        }
743        $this->scope->children[] = $out;
744        if (count($block->children)) {
745            $out->selectors = $this->multiplySelectors($env);
746            $this->compileChildrenNoReturn($block->children, $out);
747        }
748        $this->formatter->stripSemicolon($out->lines);
749        $this->popEnv();
750    }
751    /**
752     * Compile root level comment
753     *
754     * @param array $block
755     */
756    protected function compileComment($block)
757    {
758        $out = $this->makeOutputBlock(Type::T_COMMENT);
759        $out->lines[] = $block[1];
760        $this->scope->children[] = $out;
761    }
762    /**
763     * Evaluate selectors
764     *
765     * @param array $selectors
766     *
767     * @return array
768     */
769    protected function evalSelectors($selectors)
770    {
771        $this->shouldEvaluate = false;
772        $selectors = array_map(array($this, 'evalSelector'), $selectors);
773        // after evaluating interpolates, we might need a second pass
774        if ($this->shouldEvaluate) {
775            $buffer = $this->collapseSelectors($selectors);
776            $parser = $this->parserFactory(__METHOD__);
777            if ($parser->parseSelector($buffer, $newSelectors)) {
778                $selectors = array_map(array($this, 'evalSelector'), $newSelectors);
779            }
780        }
781        return $selectors;
782    }
783    /**
784     * Evaluate selector
785     *
786     * @param array $selector
787     *
788     * @return array
789     */
790    protected function evalSelector($selector)
791    {
792        return array_map(array($this, 'evalSelectorPart'), $selector);
793    }
794    /**
795     * Evaluate selector part; replaces all the interpolates, stripping quotes
796     *
797     * @param array $part
798     *
799     * @return array
800     */
801    protected function evalSelectorPart($part)
802    {
803        foreach ($part as &$p) {
804            if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
805                $p = $this->compileValue($p);
806                // force re-evaluation
807                if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
808                    $this->shouldEvaluate = true;
809                }
810            } elseif (is_string($p) && strlen($p) >= 2 && ($first = $p[0]) && ($first === '"' || $first === '\'') && substr($p, -1) === $first) {
811                $p = substr($p, 1, -1);
812            }
813        }
814        return $this->flattenSelectorSingle($part);
815    }
816    /**
817     * Collapse selectors
818     *
819     * @param array $selectors
820     *
821     * @return string
822     */
823    protected function collapseSelectors($selectors)
824    {
825        $parts = array();
826        foreach ($selectors as $selector) {
827            $output = '';
828            array_walk_recursive($selector, function ($value, $key) use(&$output) {
829                $output .= $value;
830            });
831            $parts[] = $output;
832        }
833        return implode(', ', $parts);
834    }
835    /**
836     * Flatten selector single; joins together .classes and #ids
837     *
838     * @param array $single
839     *
840     * @return array
841     */
842    protected function flattenSelectorSingle($single)
843    {
844        $joined = array();
845        foreach ($single as $part) {
846            if (empty($joined) || !is_string($part) || preg_match('/[\\[.:#%]/', $part)) {
847                $joined[] = $part;
848                continue;
849            }
850            if (is_array(end($joined))) {
851                $joined[] = $part;
852            } else {
853                $joined[count($joined) - 1] .= $part;
854            }
855        }
856        return $joined;
857    }
858    /**
859     * Compile selector to string; self(&) should have been replaced by now
860     *
861     * @param array $selector
862     *
863     * @return string
864     */
865    protected function compileSelector($selector)
866    {
867        if (!is_array($selector)) {
868            return $selector;
869        }
870        return implode(' ', array_map(array($this, 'compileSelectorPart'), $selector));
871    }
872    /**
873     * Compile selector part
874     *
875     * @param arary $piece
876     *
877     * @return string
878     */
879    protected function compileSelectorPart($piece)
880    {
881        foreach ($piece as &$p) {
882            if (!is_array($p)) {
883                continue;
884            }
885            switch ($p[0]) {
886                case Type::T_SELF:
887                    $p = '&';
888                    break;
889                default:
890                    $p = $this->compileValue($p);
891                    break;
892            }
893        }
894        return implode($piece);
895    }
896    /**
897     * Has selector placeholder?
898     *
899     * @param array $selector
900     *
901     * @return boolean
902     */
903    protected function hasSelectorPlaceholder($selector)
904    {
905        if (!is_array($selector)) {
906            return false;
907        }
908        foreach ($selector as $parts) {
909            foreach ($parts as $part) {
910                if (strlen($part) && '%' === $part[0]) {
911                    return true;
912                }
913            }
914        }
915        return false;
916    }
917    /**
918     * Compile children and return result
919     *
920     * @param array                                $stms
921     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
922     *
923     * @return array
924     */
925    protected function compileChildren($stms, OutputBlock $out)
926    {
927        foreach ($stms as $stm) {
928            $ret = $this->compileChild($stm, $out);
929            if (isset($ret)) {
930                return $ret;
931            }
932        }
933    }
934    /**
935     * Compile children and throw exception if unexpected @return
936     *
937     * @param array                                $stms
938     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
939     *
940     * @throws \Exception
941     */
942    protected function compileChildrenNoReturn($stms, OutputBlock $out)
943    {
944        foreach ($stms as $stm) {
945            $ret = $this->compileChild($stm, $out);
946            if (isset($ret)) {
947                $this->throwError('@return may only be used within a function');
948                return;
949            }
950        }
951    }
952    /**
953     * Compile media query
954     *
955     * @param array $queryList
956     *
957     * @return string
958     */
959    protected function compileMediaQuery($queryList)
960    {
961        $out = '@media';
962        $first = true;
963        foreach ($queryList as $query) {
964            $type = null;
965            $parts = array();
966            foreach ($query as $q) {
967                switch ($q[0]) {
968                    case Type::T_MEDIA_TYPE:
969                        if ($type) {
970                            $type = $this->mergeMediaTypes($type, array_map(array($this, 'compileValue'), array_slice($q, 1)));
971                            if (empty($type)) {
972                                // merge failed
973                                return null;
974                            }
975                        } else {
976                            $type = array_map(array($this, 'compileValue'), array_slice($q, 1));
977                        }
978                        break;
979                    case Type::T_MEDIA_EXPRESSION:
980                        if (isset($q[2])) {
981                            $parts[] = '(' . $this->compileValue($q[1]) . $this->formatter->assignSeparator . $this->compileValue($q[2]) . ')';
982                        } else {
983                            $parts[] = '(' . $this->compileValue($q[1]) . ')';
984                        }
985                        break;
986                    case Type::T_MEDIA_VALUE:
987                        $parts[] = $this->compileValue($q[1]);
988                        break;
989                }
990            }
991            if ($type) {
992                array_unshift($parts, implode(' ', array_filter($type)));
993            }
994            if (!empty($parts)) {
995                if ($first) {
996                    $first = false;
997                    $out .= ' ';
998                } else {
999                    $out .= $this->formatter->tagSeparator;
1000                }
1001                $out .= implode(' and ', $parts);
1002            }
1003        }
1004        return $out;
1005    }
1006    /**
1007     * Merge media types
1008     *
1009     * @param array $type1
1010     * @param array $type2
1011     *
1012     * @return array|null
1013     */
1014    protected function mergeMediaTypes($type1, $type2)
1015    {
1016        if (empty($type1)) {
1017            return $type2;
1018        }
1019        if (empty($type2)) {
1020            return $type1;
1021        }
1022        $m1 = '';
1023        $t1 = '';
1024        if (count($type1) > 1) {
1025            $m1 = strtolower($type1[0]);
1026            $t1 = strtolower($type1[1]);
1027        } else {
1028            $t1 = strtolower($type1[0]);
1029        }
1030        $m2 = '';
1031        $t2 = '';
1032        if (count($type2) > 1) {
1033            $m2 = strtolower($type2[0]);
1034            $t2 = strtolower($type2[1]);
1035        } else {
1036            $t2 = strtolower($type2[0]);
1037        }
1038        if ($m1 === Type::T_NOT ^ $m2 === Type::T_NOT) {
1039            if ($t1 === $t2) {
1040                return null;
1041            }
1042            return array($m1 === Type::T_NOT ? $m2 : $m1, $m1 === Type::T_NOT ? $t2 : $t1);
1043        }
1044        if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
1045            // CSS has no way of representing "neither screen nor print"
1046            if ($t1 !== $t2) {
1047                return null;
1048            }
1049            return array(Type::T_NOT, $t1);
1050        }
1051        if ($t1 !== $t2) {
1052            return null;
1053        }
1054        // t1 == t2, neither m1 nor m2 are "not"
1055        return array(empty($m1) ? $m2 : $m1, $t1);
1056    }
1057    /**
1058     * Compile import; returns true if the value was something that could be imported
1059     *
1060     * @param array   $rawPath
1061     * @param array   $out
1062     * @param boolean $once
1063     *
1064     * @return boolean
1065     */
1066    protected function compileImport($rawPath, $out, $once = false)
1067    {
1068        if ($rawPath[0] === Type::T_STRING) {
1069            $path = $this->compileStringContent($rawPath);
1070            if ($path = $this->findImport($path)) {
1071                if (!$once || !in_array($path, $this->importedFiles)) {
1072                    $this->importFile($path, $out);
1073                    $this->importedFiles[] = $path;
1074                }
1075                return true;
1076            }
1077            return false;
1078        }
1079        if ($rawPath[0] === Type::T_LIST) {
1080            // handle a list of strings
1081            if (count($rawPath[2]) === 0) {
1082                return false;
1083            }
1084            foreach ($rawPath[2] as $path) {
1085                if ($path[0] !== Type::T_STRING) {
1086                    return false;
1087                }
1088            }
1089            foreach ($rawPath[2] as $path) {
1090                $this->compileImport($path, $out);
1091            }
1092            return true;
1093        }
1094        return false;
1095    }
1096    /**
1097     * Compile child; returns a value to halt execution
1098     *
1099     * @param array                                $child
1100     * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1101     *
1102     * @return array
1103     */
1104    protected function compileChild($child, OutputBlock $out)
1105    {
1106        $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
1107        $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
1108        $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
1109        switch ($child[0]) {
1110            case Type::T_SCSSPHP_IMPORT_ONCE:
1111                list(, $rawPath) = $child;
1112                $rawPath = $this->reduce($rawPath);
1113                if (!$this->compileImport($rawPath, $out, true)) {
1114                    $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1115                }
1116                break;
1117            case Type::T_IMPORT:
1118                list(, $rawPath) = $child;
1119                $rawPath = $this->reduce($rawPath);
1120                if (!$this->compileImport($rawPath, $out)) {
1121                    $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1122                }
1123                break;
1124            case Type::T_DIRECTIVE:
1125                $this->compileDirective($child[1]);
1126                break;
1127            case Type::T_AT_ROOT:
1128                $this->compileAtRoot($child[1]);
1129                break;
1130            case Type::T_MEDIA:
1131                $this->compileMedia($child[1]);
1132                break;
1133            case Type::T_BLOCK:
1134                $this->compileBlock($child[1]);
1135                break;
1136            case Type::T_CHARSET:
1137                if (!$this->charsetSeen) {
1138                    $this->charsetSeen = true;
1139                    $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
1140                }
1141                break;
1142            case Type::T_ASSIGN:
1143                list(, $name, $value) = $child;
1144                if ($name[0] === Type::T_VARIABLE) {
1145                    $flags = isset($child[3]) ? $child[3] : array();
1146                    $isDefault = in_array('!default', $flags);
1147                    $isGlobal = in_array('!global', $flags);
1148                    if ($isGlobal) {
1149                        $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
1150                        break;
1151                    }
1152                    $shouldSet = $isDefault && (($result = $this->get($name[1], false)) === null || $result === self::$null);
1153                    if (!$isDefault || $shouldSet) {
1154                        $this->set($name[1], $this->reduce($value));
1155                    }
1156                    break;
1157                }
1158                $compiledName = $this->compileValue($name);
1159                // handle shorthand syntax: size / line-height
1160                if ($compiledName === 'font') {
1161                    if ($value[0] === Type::T_EXPRESSION && $value[1] === '/') {
1162                        $value = $this->expToString($value);
1163                    } elseif ($value[0] === Type::T_LIST) {
1164                        foreach ($value[2] as &$item) {
1165                            if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
1166                                $item = $this->expToString($item);
1167                            }
1168                        }
1169                    }
1170                }
1171                // if the value reduces to null from something else then
1172                // the property should be discarded
1173                if ($value[0] !== Type::T_NULL) {
1174                    $value = $this->reduce($value);
1175                    if ($value[0] === Type::T_NULL || $value === self::$nullString) {
1176                        break;
1177                    }
1178                }
1179                $compiledValue = $this->compileValue($value);
1180                $out->lines[] = $this->formatter->property($compiledName, $compiledValue);
1181                break;
1182            case Type::T_COMMENT:
1183                if ($out->type === Type::T_ROOT) {
1184                    $this->compileComment($child);
1185                    break;
1186                }
1187                $out->lines[] = $child[1];
1188                break;
1189            case Type::T_MIXIN:
1190            case Type::T_FUNCTION:
1191                list(, $block) = $child;
1192                $this->set(self::$namespaces[$block->type] . $block->name, $block);
1193                break;
1194            case Type::T_EXTEND:
1195                list(, $selectors) = $child;
1196                foreach ($selectors as $sel) {
1197                    $results = $this->evalSelectors(array($sel));
1198                    foreach ($results as $result) {
1199                        // only use the first one
1200                        $result = current($result);
1201                        $this->pushExtends($result, $out->selectors, $child);
1202                    }
1203                }
1204                break;
1205            case Type::T_IF:
1206                list(, $if) = $child;
1207                if ($this->isTruthy($this->reduce($if->cond, true))) {
1208                    return $this->compileChildren($if->children, $out);
1209                }
1210                foreach ($if->cases as $case) {
1211                    if ($case->type === Type::T_ELSE || $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))) {
1212                        return $this->compileChildren($case->children, $out);
1213                    }
1214                }
1215                break;
1216            case Type::T_EACH:
1217                list(, $each) = $child;
1218                $list = $this->coerceList($this->reduce($each->list));
1219                $this->pushEnv();
1220                foreach ($list[2] as $item) {
1221                    if (count($each->vars) === 1) {
1222                        $this->set($each->vars[0], $item, true);
1223                    } else {
1224                        list(, , $values) = $this->coerceList($item);
1225                        foreach ($each->vars as $i => $var) {
1226                            $this->set($var, isset($values[$i]) ? $values[$i] : self::$null, true);
1227                        }
1228                    }
1229                    $ret = $this->compileChildren($each->children, $out);
1230                    if ($ret) {
1231                        if ($ret[0] !== Type::T_CONTROL) {
1232                            $this->popEnv();
1233                            return $ret;
1234                        }
1235                        if ($ret[1]) {
1236                            break;
1237                        }
1238                    }
1239                }
1240                $this->popEnv();
1241                break;
1242            case Type::T_WHILE:
1243                list(, $while) = $child;
1244                while ($this->isTruthy($this->reduce($while->cond, true))) {
1245                    $ret = $this->compileChildren($while->children, $out);
1246                    if ($ret) {
1247                        if ($ret[0] !== Type::T_CONTROL) {
1248                            return $ret;
1249                        }
1250                        if ($ret[1]) {
1251                            break;
1252                        }
1253                    }
1254                }
1255                break;
1256            case Type::T_FOR:
1257                list(, $for) = $child;
1258                $start = $this->reduce($for->start, true);
1259                $start = $start[1];
1260                $end = $this->reduce($for->end, true);
1261                $end = $end[1];
1262                $d = $start < $end ? 1 : -1;
1263                while (true) {
1264                    if (!$for->until && $start - $d == $end || $for->until && $start == $end) {
1265                        break;
1266                    }
1267                    $this->set($for->var, new Node\Number($start, ''));
1268                    $start += $d;
1269                    $ret = $this->compileChildren($for->children, $out);
1270                    if ($ret) {
1271                        if ($ret[0] !== Type::T_CONTROL) {
1272                            return $ret;
1273                        }
1274                        if ($ret[1]) {
1275                            break;
1276                        }
1277                    }
1278                }
1279                break;
1280            case Type::T_BREAK:
1281                return array(Type::T_CONTROL, true);
1282            case Type::T_CONTINUE:
1283                return array(Type::T_CONTROL, false);
1284            case Type::T_RETURN:
1285                return $this->reduce($child[1], true);
1286            case Type::T_NESTED_PROPERTY:
1287                list(, $prop) = $child;
1288                $prefixed = array();
1289                $prefix = $this->compileValue($prop->prefix) . '-';
1290                foreach ($prop->children as $child) {
1291                    switch ($child[0]) {
1292                        case Type::T_ASSIGN:
1293                            array_unshift($child[1][2], $prefix);
1294                            break;
1295                        case Type::T_NESTED_PROPERTY:
1296                            array_unshift($child[1]->prefix[2], $prefix);
1297                            break;
1298                    }
1299                    $prefixed[] = $child;
1300                }
1301                $this->compileChildrenNoReturn($prefixed, $out);
1302                break;
1303            case Type::T_INCLUDE:
1304                // including a mixin
1305                list(, $name, $argValues, $content) = $child;
1306                $mixin = $this->get(self::$namespaces['mixin'] . $name, false);
1307                if (!$mixin) {
1308                    $this->throwError("Undefined mixin {$name}");
1309                    break;
1310                }
1311                $callingScope = $this->getStoreEnv();
1312                // push scope, apply args
1313                $this->pushEnv();
1314                $this->env->depth--;
1315                if (isset($content)) {
1316                    $content->scope = $callingScope;
1317                    $this->setRaw(self::$namespaces['special'] . 'content', $content, $this->env);
1318                }
1319                if (isset($mixin->args)) {
1320                    $this->applyArguments($mixin->args, $argValues);
1321                }
1322                $this->env->marker = 'mixin';
1323                $this->compileChildrenNoReturn($mixin->children, $out);
1324                $this->popEnv();
1325                break;
1326            case Type::T_MIXIN_CONTENT:
1327                $content = $this->get(self::$namespaces['special'] . 'content', false, $this->getStoreEnv()) ?: $this->get(self::$namespaces['special'] . 'content', false, $this->env);
1328                if (!$content) {
1329                    $this->throwError('Expected @content inside of mixin');
1330                    break;
1331                }
1332                $storeEnv = $this->storeEnv;
1333                $this->storeEnv = $content->scope;
1334                $this->compileChildrenNoReturn($content->children, $out);
1335                $this->storeEnv = $storeEnv;
1336                break;
1337            case Type::T_DEBUG:
1338                list(, $value) = $child;
1339                $line = $this->sourceLine;
1340                $value = $this->compileValue($this->reduce($value, true));
1341                fwrite($this->stderr, "Line {$line} DEBUG: {$value}\n");
1342                break;
1343            case Type::T_WARN:
1344                list(, $value) = $child;
1345                $line = $this->sourceLine;
1346                $value = $this->compileValue($this->reduce($value, true));
1347                fwrite($this->stderr, "Line {$line} WARN: {$value}\n");
1348                break;
1349            case Type::T_ERROR:
1350                list(, $value) = $child;
1351                $line = $this->sourceLine;
1352                $value = $this->compileValue($this->reduce($value, true));
1353                $this->throwError("Line {$line} ERROR: {$value}\n");
1354                break;
1355            case Type::T_CONTROL:
1356                $this->throwError('@break/@continue not permitted in this scope');
1357                break;
1358            default:
1359                $this->throwError("unknown child type: {$child['0']}");
1360        }
1361    }
1362    /**
1363     * Reduce expression to string
1364     *
1365     * @param array $exp
1366     *
1367     * @return array
1368     */
1369    protected function expToString($exp)
1370    {
1371        list(, $op, $left, $right, , $whiteLeft, $whiteRight) = $exp;
1372        $content = array($this->reduce($left));
1373        if ($whiteLeft) {
1374            $content[] = ' ';
1375        }
1376        $content[] = $op;
1377        if ($whiteRight) {
1378            $content[] = ' ';
1379        }
1380        $content[] = $this->reduce($right);
1381        return array(Type::T_STRING, '', $content);
1382    }
1383    /**
1384     * Is truthy?
1385     *
1386     * @param array $value
1387     *
1388     * @return array
1389     */
1390    protected function isTruthy($value)
1391    {
1392        return $value !== self::$false && $value !== self::$null;
1393    }
1394    /**
1395     * Should $value cause its operand to eval
1396     *
1397     * @param array $value
1398     *
1399     * @return boolean
1400     */
1401    protected function shouldEval($value)
1402    {
1403        switch ($value[0]) {
1404            case Type::T_EXPRESSION:
1405                if ($value[1] === '/') {
1406                    return $this->shouldEval($value[2], $value[3]);
1407                }
1408            // fall-thru
1409            case Type::T_VARIABLE:
1410            case Type::T_FUNCTION_CALL:
1411                return true;
1412        }
1413        return false;
1414    }
1415    /**
1416     * Reduce value
1417     *
1418     * @param array   $value
1419     * @param boolean $inExp
1420     *
1421     * @return array
1422     */
1423    protected function reduce($value, $inExp = false)
1424    {
1425        list($type) = $value;
1426        switch ($type) {
1427            case Type::T_EXPRESSION:
1428                list(, $op, $left, $right, $inParens) = $value;
1429                $opName = isset(self::$operatorNames[$op]) ? self::$operatorNames[$op] : $op;
1430                $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
1431                $left = $this->reduce($left, true);
1432                if ($op !== 'and' && $op !== 'or') {
1433                    $right = $this->reduce($right, true);
1434                }
1435                // special case: looks like css shorthand
1436                if ($opName == 'div' && !$inParens && !$inExp && isset($right[2]) && ($right[0] !== Type::T_NUMBER && $right[2] != '' || $right[0] === Type::T_NUMBER && !$right->unitless())) {
1437                    return $this->expToString($value);
1438                }
1439                $left = $this->coerceForExpression($left);
1440                $right = $this->coerceForExpression($right);
1441                $ltype = $left[0];
1442                $rtype = $right[0];
1443                $ucOpName = ucfirst($opName);
1444                $ucLType = ucfirst($ltype);
1445                $ucRType = ucfirst($rtype);
1446                // this tries:
1447                // 1. op[op name][left type][right type]
1448                // 2. op[left type][right type] (passing the op as first arg
1449                // 3. op[op name]
1450                $fn = "op{$ucOpName}{$ucLType}{$ucRType}";
1451                if (is_callable(array($this, $fn)) || ($fn = "op{$ucLType}{$ucRType}") && is_callable(array($this, $fn)) && ($passOp = true) || ($fn = "op{$ucOpName}") && is_callable(array($this, $fn)) && ($genOp = true)) {
1452                    $coerceUnit = false;
1453                    if (!isset($genOp) && $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER) {
1454                        $coerceUnit = true;
1455                        switch ($opName) {
1456                            case 'mul':
1457                                $targetUnit = $left[2];
1458                                foreach ($right[2] as $unit => $exp) {
1459                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
1460                                }
1461                                break;
1462                            case 'div':
1463                                $targetUnit = $left[2];
1464                                foreach ($right[2] as $unit => $exp) {
1465                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
1466                                }
1467                                break;
1468                            case 'mod':
1469                                $targetUnit = $left[2];
1470                                break;
1471                            default:
1472                                $targetUnit = $left->unitless() ? $right[2] : $left[2];
1473                        }
1474                        if (!$left->unitless() && !$right->unitless()) {
1475                            $left = $left->normalize();
1476                            $right = $right->normalize();
1477                        }
1478                    }
1479                    $shouldEval = $inParens || $inExp;
1480                    if (isset($passOp)) {
1481                        $out = $this->{$fn}($op, $left, $right, $shouldEval);
1482                    } else {
1483                        $out = $this->{$fn}($left, $right, $shouldEval);
1484                    }
1485                    if (isset($out)) {
1486                        if ($coerceUnit && $out[0] === Type::T_NUMBER) {
1487                            $out = $out->coerce($targetUnit);
1488                        }
1489                        return $out;
1490                    }
1491                }
1492                return $this->expToString($value);
1493            case Type::T_UNARY:
1494                list(, $op, $exp, $inParens) = $value;
1495                $inExp = $inExp || $this->shouldEval($exp);
1496                $exp = $this->reduce($exp);
1497                if ($exp[0] === Type::T_NUMBER) {
1498                    switch ($op) {
1499                        case '+':
1500                            return new Node\Number($exp[1], $exp[2]);
1501                        case '-':
1502                            return new Node\Number(-$exp[1], $exp[2]);
1503                    }
1504                }
1505                if ($op === 'not') {
1506                    if ($inExp || $inParens) {
1507                        if ($exp === self::$false || $exp === self::$null) {
1508                            return self::$true;
1509                        }
1510                        return self::$false;
1511                    }
1512                    $op = $op . ' ';
1513                }
1514                return array(Type::T_STRING, '', array($op, $exp));
1515            case Type::T_VARIABLE:
1516                list(, $name) = $value;
1517                return $this->reduce($this->get($name));
1518            case Type::T_LIST:
1519                foreach ($value[2] as &$item) {
1520                    $item = $this->reduce($item);
1521                }
1522                return $value;
1523            case Type::T_MAP:
1524                foreach ($value[1] as &$item) {
1525                    $item = $this->reduce($item);
1526                }
1527                foreach ($value[2] as &$item) {
1528                    $item = $this->reduce($item);
1529                }
1530                return $value;
1531            case Type::T_STRING:
1532                foreach ($value[2] as &$item) {
1533                    if (is_array($item) || $item instanceof \ArrayAccess) {
1534                        $item = $this->reduce($item);
1535                    }
1536                }
1537                return $value;
1538            case Type::T_INTERPOLATE:
1539                $value[1] = $this->reduce($value[1]);
1540                return $value;
1541            case Type::T_FUNCTION_CALL:
1542                list(, $name, $argValues) = $value;
1543                return $this->fncall($name, $argValues);
1544            default:
1545                return $value;
1546        }
1547    }
1548    /**
1549     * Function caller
1550     *
1551     * @param string $name
1552     * @param array  $argValues
1553     *
1554     * @return array|null
1555     */
1556    private function fncall($name, $argValues)
1557    {
1558        // SCSS @function
1559        if ($this->callScssFunction($name, $argValues, $returnValue)) {
1560            return $returnValue;
1561        }
1562        // native PHP functions
1563        if ($this->callNativeFunction($name, $argValues, $returnValue)) {
1564            return $returnValue;
1565        }
1566        // for CSS functions, simply flatten the arguments into a list
1567        $listArgs = array();
1568        foreach ((array) $argValues as $arg) {
1569            if (empty($arg[0])) {
1570                $listArgs[] = $this->reduce($arg[1]);
1571            }
1572        }
1573        return array(Type::T_FUNCTION, $name, array(Type::T_LIST, ',', $listArgs));
1574    }
1575    /**
1576     * Normalize name
1577     *
1578     * @param string $name
1579     *
1580     * @return string
1581     */
1582    protected function normalizeName($name)
1583    {
1584        return str_replace('-', '_', $name);
1585    }
1586    /**
1587     * Normalize value
1588     *
1589     * @param array $value
1590     *
1591     * @return array
1592     */
1593    public function normalizeValue($value)
1594    {
1595        $value = $this->coerceForExpression($this->reduce($value));
1596        list($type) = $value;
1597        switch ($type) {
1598            case Type::T_LIST:
1599                $value = $this->extractInterpolation($value);
1600                if ($value[0] !== Type::T_LIST) {
1601                    return array(Type::T_KEYWORD, $this->compileValue($value));
1602                }
1603                foreach ($value[2] as $key => $item) {
1604                    $value[2][$key] = $this->normalizeValue($item);
1605                }
1606                return $value;
1607            case Type::T_STRING:
1608                return array($type, '"', array($this->compileStringContent($value)));
1609            case Type::T_NUMBER:
1610                return $value->normalize();
1611            case Type::T_INTERPOLATE:
1612                return array(Type::T_KEYWORD, $this->compileValue($value));
1613            default:
1614                return $value;
1615        }
1616    }
1617    /**
1618     * Add numbers
1619     *
1620     * @param array $left
1621     * @param array $right
1622     *
1623     * @return array
1624     */
1625    protected function opAddNumberNumber($left, $right)
1626    {
1627        return new Node\Number($left[1] + $right[1], $left[2]);
1628    }
1629    /**
1630     * Multiply numbers
1631     *
1632     * @param array $left
1633     * @param array $right
1634     *
1635     * @return array
1636     */
1637    protected function opMulNumberNumber($left, $right)
1638    {
1639        return new Node\Number($left[1] * $right[1], $left[2]);
1640    }
1641    /**
1642     * Subtract numbers
1643     *
1644     * @param array $left
1645     * @param array $right
1646     *
1647     * @return array
1648     */
1649    protected function opSubNumberNumber($left, $right)
1650    {
1651        return new Node\Number($left[1] - $right[1], $left[2]);
1652    }
1653    /**
1654     * Divide numbers
1655     *
1656     * @param array $left
1657     * @param array $right
1658     *
1659     * @return array
1660     */
1661    protected function opDivNumberNumber($left, $right)
1662    {
1663        if ($right[1] == 0) {
1664            return array(Type::T_STRING, '', array($left[1] . $left[2] . '/' . $right[1] . $right[2]));
1665        }
1666        return new Node\Number($left[1] / $right[1], $left[2]);
1667    }
1668    /**
1669     * Mod numbers
1670     *
1671     * @param array $left
1672     * @param array $right
1673     *
1674     * @return array
1675     */
1676    protected function opModNumberNumber($left, $right)
1677    {
1678        return new Node\Number($left[1] % $right[1], $left[2]);
1679    }
1680    /**
1681     * Add strings
1682     *
1683     * @param array $left
1684     * @param array $right
1685     *
1686     * @return array
1687     */
1688    protected function opAdd($left, $right)
1689    {
1690        if ($strLeft = $this->coerceString($left)) {
1691            if ($right[0] === Type::T_STRING) {
1692                $right[1] = '';
1693            }
1694            $strLeft[2][] = $right;
1695            return $strLeft;
1696        }
1697        if ($strRight = $this->coerceString($right)) {
1698            if ($left[0] === Type::T_STRING) {
1699                $left[1] = '';
1700            }
1701            array_unshift($strRight[2], $left);
1702            return $strRight;
1703        }
1704    }
1705    /**
1706     * Boolean and
1707     *
1708     * @param array   $left
1709     * @param array   $right
1710     * @param boolean $shouldEval
1711     *
1712     * @return array
1713     */
1714    protected function opAnd($left, $right, $shouldEval)
1715    {
1716        if (!$shouldEval) {
1717            return;
1718        }
1719        if ($left !== self::$false and $left !== self::$null) {
1720            return $this->reduce($right, true);
1721        }
1722        return $left;
1723    }
1724    /**
1725     * Boolean or
1726     *
1727     * @param array   $left
1728     * @param array   $right
1729     * @param boolean $shouldEval
1730     *
1731     * @return array
1732     */
1733    protected function opOr($left, $right, $shouldEval)
1734    {
1735        if (!$shouldEval) {
1736            return;
1737        }
1738        if ($left !== self::$false and $left !== self::$null) {
1739            return $left;
1740        }
1741        return $this->reduce($right, true);
1742    }
1743    /**
1744     * Compare colors
1745     *
1746     * @param string $op
1747     * @param array  $left
1748     * @param array  $right
1749     *
1750     * @return array
1751     */
1752    protected function opColorColor($op, $left, $right)
1753    {
1754        $out = array(Type::T_COLOR);
1755        foreach (array(1, 2, 3) as $i) {
1756            $lval = isset($left[$i]) ? $left[$i] : 0;
1757            $rval = isset($right[$i]) ? $right[$i] : 0;
1758            switch ($op) {
1759                case '+':
1760                    $out[] = $lval + $rval;
1761                    break;
1762                case '-':
1763                    $out[] = $lval - $rval;
1764                    break;
1765                case '*':
1766                    $out[] = $lval * $rval;
1767                    break;
1768                case '%':
1769                    $out[] = $lval % $rval;
1770                    break;
1771                case '/':
1772                    if ($rval == 0) {
1773                        $this->throwError('color: Can\'t divide by zero');
1774                        break 2;
1775                    }
1776                    $out[] = (int) ($lval / $rval);
1777                    break;
1778                case '==':
1779                    return $this->opEq($left, $right);
1780                case '!=':
1781                    return $this->opNeq($left, $right);
1782                default:
1783                    $this->throwError("color: unknown op {$op}");
1784                    break 2;
1785            }
1786        }
1787        if (isset($left[4])) {
1788            $out[4] = $left[4];
1789        } elseif (isset($right[4])) {
1790            $out[4] = $right[4];
1791        }
1792        return $this->fixColor($out);
1793    }
1794    /**
1795     * Compare color and number
1796     *
1797     * @param string $op
1798     * @param array  $left
1799     * @param array  $right
1800     *
1801     * @return array
1802     */
1803    protected function opColorNumber($op, $left, $right)
1804    {
1805        $value = $right[1];
1806        return $this->opColorColor($op, $left, array(Type::T_COLOR, $value, $value, $value));
1807    }
1808    /**
1809     * Compare number and color
1810     *
1811     * @param string $op
1812     * @param array  $left
1813     * @param array  $right
1814     *
1815     * @return array
1816     */
1817    protected function opNumberColor($op, $left, $right)
1818    {
1819        $value = $left[1];
1820        return $this->opColorColor($op, array(Type::T_COLOR, $value, $value, $value), $right);
1821    }
1822    /**
1823     * Compare number1 == number2
1824     *
1825     * @param array $left
1826     * @param array $right
1827     *
1828     * @return array
1829     */
1830    protected function opEq($left, $right)
1831    {
1832        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1833            $lStr[1] = '';
1834            $rStr[1] = '';
1835            $left = $this->compileValue($lStr);
1836            $right = $this->compileValue($rStr);
1837        }
1838        return $this->toBool($left === $right);
1839    }
1840    /**
1841     * Compare number1 != number2
1842     *
1843     * @param array $left
1844     * @param array $right
1845     *
1846     * @return array
1847     */
1848    protected function opNeq($left, $right)
1849    {
1850        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
1851            $lStr[1] = '';
1852            $rStr[1] = '';
1853            $left = $this->compileValue($lStr);
1854            $right = $this->compileValue($rStr);
1855        }
1856        return $this->toBool($left !== $right);
1857    }
1858    /**
1859     * Compare number1 >= number2
1860     *
1861     * @param array $left
1862     * @param array $right
1863     *
1864     * @return array
1865     */
1866    protected function opGteNumberNumber($left, $right)
1867    {
1868        return $this->toBool($left[1] >= $right[1]);
1869    }
1870    /**
1871     * Compare number1 > number2
1872     *
1873     * @param array $left
1874     * @param array $right
1875     *
1876     * @return array
1877     */
1878    protected function opGtNumberNumber($left, $right)
1879    {
1880        return $this->toBool($left[1] > $right[1]);
1881    }
1882    /**
1883     * Compare number1 <= number2
1884     *
1885     * @param array $left
1886     * @param array $right
1887     *
1888     * @return array
1889     */
1890    protected function opLteNumberNumber($left, $right)
1891    {
1892        return $this->toBool($left[1] <= $right[1]);
1893    }
1894    /**
1895     * Compare number1 < number2
1896     *
1897     * @param array $left
1898     * @param array $right
1899     *
1900     * @return array
1901     */
1902    protected function opLtNumberNumber($left, $right)
1903    {
1904        return $this->toBool($left[1] < $right[1]);
1905    }
1906    /**
1907     * Three-way comparison, aka spaceship operator
1908     *
1909     * @param array $left
1910     * @param array $right
1911     *
1912     * @return array
1913     */
1914    protected function opCmpNumberNumber($left, $right)
1915    {
1916        $n = $left[1] - $right[1];
1917        return new Node\Number($n ? $n / abs($n) : 0, '');
1918    }
1919    /**
1920     * Cast to boolean
1921     *
1922     * @api
1923     *
1924     * @param mixed $thing
1925     *
1926     * @return array
1927     */
1928    public function toBool($thing)
1929    {
1930        return $thing ? self::$true : self::$false;
1931    }
1932    /**
1933     * Compiles a primitive value into a CSS property value.
1934     *
1935     * Values in scssphp are typed by being wrapped in arrays, their format is
1936     * typically:
1937     *
1938     *     array(type, contents [, additional_contents]*)
1939     *
1940     * The input is expected to be reduced. This function will not work on
1941     * things like expressions and variables.
1942     *
1943     * @api
1944     *
1945     * @param array $value
1946     *
1947     * @return string
1948     */
1949    public function compileValue($value)
1950    {
1951        $value = $this->reduce($value);
1952        list($type) = $value;
1953        switch ($type) {
1954            case Type::T_KEYWORD:
1955                return $value[1];
1956            case Type::T_COLOR:
1957                // [1] - red component (either number for a %)
1958                // [2] - green component
1959                // [3] - blue component
1960                // [4] - optional alpha component
1961                list(, $r, $g, $b) = $value;
1962                $r = round($r);
1963                $g = round($g);
1964                $b = round($b);
1965                if (count($value) === 5 && $value[4] !== 1) {
1966                    // rgba
1967                    return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $value[4] . ')';
1968                }
1969                $h = sprintf('#%02x%02x%02x', $r, $g, $b);
1970                // Converting hex color to short notation (e.g. #003399 to #039)
1971                if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
1972                    $h = '#' . $h[1] . $h[3] . $h[5];
1973                }
1974                return $h;
1975            case Type::T_NUMBER:
1976                return $value->output($this);
1977            case Type::T_STRING:
1978                return $value[1] . $this->compileStringContent($value) . $value[1];
1979            case Type::T_FUNCTION:
1980                $args = !empty($value[2]) ? $this->compileValue($value[2]) : '';
1981                return "{$value['1']}({$args})";
1982            case Type::T_LIST:
1983                $value = $this->extractInterpolation($value);
1984                if ($value[0] !== Type::T_LIST) {
1985                    return $this->compileValue($value);
1986                }
1987                list(, $delim, $items) = $value;
1988                if ($delim !== ' ') {
1989                    $delim .= ' ';
1990                }
1991                $filtered = array();
1992                foreach ($items as $item) {
1993                    if ($item[0] === Type::T_NULL) {
1994                        continue;
1995                    }
1996                    $filtered[] = $this->compileValue($item);
1997                }
1998                return implode("{$delim}", $filtered);
1999            case Type::T_MAP:
2000                $keys = $value[1];
2001                $values = $value[2];
2002                $filtered = array();
2003                for ($i = 0, $s = count($keys); $i < $s; $i++) {
2004                    $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
2005                }
2006                array_walk($filtered, function (&$value, $key) {
2007                    $value = $key . ': ' . $value;
2008                });
2009                return '(' . implode(', ', $filtered) . ')';
2010            case Type::T_INTERPOLATED:
2011                // node created by extractInterpolation
2012                list(, $interpolate, $left, $right) = $value;
2013                list(, , $whiteLeft, $whiteRight) = $interpolate;
2014                $left = count($left[2]) > 0 ? $this->compileValue($left) . $whiteLeft : '';
2015                $right = count($right[2]) > 0 ? $whiteRight . $this->compileValue($right) : '';
2016                return $left . $this->compileValue($interpolate) . $right;
2017            case Type::T_INTERPOLATE:
2018                // raw parse node
2019                list(, $exp) = $value;
2020                // strip quotes if it's a string
2021                $reduced = $this->reduce($exp);
2022                switch ($reduced[0]) {
2023                    case Type::T_STRING:
2024                        $reduced = array(Type::T_KEYWORD, $this->compileStringContent($reduced));
2025                        break;
2026                    case Type::T_NULL:
2027                        $reduced = array(Type::T_KEYWORD, '');
2028                }
2029                return $this->compileValue($reduced);
2030            case Type::T_NULL:
2031                return 'null';
2032            default:
2033                $this->throwError("unknown value type: {$type}");
2034        }
2035    }
2036    /**
2037     * Flatten list
2038     *
2039     * @param array $list
2040     *
2041     * @return string
2042     */
2043    protected function flattenList($list)
2044    {
2045        return $this->compileValue($list);
2046    }
2047    /**
2048     * Compile string content
2049     *
2050     * @param array $string
2051     *
2052     * @return string
2053     */
2054    protected function compileStringContent($string)
2055    {
2056        $parts = array();
2057        foreach ($string[2] as $part) {
2058            if (is_array($part) || $part instanceof \ArrayAccess) {
2059                $parts[] = $this->compileValue($part);
2060            } else {
2061                $parts[] = $part;
2062            }
2063        }
2064        return implode($parts);
2065    }
2066    /**
2067     * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
2068     *
2069     * @param array $list
2070     *
2071     * @return array
2072     */
2073    protected function extractInterpolation($list)
2074    {
2075        $items = $list[2];
2076        foreach ($items as $i => $item) {
2077            if ($item[0] === Type::T_INTERPOLATE) {
2078                $before = array(Type::T_LIST, $list[1], array_slice($items, 0, $i));
2079                $after = array(Type::T_LIST, $list[1], array_slice($items, $i + 1));
2080                return array(Type::T_INTERPOLATED, $item, $before, $after);
2081            }
2082        }
2083        return $list;
2084    }
2085    /**
2086     * Find the final set of selectors
2087     *
2088     * @param \Leafo\ScssPhp\Compiler\Environment $env
2089     *
2090     * @return array
2091     */
2092    protected function multiplySelectors(Environment $env)
2093    {
2094        $envs = $this->compactEnv($env);
2095        $selectors = array();
2096        $parentSelectors = array(array());
2097        while ($env = array_pop($envs)) {
2098            if (empty($env->selectors)) {
2099                continue;
2100            }
2101            $selectors = array();
2102            foreach ($env->selectors as $selector) {
2103                foreach ($parentSelectors as $parent) {
2104                    $selectors[] = $this->joinSelectors($parent, $selector);
2105                }
2106            }
2107            $parentSelectors = $selectors;
2108        }
2109        return $selectors;
2110    }
2111    /**
2112     * Join selectors; looks for & to replace, or append parent before child
2113     *
2114     * @param array $parent
2115     * @param array $child
2116     *
2117     * @return array
2118     */
2119    protected function joinSelectors($parent, $child)
2120    {
2121        $setSelf = false;
2122        $out = array();
2123        foreach ($child as $part) {
2124            $newPart = array();
2125            foreach ($part as $p) {
2126                if ($p === self::$selfSelector) {
2127                    $setSelf = true;
2128                    foreach ($parent as $i => $parentPart) {
2129                        if ($i > 0) {
2130                            $out[] = $newPart;
2131                            $newPart = array();
2132                        }
2133                        foreach ($parentPart as $pp) {
2134                            $newPart[] = $pp;
2135                        }
2136                    }
2137                } else {
2138                    $newPart[] = $p;
2139                }
2140            }
2141            $out[] = $newPart;
2142        }
2143        return $setSelf ? $out : array_merge($parent, $child);
2144    }
2145    /**
2146     * Multiply media
2147     *
2148     * @param \Leafo\ScssPhp\Compiler\Environment $env
2149     * @param array                               $childQueries
2150     *
2151     * @return array
2152     */
2153    protected function multiplyMedia(Environment $env = null, $childQueries = null)
2154    {
2155        if (!isset($env) || !empty($env->block->type) && $env->block->type !== Type::T_MEDIA) {
2156            return $childQueries;
2157        }
2158        // plain old block, skip
2159        if (empty($env->block->type)) {
2160            return $this->multiplyMedia($env->parent, $childQueries);
2161        }
2162        $parentQueries = isset($env->block->queryList) ? $env->block->queryList : array(array(array(Type::T_MEDIA_VALUE, $env->block->value)));
2163        if ($childQueries === null) {
2164            $childQueries = $parentQueries;
2165        } else {
2166            $originalQueries = $childQueries;
2167            $childQueries = array();
2168            foreach ($parentQueries as $parentQuery) {
2169                foreach ($originalQueries as $childQuery) {
2170                    $childQueries[] = array_merge($parentQuery, $childQuery);
2171                }
2172            }
2173        }
2174        return $this->multiplyMedia($env->parent, $childQueries);
2175    }
2176    /**
2177     * Convert env linked list to stack
2178     *
2179     * @param \Leafo\ScssPhp\Compiler\Environment $env
2180     *
2181     * @return array
2182     */
2183    private function compactEnv(Environment $env)
2184    {
2185        for ($envs = array(); $env; $env = $env->parent) {
2186            $envs[] = $env;
2187        }
2188        return $envs;
2189    }
2190    /**
2191     * Convert env stack to singly linked list
2192     *
2193     * @param array $envs
2194     *
2195     * @return \Leafo\ScssPhp\Compiler\Environment
2196     */
2197    private function extractEnv($envs)
2198    {
2199        for ($env = null; $e = array_pop($envs);) {
2200            $e->parent = $env;
2201            $env = $e;
2202        }
2203        return $env;
2204    }
2205    /**
2206     * Push environment
2207     *
2208     * @param \Leafo\ScssPhp\Block $block
2209     *
2210     * @return \Leafo\ScssPhp\Compiler\Environment
2211     */
2212    protected function pushEnv(Block $block = null)
2213    {
2214        $env = new Environment();
2215        $env->parent = $this->env;
2216        $env->store = array();
2217        $env->block = $block;
2218        $env->depth = isset($this->env->depth) ? $this->env->depth + 1 : 0;
2219        $this->env = $env;
2220        return $env;
2221    }
2222    /**
2223     * Pop environment
2224     */
2225    protected function popEnv()
2226    {
2227        $this->env = $this->env->parent;
2228    }
2229    /**
2230     * Get store environment
2231     *
2232     * @return \Leafo\ScssPhp\Compiler\Environment
2233     */
2234    protected function getStoreEnv()
2235    {
2236        return isset($this->storeEnv) ? $this->storeEnv : $this->env;
2237    }
2238    /**
2239     * Set variable
2240     *
2241     * @param string                              $name
2242     * @param mixed                               $value
2243     * @param boolean                             $shadow
2244     * @param \Leafo\ScssPhp\Compiler\Environment $env
2245     */
2246    protected function set($name, $value, $shadow = false, Environment $env = null)
2247    {
2248        $name = $this->normalizeName($name);
2249        if (!isset($env)) {
2250            $env = $this->getStoreEnv();
2251        }
2252        if ($shadow) {
2253            $this->setRaw($name, $value, $env);
2254        } else {
2255            $this->setExisting($name, $value, $env);
2256        }
2257    }
2258    /**
2259     * Set existing variable
2260     *
2261     * @param string                              $name
2262     * @param mixed                               $value
2263     * @param \Leafo\ScssPhp\Compiler\Environment $env
2264     */
2265    protected function setExisting($name, $value, Environment $env)
2266    {
2267        $storeEnv = $env;
2268        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2269        for (;;) {
2270            if (array_key_exists($name, $env->store)) {
2271                break;
2272            }
2273            if (!$hasNamespace && isset($env->marker)) {
2274                $env = $storeEnv;
2275                break;
2276            }
2277            if (!isset($env->parent)) {
2278                $env = $storeEnv;
2279                break;
2280            }
2281            $env = $env->parent;
2282        }
2283        $env->store[$name] = $value;
2284    }
2285    /**
2286     * Set raw variable
2287     *
2288     * @param string                              $name
2289     * @param mixed                               $value
2290     * @param \Leafo\ScssPhp\Compiler\Environment $env
2291     */
2292    protected function setRaw($name, $value, Environment $env)
2293    {
2294        $env->store[$name] = $value;
2295    }
2296    /**
2297     * Get variable
2298     *
2299     * @api
2300     *
2301     * @param string                              $name
2302     * @param boolean                             $shouldThrow
2303     * @param \Leafo\ScssPhp\Compiler\Environment $env
2304     *
2305     * @return mixed
2306     */
2307    public function get($name, $shouldThrow = true, Environment $env = null)
2308    {
2309        $name = $this->normalizeName($name);
2310        if (!isset($env)) {
2311            $env = $this->getStoreEnv();
2312        }
2313        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
2314        for (;;) {
2315            if (array_key_exists($name, $env->store)) {
2316                return $env->store[$name];
2317            }
2318            if (!$hasNamespace && isset($env->marker)) {
2319                $env = $this->rootEnv;
2320                continue;
2321            }
2322            if (!isset($env->parent)) {
2323                break;
2324            }
2325            $env = $env->parent;
2326        }
2327        if ($shouldThrow) {
2328            $this->throwError("Undefined variable \${$name}");
2329        }
2330    }
2331    /**
2332     * Has variable?
2333     *
2334     * @param string                              $name
2335     * @param \Leafo\ScssPhp\Compiler\Environment $env
2336     *
2337     * @return boolean
2338     */
2339    protected function has($name, Environment $env = null)
2340    {
2341        return $this->get($name, false, $env) !== null;
2342    }
2343    /**
2344     * Inject variables
2345     *
2346     * @param array $args
2347     */
2348    protected function injectVariables(array $args)
2349    {
2350        if (empty($args)) {
2351            return;
2352        }
2353        $parser = $this->parserFactory(__METHOD__);
2354        foreach ($args as $name => $strValue) {
2355            if ($name[0] === '$') {
2356                $name = substr($name, 1);
2357            }
2358            if (!$parser->parseValue($strValue, $value)) {
2359                $value = $this->coerceValue($strValue);
2360            }
2361            $this->set($name, $value);
2362        }
2363    }
2364    /**
2365     * Set variables
2366     *
2367     * @api
2368     *
2369     * @param array $variables
2370     */
2371    public function setVariables(array $variables)
2372    {
2373        $this->registeredVars = array_merge($this->registeredVars, $variables);
2374    }
2375    /**
2376     * Unset variable
2377     *
2378     * @api
2379     *
2380     * @param string $name
2381     */
2382    public function unsetVariable($name)
2383    {
2384        unset($this->registeredVars[$name]);
2385    }
2386    /**
2387     * Returns list of variables
2388     *
2389     * @api
2390     *
2391     * @return array
2392     */
2393    public function getVariables()
2394    {
2395        return $this->registeredVars;
2396    }
2397    /**
2398     * Adds to list of parsed files
2399     *
2400     * @api
2401     *
2402     * @param string $path
2403     */
2404    public function addParsedFile($path)
2405    {
2406        if (isset($path) && file_exists($path)) {
2407            $this->parsedFiles[realpath($path)] = filemtime($path);
2408        }
2409    }
2410    /**
2411     * Returns list of parsed files
2412     *
2413     * @api
2414     *
2415     * @return array
2416     */
2417    public function getParsedFiles()
2418    {
2419        return $this->parsedFiles;
2420    }
2421    /**
2422     * Add import path
2423     *
2424     * @api
2425     *
2426     * @param string $path
2427     */
2428    public function addImportPath($path)
2429    {
2430        if (!in_array($path, $this->importPaths)) {
2431            $this->importPaths[] = $path;
2432        }
2433    }
2434    /**
2435     * Set import paths
2436     *
2437     * @api
2438     *
2439     * @param string|array $path
2440     */
2441    public function setImportPaths($path)
2442    {
2443        $this->importPaths = (array) $path;
2444    }
2445    /**
2446     * Set number precision
2447     *
2448     * @api
2449     *
2450     * @param integer $numberPrecision
2451     */
2452    public function setNumberPrecision($numberPrecision)
2453    {
2454        Node\Number::$precision = $numberPrecision;
2455    }
2456    /**
2457     * Set formatter
2458     *
2459     * @api
2460     *
2461     * @param string $formatterName
2462     */
2463    public function setFormatter($formatterName)
2464    {
2465        $this->formatter = $formatterName;
2466    }
2467    /**
2468     * Set line number style
2469     *
2470     * @api
2471     *
2472     * @param string $lineNumberStyle
2473     */
2474    public function setLineNumberStyle($lineNumberStyle)
2475    {
2476        $this->lineNumberStyle = $lineNumberStyle;
2477    }
2478    /**
2479     * Register function
2480     *
2481     * @api
2482     *
2483     * @param string   $name
2484     * @param callable $func
2485     * @param array    $prototype
2486     */
2487    public function registerFunction($name, $func, $prototype = null)
2488    {
2489        $this->userFunctions[$this->normalizeName($name)] = array($func, $prototype);
2490    }
2491    /**
2492     * Unregister function
2493     *
2494     * @api
2495     *
2496     * @param string $name
2497     */
2498    public function unregisterFunction($name)
2499    {
2500        unset($this->userFunctions[$this->normalizeName($name)]);
2501    }
2502    /**
2503     * Add feature
2504     *
2505     * @api
2506     *
2507     * @param string $name
2508     */
2509    public function addFeature($name)
2510    {
2511        $this->registeredFeatures[$name] = true;
2512    }
2513    /**
2514     * Import file
2515     *
2516     * @param string $path
2517     * @param array  $out
2518     */
2519    protected function importFile($path, $out)
2520    {
2521        // see if tree is cached
2522        $realPath = realpath($path);
2523        if (isset($this->importCache[$realPath])) {
2524            $this->handleImportLoop($realPath);
2525            $tree = $this->importCache[$realPath];
2526        } else {
2527            $code = file_get_contents($path);
2528            $parser = $this->parserFactory($path);
2529            $tree = $parser->parse($code);
2530            $this->importCache[$realPath] = $tree;
2531        }
2532        $pi = pathinfo($path);
2533        array_unshift($this->importPaths, $pi['dirname']);
2534        $this->compileChildrenNoReturn($tree->children, $out);
2535        array_shift($this->importPaths);
2536    }
2537    /**
2538     * Return the file path for an import url if it exists
2539     *
2540     * @api
2541     *
2542     * @param string $url
2543     *
2544     * @return string|null
2545     */
2546    public function findImport($url)
2547    {
2548        $urls = array();
2549        // for "normal" scss imports (ignore vanilla css and external requests)
2550        if (!preg_match('/\\.css$|^https?:\\/\\//', $url)) {
2551            // try both normal and the _partial filename
2552            $urls = array($url, preg_replace('/[^\\/]+$/', '_\\0', $url));
2553        }
2554        foreach ($this->importPaths as $dir) {
2555            if (is_string($dir)) {
2556                // check urls for normal import paths
2557                foreach ($urls as $full) {
2558                    $full = $dir . (!empty($dir) && substr($dir, -1) !== '/' ? '/' : '') . $full;
2559                    if ($this->fileExists($file = $full . '.scss') || $this->fileExists($file = $full)) {
2560                        return $file;
2561                    }
2562                }
2563            } elseif (is_callable($dir)) {
2564                // check custom callback for import path
2565                $file = call_user_func($dir, $url);
2566                if ($file !== null) {
2567                    return $file;
2568                }
2569            }
2570        }
2571        return null;
2572    }
2573    /**
2574     * Set encoding
2575     *
2576     * @api
2577     *
2578     * @param string $encoding
2579     */
2580    public function setEncoding($encoding)
2581    {
2582        $this->encoding = $encoding;
2583    }
2584    /**
2585     * Ignore errors?
2586     *
2587     * @api
2588     *
2589     * @param boolean $ignoreErrors
2590     *
2591     * @return \Leafo\ScssPhp\Compiler
2592     */
2593    public function setIgnoreErrors($ignoreErrors)
2594    {
2595        $this->ignoreErrors = $ignoreErrors;
2596    }
2597    /**
2598     * Throw error (exception)
2599     *
2600     * @api
2601     *
2602     * @param string $msg Message with optional sprintf()-style vararg parameters
2603     *
2604     * @throws \Leafo\ScssPhp\Exception\CompilerException
2605     */
2606    public function throwError($msg)
2607    {
2608        if ($this->ignoreErrors) {
2609            return;
2610        }
2611        if (func_num_args() > 1) {
2612            $msg = call_user_func_array('sprintf', func_get_args());
2613        }
2614        $line = $this->sourceLine;
2615        $msg = "{$msg}: line: {$line}";
2616        throw new CompilerException($msg);
2617    }
2618    /**
2619     * Handle import loop
2620     *
2621     * @param string $name
2622     *
2623     * @throws \Exception
2624     */
2625    protected function handleImportLoop($name)
2626    {
2627        for ($env = $this->env; $env; $env = $env->parent) {
2628            $file = $this->sourceNames[$env->block->sourceIndex];
2629            if (realpath($file) === $name) {
2630                $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
2631                break;
2632            }
2633        }
2634    }
2635    /**
2636     * Does file exist?
2637     *
2638     * @param string $name
2639     *
2640     * @return boolean
2641     */
2642    protected function fileExists($name)
2643    {
2644        return is_file($name);
2645    }
2646    /**
2647     * Call SCSS @function
2648     *
2649     * @param string $name
2650     * @param array  $args
2651     * @param array  $returnValue
2652     *
2653     * @return boolean Returns true if returnValue is set; otherwise, false
2654     */
2655    protected function callScssFunction($name, $argValues, &$returnValue)
2656    {
2657        $func = $this->get(self::$namespaces['function'] . $name, false);
2658        if (!$func) {
2659            return false;
2660        }
2661        $this->pushEnv();
2662        // set the args
2663        if (isset($func->args)) {
2664            $this->applyArguments($func->args, $argValues);
2665        }
2666        // throw away lines and children
2667        $tmp = new OutputBlock();
2668        $tmp->lines = array();
2669        $tmp->children = array();
2670        $this->env->marker = 'function';
2671        $ret = $this->compileChildren($func->children, $tmp);
2672        $this->popEnv();
2673        $returnValue = !isset($ret) ? self::$defaultValue : $ret;
2674        return true;
2675    }
2676    /**
2677     * Call built-in and registered (PHP) functions
2678     *
2679     * @param string $name
2680     * @param array  $args
2681     * @param array  $returnValue
2682     *
2683     * @return boolean Returns true if returnValue is set; otherwise, false
2684     */
2685    protected function callNativeFunction($name, $args, &$returnValue)
2686    {
2687        // try a lib function
2688        $name = $this->normalizeName($name);
2689        if (isset($this->userFunctions[$name])) {
2690            // see if we can find a user function
2691            list($f, $prototype) = $this->userFunctions[$name];
2692        } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
2693            $libName = $f[1];
2694            $prototype = isset(self::${$libName}) ? self::${$libName} : null;
2695        } else {
2696            return false;
2697        }
2698        list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
2699        if ($name !== 'if' && $name !== 'call') {
2700            foreach ($sorted as &$val) {
2701                $val = $this->reduce($val, true);
2702            }
2703        }
2704        $returnValue = call_user_func($f, $sorted, $kwargs);
2705        if (!isset($returnValue)) {
2706            return false;
2707        }
2708        $returnValue = $this->coerceValue($returnValue);
2709        return true;
2710    }
2711    /**
2712     * Get built-in function
2713     *
2714     * @param string $name Normalized name
2715     *
2716     * @return array
2717     */
2718    protected function getBuiltinFunction($name)
2719    {
2720        $libName = 'lib' . preg_replace_callback('/_(.)/', function ($m) {
2721            return ucfirst($m[1]);
2722        }, ucfirst($name));
2723        return array($this, $libName);
2724    }
2725    /**
2726     * Sorts keyword arguments
2727     *
2728     * @param array $prototype
2729     * @param array $args
2730     *
2731     * @return array
2732     */
2733    protected function sortArgs($prototype, $args)
2734    {
2735        $keyArgs = array();
2736        $posArgs = array();
2737        // separate positional and keyword arguments
2738        foreach ($args as $arg) {
2739            list($key, $value) = $arg;
2740            $key = $key[1];
2741            if (empty($key)) {
2742                $posArgs[] = $value;
2743            } else {
2744                $keyArgs[$key] = $value;
2745            }
2746        }
2747        if (!isset($prototype)) {
2748            return array($posArgs, $keyArgs);
2749        }
2750        // copy positional args
2751        $finalArgs = array_pad($posArgs, count($prototype), null);
2752        // overwrite positional args with keyword args
2753        foreach ($prototype as $i => $names) {
2754            foreach ((array) $names as $name) {
2755                if (isset($keyArgs[$name])) {
2756                    $finalArgs[$i] = $keyArgs[$name];
2757                }
2758            }
2759        }
2760        return array($finalArgs, $keyArgs);
2761    }
2762    /**
2763     * Apply argument values per definition
2764     *
2765     * @param array $argDef
2766     * @param array $argValues
2767     *
2768     * @throws \Exception
2769     */
2770    protected function applyArguments($argDef, $argValues)
2771    {
2772        $storeEnv = $this->getStoreEnv();
2773        $env = new Environment();
2774        $env->store = $storeEnv->store;
2775        $hasVariable = false;
2776        $args = array();
2777        foreach ($argDef as $i => $arg) {
2778            list($name, $default, $isVariable) = $argDef[$i];
2779            $args[$name] = array($i, $name, $default, $isVariable);
2780            $hasVariable |= $isVariable;
2781        }
2782        $keywordArgs = array();
2783        $deferredKeywordArgs = array();
2784        $remaining = array();
2785        // assign the keyword args
2786        foreach ((array) $argValues as $arg) {
2787            if (!empty($arg[0])) {
2788                if (!isset($args[$arg[0][1]])) {
2789                    if ($hasVariable) {
2790                        $deferredKeywordArgs[$arg[0][1]] = $arg[1];
2791                    } else {
2792                        $this->throwError('Mixin or function doesn\'t have an argument named $%s.', $arg[0][1]);
2793                        break;
2794                    }
2795                } elseif ($args[$arg[0][1]][0] < count($remaining)) {
2796                    $this->throwError('The argument $%s was passed both by position and by name.', $arg[0][1]);
2797                    break;
2798                } else {
2799                    $keywordArgs[$arg[0][1]] = $arg[1];
2800                }
2801            } elseif (count($keywordArgs)) {
2802                $this->throwError('Positional arguments must come before keyword arguments.');
2803                break;
2804            } elseif ($arg[2] === true) {
2805                $val = $this->reduce($arg[1], true);
2806                if ($val[0] === Type::T_LIST) {
2807                    foreach ($val[2] as $name => $item) {
2808                        if (!is_numeric($name)) {
2809                            $keywordArgs[$name] = $item;
2810                        } else {
2811                            $remaining[] = $item;
2812                        }
2813                    }
2814                } elseif ($val[0] === Type::T_MAP) {
2815                    foreach ($val[1] as $i => $name) {
2816                        $name = $this->compileStringContent($this->coerceString($name));
2817                        $item = $val[2][$i];
2818                        if (!is_numeric($name)) {
2819                            $keywordArgs[$name] = $item;
2820                        } else {
2821                            $remaining[] = $item;
2822                        }
2823                    }
2824                } else {
2825                    $remaining[] = $val;
2826                }
2827            } else {
2828                $remaining[] = $arg[1];
2829            }
2830        }
2831        foreach ($args as $arg) {
2832            list($i, $name, $default, $isVariable) = $arg;
2833            if ($isVariable) {
2834                $val = array(Type::T_LIST, ',', array(), $isVariable);
2835                for ($count = count($remaining); $i < $count; $i++) {
2836                    $val[2][] = $remaining[$i];
2837                }
2838                foreach ($deferredKeywordArgs as $itemName => $item) {
2839                    $val[2][$itemName] = $item;
2840                }
2841            } elseif (isset($remaining[$i])) {
2842                $val = $remaining[$i];
2843            } elseif (isset($keywordArgs[$name])) {
2844                $val = $keywordArgs[$name];
2845            } elseif (!empty($default)) {
2846                continue;
2847            } else {
2848                $this->throwError("Missing argument {$name}");
2849                break;
2850            }
2851            $this->set($name, $this->reduce($val, true), true, $env);
2852        }
2853        $storeEnv->store = $env->store;
2854        foreach ($args as $arg) {
2855            list($i, $name, $default, $isVariable) = $arg;
2856            if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
2857                continue;
2858            }
2859            $this->set($name, $this->reduce($default, true), true);
2860        }
2861    }
2862    /**
2863     * Coerce a php value into a scss one
2864     *
2865     * @param mixed $value
2866     *
2867     * @return array
2868     */
2869    private function coerceValue($value)
2870    {
2871        if (is_array($value) || $value instanceof \ArrayAccess) {
2872            return $value;
2873        }
2874        if (is_bool($value)) {
2875            return $this->toBool($value);
2876        }
2877        if ($value === null) {
2878            $value = self::$null;
2879        }
2880        if (is_numeric($value)) {
2881            return new Node\Number($value, '');
2882        }
2883        if ($value === '') {
2884            return self::$emptyString;
2885        }
2886        return array(Type::T_KEYWORD, $value);
2887    }
2888    /**
2889     * Coerce something to map
2890     *
2891     * @param array $item
2892     *
2893     * @return array
2894     */
2895    protected function coerceMap($item)
2896    {
2897        if ($item[0] === Type::T_MAP) {
2898            return $item;
2899        }
2900        if ($item === self::$emptyList) {
2901            return self::$emptyMap;
2902        }
2903        return array(Type::T_MAP, array($item), array(self::$null));
2904    }
2905    /**
2906     * Coerce something to list
2907     *
2908     * @param array $item
2909     *
2910     * @return array
2911     */
2912    protected function coerceList($item, $delim = ',')
2913    {
2914        if (isset($item) && $item[0] === Type::T_LIST) {
2915            return $item;
2916        }
2917        if (isset($item) && $item[0] === Type::T_MAP) {
2918            $keys = $item[1];
2919            $values = $item[2];
2920            $list = array();
2921            for ($i = 0, $s = count($keys); $i < $s; $i++) {
2922                $key = $keys[$i];
2923                $value = $values[$i];
2924                $list[] = array(Type::T_LIST, '', array(array(Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))), $value));
2925            }
2926            return array(Type::T_LIST, ',', $list);
2927        }
2928        return array(Type::T_LIST, $delim, !isset($item) ? array() : array($item));
2929    }
2930    /**
2931     * Coerce color for expression
2932     *
2933     * @param array $value
2934     *
2935     * @return array|null
2936     */
2937    protected function coerceForExpression($value)
2938    {
2939        if ($color = $this->coerceColor($value)) {
2940            return $color;
2941        }
2942        return $value;
2943    }
2944    /**
2945     * Coerce value to color
2946     *
2947     * @param array $value
2948     *
2949     * @return array|null
2950     */
2951    protected function coerceColor($value)
2952    {
2953        switch ($value[0]) {
2954            case Type::T_COLOR:
2955                return $value;
2956            case Type::T_KEYWORD:
2957                $name = strtolower($value[1]);
2958                if (isset(Colors::$cssColors[$name])) {
2959                    $rgba = explode(',', Colors::$cssColors[$name]);
2960                    return isset($rgba[3]) ? array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]) : array(Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]);
2961                }
2962                return null;
2963        }
2964        return null;
2965    }
2966    /**
2967     * Coerce value to string
2968     *
2969     * @param array $value
2970     *
2971     * @return array|null
2972     */
2973    protected function coerceString($value)
2974    {
2975        if ($value[0] === Type::T_STRING) {
2976            return $value;
2977        }
2978        return array(Type::T_STRING, '', array($this->compileValue($value)));
2979    }
2980    /**
2981     * Coerce value to a percentage
2982     *
2983     * @param array $value
2984     *
2985     * @return integer|float
2986     */
2987    protected function coercePercent($value)
2988    {
2989        if ($value[0] === Type::T_NUMBER) {
2990            if (!empty($value[2]['%'])) {
2991                return $value[1] / 100;
2992            }
2993            return $value[1];
2994        }
2995        return 0;
2996    }
2997    /**
2998     * Assert value is a map
2999     *
3000     * @api
3001     *
3002     * @param array $value
3003     *
3004     * @return array
3005     *
3006     * @throws \Exception
3007     */
3008    public function assertMap($value)
3009    {
3010        $value = $this->coerceMap($value);
3011        if ($value[0] !== Type::T_MAP) {
3012            $this->throwError('expecting map');
3013        }
3014        return $value;
3015    }
3016    /**
3017     * Assert value is a list
3018     *
3019     * @api
3020     *
3021     * @param array $value
3022     *
3023     * @return array
3024     *
3025     * @throws \Exception
3026     */
3027    public function assertList($value)
3028    {
3029        if ($value[0] !== Type::T_LIST) {
3030            $this->throwError('expecting list');
3031        }
3032        return $value;
3033    }
3034    /**
3035     * Assert value is a color
3036     *
3037     * @api
3038     *
3039     * @param array $value
3040     *
3041     * @return array
3042     *
3043     * @throws \Exception
3044     */
3045    public function assertColor($value)
3046    {
3047        if ($color = $this->coerceColor($value)) {
3048            return $color;
3049        }
3050        $this->throwError('expecting color');
3051    }
3052    /**
3053     * Assert value is a number
3054     *
3055     * @api
3056     *
3057     * @param array $value
3058     *
3059     * @return integer|float
3060     *
3061     * @throws \Exception
3062     */
3063    public function assertNumber($value)
3064    {
3065        if ($value[0] !== Type::T_NUMBER) {
3066            $this->throwError('expecting number');
3067        }
3068        return $value[1];
3069    }
3070    /**
3071     * Make sure a color's components don't go out of bounds
3072     *
3073     * @param array $c
3074     *
3075     * @return array
3076     */
3077    protected function fixColor($c)
3078    {
3079        foreach (array(1, 2, 3) as $i) {
3080            if ($c[$i] < 0) {
3081                $c[$i] = 0;
3082            }
3083            if ($c[$i] > 255) {
3084                $c[$i] = 255;
3085            }
3086        }
3087        return $c;
3088    }
3089    /**
3090     * Convert RGB to HSL
3091     *
3092     * @api
3093     *
3094     * @param integer $red
3095     * @param integer $green
3096     * @param integer $blue
3097     *
3098     * @return array
3099     */
3100    public function toHSL($red, $green, $blue)
3101    {
3102        $min = min($red, $green, $blue);
3103        $max = max($red, $green, $blue);
3104        $l = $min + $max;
3105        $d = $max - $min;
3106        if ((int) $d === 0) {
3107            $h = $s = 0;
3108        } else {
3109            if ($l < 255) {
3110                $s = $d / $l;
3111            } else {
3112                $s = $d / (510 - $l);
3113            }
3114            if ($red == $max) {
3115                $h = 60 * ($green - $blue) / $d;
3116            } elseif ($green == $max) {
3117                $h = 60 * ($blue - $red) / $d + 120;
3118            } elseif ($blue == $max) {
3119                $h = 60 * ($red - $green) / $d + 240;
3120            }
3121        }
3122        return array(Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1);
3123    }
3124    /**
3125     * Hue to RGB helper
3126     *
3127     * @param float $m1
3128     * @param float $m2
3129     * @param float $h
3130     *
3131     * @return float
3132     */
3133    private function hueToRGB($m1, $m2, $h)
3134    {
3135        if ($h < 0) {
3136            $h += 1;
3137        } elseif ($h > 1) {
3138            $h -= 1;
3139        }
3140        if ($h * 6 < 1) {
3141            return $m1 + ($m2 - $m1) * $h * 6;
3142        }
3143        if ($h * 2 < 1) {
3144            return $m2;
3145        }
3146        if ($h * 3 < 2) {
3147            return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
3148        }
3149        return $m1;
3150    }
3151    /**
3152     * Convert HSL to RGB
3153     *
3154     * @api
3155     *
3156     * @param integer $hue        H from 0 to 360
3157     * @param integer $saturation S from 0 to 100
3158     * @param integer $lightness  L from 0 to 100
3159     *
3160     * @return array
3161     */
3162    public function toRGB($hue, $saturation, $lightness)
3163    {
3164        if ($hue < 0) {
3165            $hue += 360;
3166        }
3167        $h = $hue / 360;
3168        $s = min(100, max(0, $saturation)) / 100;
3169        $l = min(100, max(0, $lightness)) / 100;
3170        $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
3171        $m1 = $l * 2 - $m2;
3172        $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
3173        $g = $this->hueToRGB($m1, $m2, $h) * 255;
3174        $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
3175        $out = array(Type::T_COLOR, $r, $g, $b);
3176        return $out;
3177    }
3178    // Built in functions
3179    //protected static $libCall = ['name', 'args...'];
3180    protected function libCall($args, $kwargs)
3181    {
3182        $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
3183        $args = array_map(function ($a) {
3184            return array(null, $a, false);
3185        }, $args);
3186        if (count($kwargs)) {
3187            foreach ($kwargs as $key => $value) {
3188                $args[] = array(array(Type::T_VARIABLE, $key), $value, false);
3189            }
3190        }
3191        return $this->reduce(array(Type::T_FUNCTION_CALL, $name, $args));
3192    }
3193    protected static $libIf = array('condition', 'if-true', 'if-false');
3194    protected function libIf($args)
3195    {
3196        list($cond, $t, $f) = $args;
3197        if (!$this->isTruthy($this->reduce($cond, true))) {
3198            return $this->reduce($f, true);
3199        }
3200        return $this->reduce($t, true);
3201    }
3202    protected static $libIndex = array('list', 'value');
3203    protected function libIndex($args)
3204    {
3205        list($list, $value) = $args;
3206        if ($value[0] === Type::T_MAP) {
3207            return self::$null;
3208        }
3209        if ($list[0] === Type::T_MAP || $list[0] === Type::T_STRING || $list[0] === Type::T_KEYWORD || $list[0] === Type::T_INTERPOLATE) {
3210            $list = $this->coerceList($list, ' ');
3211        }
3212        if ($list[0] !== Type::T_LIST) {
3213            return self::$null;
3214        }
3215        $values = array();
3216        foreach ($list[2] as $item) {
3217            $values[] = $this->normalizeValue($item);
3218        }
3219        $key = array_search($this->normalizeValue($value), $values);
3220        return false === $key ? self::$null : $key + 1;
3221    }
3222    protected static $libRgb = array('red', 'green', 'blue');
3223    protected function libRgb($args)
3224    {
3225        list($r, $g, $b) = $args;
3226        return array(Type::T_COLOR, $r[1], $g[1], $b[1]);
3227    }
3228    protected static $libRgba = array(array('red', 'color'), 'green', 'blue', 'alpha');
3229    protected function libRgba($args)
3230    {
3231        if ($color = $this->coerceColor($args[0])) {
3232            $num = !isset($args[1]) ? $args[3] : $args[1];
3233            $alpha = $this->assertNumber($num);
3234            $color[4] = $alpha;
3235            return $color;
3236        }
3237        list($r, $g, $b, $a) = $args;
3238        return array(Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]);
3239    }
3240    // helper function for adjust_color, change_color, and scale_color
3241    protected function alterColor($args, $fn)
3242    {
3243        $color = $this->assertColor($args[0]);
3244        foreach (array(1, 2, 3, 7) as $i) {
3245            if (isset($args[$i])) {
3246                $val = $this->assertNumber($args[$i]);
3247                $ii = $i === 7 ? 4 : $i;
3248                // alpha
3249                $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
3250            }
3251        }
3252        if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
3253            $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3254            foreach (array(4, 5, 6) as $i) {
3255                if (isset($args[$i])) {
3256                    $val = $this->assertNumber($args[$i]);
3257                    $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
3258                }
3259            }
3260            $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
3261            if (isset($color[4])) {
3262                $rgb[4] = $color[4];
3263            }
3264            $color = $rgb;
3265        }
3266        return $color;
3267    }
3268    protected static $libAdjustColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
3269    protected function libAdjustColor($args)
3270    {
3271        return $this->alterColor($args, function ($base, $alter, $i) {
3272            return $base + $alter;
3273        });
3274    }
3275    protected static $libChangeColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
3276    protected function libChangeColor($args)
3277    {
3278        return $this->alterColor($args, function ($base, $alter, $i) {
3279            return $alter;
3280        });
3281    }
3282    protected static $libScaleColor = array('color', 'red', 'green', 'blue', 'hue', 'saturation', 'lightness', 'alpha');
3283    protected function libScaleColor($args)
3284    {
3285        return $this->alterColor($args, function ($base, $scale, $i) {
3286            // 1, 2, 3 - rgb
3287            // 4, 5, 6 - hsl
3288            // 7 - a
3289            switch ($i) {
3290                case 1:
3291                case 2:
3292                case 3:
3293                    $max = 255;
3294                    break;
3295                case 4:
3296                    $max = 360;
3297                    break;
3298                case 7:
3299                    $max = 1;
3300                    break;
3301                default:
3302                    $max = 100;
3303            }
3304            $scale = $scale / 100;
3305            if ($scale < 0) {
3306                return $base * $scale + $base;
3307            }
3308            return ($max - $base) * $scale + $base;
3309        });
3310    }
3311    protected static $libIeHexStr = array('color');
3312    protected function libIeHexStr($args)
3313    {
3314        $color = $this->coerceColor($args[0]);
3315        $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
3316        return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
3317    }
3318    protected static $libRed = array('color');
3319    protected function libRed($args)
3320    {
3321        $color = $this->coerceColor($args[0]);
3322        return $color[1];
3323    }
3324    protected static $libGreen = array('color');
3325    protected function libGreen($args)
3326    {
3327        $color = $this->coerceColor($args[0]);
3328        return $color[2];
3329    }
3330    protected static $libBlue = array('color');
3331    protected function libBlue($args)
3332    {
3333        $color = $this->coerceColor($args[0]);
3334        return $color[3];
3335    }
3336    protected static $libAlpha = array('color');
3337    protected function libAlpha($args)
3338    {
3339        if ($color = $this->coerceColor($args[0])) {
3340            return isset($color[4]) ? $color[4] : 1;
3341        }
3342        // this might be the IE function, so return value unchanged
3343        return null;
3344    }
3345    protected static $libOpacity = array('color');
3346    protected function libOpacity($args)
3347    {
3348        $value = $args[0];
3349        if ($value[0] === Type::T_NUMBER) {
3350            return null;
3351        }
3352        return $this->libAlpha($args);
3353    }
3354    // mix two colors
3355    protected static $libMix = array('color-1', 'color-2', 'weight');
3356    protected function libMix($args)
3357    {
3358        list($first, $second, $weight) = $args;
3359        $first = $this->assertColor($first);
3360        $second = $this->assertColor($second);
3361        if (!isset($weight)) {
3362            $weight = 0.5;
3363        } else {
3364            $weight = $this->coercePercent($weight);
3365        }
3366        $firstAlpha = isset($first[4]) ? $first[4] : 1;
3367        $secondAlpha = isset($second[4]) ? $second[4] : 1;
3368        $w = $weight * 2 - 1;
3369        $a = $firstAlpha - $secondAlpha;
3370        $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
3371        $w2 = 1.0 - $w1;
3372        $new = array(Type::T_COLOR, $w1 * $first[1] + $w2 * $second[1], $w1 * $first[2] + $w2 * $second[2], $w1 * $first[3] + $w2 * $second[3]);
3373        if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
3374            $new[] = $firstAlpha * $weight + $secondAlpha * ($weight - 1);
3375        }
3376        return $this->fixColor($new);
3377    }
3378    protected static $libHsl = array('hue', 'saturation', 'lightness');
3379    protected function libHsl($args)
3380    {
3381        list($h, $s, $l) = $args;
3382        return $this->toRGB($h[1], $s[1], $l[1]);
3383    }
3384    protected static $libHsla = array('hue', 'saturation', 'lightness', 'alpha');
3385    protected function libHsla($args)
3386    {
3387        list($h, $s, $l, $a) = $args;
3388        $color = $this->toRGB($h[1], $s[1], $l[1]);
3389        $color[4] = $a[1];
3390        return $color;
3391    }
3392    protected static $libHue = array('color');
3393    protected function libHue($args)
3394    {
3395        $color = $this->assertColor($args[0]);
3396        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3397        return new Node\Number($hsl[1], 'deg');
3398    }
3399    protected static $libSaturation = array('color');
3400    protected function libSaturation($args)
3401    {
3402        $color = $this->assertColor($args[0]);
3403        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3404        return new Node\Number($hsl[2], '%');
3405    }
3406    protected static $libLightness = array('color');
3407    protected function libLightness($args)
3408    {
3409        $color = $this->assertColor($args[0]);
3410        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3411        return new Node\Number($hsl[3], '%');
3412    }
3413    protected function adjustHsl($color, $idx, $amount)
3414    {
3415        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
3416        $hsl[$idx] += $amount;
3417        $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
3418        if (isset($color[4])) {
3419            $out[4] = $color[4];
3420        }
3421        return $out;
3422    }
3423    protected static $libAdjustHue = array('color', 'degrees');
3424    protected function libAdjustHue($args)
3425    {
3426        $color = $this->assertColor($args[0]);
3427        $degrees = $this->assertNumber($args[1]);
3428        return $this->adjustHsl($color, 1, $degrees);
3429    }
3430    protected static $libLighten = array('color', 'amount');
3431    protected function libLighten($args)
3432    {
3433        $color = $this->assertColor($args[0]);
3434        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
3435        return $this->adjustHsl($color, 3, $amount);
3436    }
3437    protected static $libDarken = array('color', 'amount');
3438    protected function libDarken($args)
3439    {
3440        $color = $this->assertColor($args[0]);
3441        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
3442        return $this->adjustHsl($color, 3, -$amount);
3443    }
3444    protected static $libSaturate = array('color', 'amount');
3445    protected function libSaturate($args)
3446    {
3447        $value = $args[0];
3448        if ($value[0] === Type::T_NUMBER) {
3449            return null;
3450        }
3451        $color = $this->assertColor($value);
3452        $amount = 100 * $this->coercePercent($args[1]);
3453        return $this->adjustHsl($color, 2, $amount);
3454    }
3455    protected static $libDesaturate = array('color', 'amount');
3456    protected function libDesaturate($args)
3457    {
3458        $color = $this->assertColor($args[0]);
3459        $amount = 100 * $this->coercePercent($args[1]);
3460        return $this->adjustHsl($color, 2, -$amount);
3461    }
3462    protected static $libGrayscale = array('color');
3463    protected function libGrayscale($args)
3464    {
3465        $value = $args[0];
3466        if ($value[0] === Type::T_NUMBER) {
3467            return null;
3468        }
3469        return $this->adjustHsl($this->assertColor($value), 2, -100);
3470    }
3471    protected static $libComplement = array('color');
3472    protected function libComplement($args)
3473    {
3474        return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
3475    }
3476    protected static $libInvert = array('color');
3477    protected function libInvert($args)
3478    {
3479        $value = $args[0];
3480        if ($value[0] === Type::T_NUMBER) {
3481            return null;
3482        }
3483        $color = $this->assertColor($value);
3484        $color[1] = 255 - $color[1];
3485        $color[2] = 255 - $color[2];
3486        $color[3] = 255 - $color[3];
3487        return $color;
3488    }
3489    // increases opacity by amount
3490    protected static $libOpacify = array('color', 'amount');
3491    protected function libOpacify($args)
3492    {
3493        $color = $this->assertColor($args[0]);
3494        $amount = $this->coercePercent($args[1]);
3495        $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
3496        $color[4] = min(1, max(0, $color[4]));
3497        return $color;
3498    }
3499    protected static $libFadeIn = array('color', 'amount');
3500    protected function libFadeIn($args)
3501    {
3502        return $this->libOpacify($args);
3503    }
3504    // decreases opacity by amount
3505    protected static $libTransparentize = array('color', 'amount');
3506    protected function libTransparentize($args)
3507    {
3508        $color = $this->assertColor($args[0]);
3509        $amount = $this->coercePercent($args[1]);
3510        $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
3511        $color[4] = min(1, max(0, $color[4]));
3512        return $color;
3513    }
3514    protected static $libFadeOut = array('color', 'amount');
3515    protected function libFadeOut($args)
3516    {
3517        return $this->libTransparentize($args);
3518    }
3519    protected static $libUnquote = array('string');
3520    protected function libUnquote($args)
3521    {
3522        $str = $args[0];
3523        if ($str[0] === Type::T_STRING) {
3524            $str[1] = '';
3525        }
3526        return $str;
3527    }
3528    protected static $libQuote = array('string');
3529    protected function libQuote($args)
3530    {
3531        $value = $args[0];
3532        if ($value[0] === Type::T_STRING && !empty($value[1])) {
3533            return $value;
3534        }
3535        return array(Type::T_STRING, '"', array($value));
3536    }
3537    protected static $libPercentage = array('value');
3538    protected function libPercentage($args)
3539    {
3540        return new Node\Number($this->coercePercent($args[0]) * 100, '%');
3541    }
3542    protected static $libRound = array('value');
3543    protected function libRound($args)
3544    {
3545        $num = $args[0];
3546        $num[1] = round($num[1]);
3547        return $num;
3548    }
3549    protected static $libFloor = array('value');
3550    protected function libFloor($args)
3551    {
3552        $num = $args[0];
3553        $num[1] = floor($num[1]);
3554        return $num;
3555    }
3556    protected static $libCeil = array('value');
3557    protected function libCeil($args)
3558    {
3559        $num = $args[0];
3560        $num[1] = ceil($num[1]);
3561        return $num;
3562    }
3563    protected static $libAbs = array('value');
3564    protected function libAbs($args)
3565    {
3566        $num = $args[0];
3567        $num[1] = abs($num[1]);
3568        return $num;
3569    }
3570    protected function libMin($args)
3571    {
3572        $numbers = $this->getNormalizedNumbers($args);
3573        $min = null;
3574        foreach ($numbers as $key => $number) {
3575            if (null === $min || $number[1] <= $min[1]) {
3576                $min = array($key, $number[1]);
3577            }
3578        }
3579        return $args[$min[0]];
3580    }
3581    protected function libMax($args)
3582    {
3583        $numbers = $this->getNormalizedNumbers($args);
3584        $max = null;
3585        foreach ($numbers as $key => $number) {
3586            if (null === $max || $number[1] >= $max[1]) {
3587                $max = array($key, $number[1]);
3588            }
3589        }
3590        return $args[$max[0]];
3591    }
3592    /**
3593     * Helper to normalize args containing numbers
3594     *
3595     * @param array $args
3596     *
3597     * @return array
3598     */
3599    protected function getNormalizedNumbers($args)
3600    {
3601        $unit = null;
3602        $originalUnit = null;
3603        $numbers = array();
3604        foreach ($args as $key => $item) {
3605            if ($item[0] !== Type::T_NUMBER) {
3606                $this->throwError('%s is not a number', $item[0]);
3607                break;
3608            }
3609            $number = $item->normalize();
3610            if (null === $unit) {
3611                $unit = $number[2];
3612                $originalUnit = $item->unitStr();
3613            } elseif ($unit !== $number[2]) {
3614                $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
3615                break;
3616            }
3617            $numbers[$key] = $number;
3618        }
3619        return $numbers;
3620    }
3621    protected static $libLength = array('list');
3622    protected function libLength($args)
3623    {
3624        $list = $this->coerceList($args[0]);
3625        return count($list[2]);
3626    }
3627    //protected static $libListSeparator = ['list...'];
3628    protected function libListSeparator($args)
3629    {
3630        if (count($args) > 1) {
3631            return 'comma';
3632        }
3633        $list = $this->coerceList($args[0]);
3634        if (count($list[2]) <= 1) {
3635            return 'space';
3636        }
3637        if ($list[1] === ',') {
3638            return 'comma';
3639        }
3640        return 'space';
3641    }
3642    protected static $libNth = array('list', 'n');
3643    protected function libNth($args)
3644    {
3645        $list = $this->coerceList($args[0]);
3646        $n = $this->assertNumber($args[1]);
3647        if ($n > 0) {
3648            $n--;
3649        } elseif ($n < 0) {
3650            $n += count($list[2]);
3651        }
3652        return isset($list[2][$n]) ? $list[2][$n] : self::$defaultValue;
3653    }
3654    protected static $libSetNth = array('list', 'n', 'value');
3655    protected function libSetNth($args)
3656    {
3657        $list = $this->coerceList($args[0]);
3658        $n = $this->assertNumber($args[1]);
3659        if ($n > 0) {
3660            $n--;
3661        } elseif ($n < 0) {
3662            $n += count($list[2]);
3663        }
3664        if (!isset($list[2][$n])) {
3665            $this->throwError('Invalid argument for "n"');
3666            return;
3667        }
3668        $list[2][$n] = $args[2];
3669        return $list;
3670    }
3671    protected static $libMapGet = array('map', 'key');
3672    protected function libMapGet($args)
3673    {
3674        $map = $this->assertMap($args[0]);
3675        $key = $this->compileStringContent($this->coerceString($args[1]));
3676        for ($i = count($map[1]) - 1; $i >= 0; $i--) {
3677            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
3678                return $map[2][$i];
3679            }
3680        }
3681        return self::$null;
3682    }
3683    protected static $libMapKeys = array('map');
3684    protected function libMapKeys($args)
3685    {
3686        $map = $this->assertMap($args[0]);
3687        $keys = $map[1];
3688        return array(Type::T_LIST, ',', $keys);
3689    }
3690    protected static $libMapValues = array('map');
3691    protected function libMapValues($args)
3692    {
3693        $map = $this->assertMap($args[0]);
3694        $values = $map[2];
3695        return array(Type::T_LIST, ',', $values);
3696    }
3697    protected static $libMapRemove = array('map', 'key');
3698    protected function libMapRemove($args)
3699    {
3700        $map = $this->assertMap($args[0]);
3701        $key = $this->compileStringContent($this->coerceString($args[1]));
3702        for ($i = count($map[1]) - 1; $i >= 0; $i--) {
3703            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
3704                array_splice($map[1], $i, 1);
3705                array_splice($map[2], $i, 1);
3706            }
3707        }
3708        return $map;
3709    }
3710    protected static $libMapHasKey = array('map', 'key');
3711    protected function libMapHasKey($args)
3712    {
3713        $map = $this->assertMap($args[0]);
3714        $key = $this->compileStringContent($this->coerceString($args[1]));
3715        for ($i = count($map[1]) - 1; $i >= 0; $i--) {
3716            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
3717                return true;
3718            }
3719        }
3720        return false;
3721    }
3722    protected static $libMapMerge = array('map-1', 'map-2');
3723    protected function libMapMerge($args)
3724    {
3725        $map1 = $this->assertMap($args[0]);
3726        $map2 = $this->assertMap($args[1]);
3727        return array(Type::T_MAP, array_merge($map1[1], $map2[1]), array_merge($map1[2], $map2[2]));
3728    }
3729    protected static $libKeywords = array('args');
3730    protected function libKeywords($args)
3731    {
3732        $this->assertList($args[0]);
3733        $keys = array();
3734        $values = array();
3735        foreach ($args[0][2] as $name => $arg) {
3736            $keys[] = array(Type::T_KEYWORD, $name);
3737            $values[] = $arg;
3738        }
3739        return array(Type::T_MAP, $keys, $values);
3740    }
3741    protected function listSeparatorForJoin($list1, $sep)
3742    {
3743        if (!isset($sep)) {
3744            return $list1[1];
3745        }
3746        switch ($this->compileValue($sep)) {
3747            case 'comma':
3748                return ',';
3749            case 'space':
3750                return '';
3751            default:
3752                return $list1[1];
3753        }
3754    }
3755    protected static $libJoin = array('list1', 'list2', 'separator');
3756    protected function libJoin($args)
3757    {
3758        list($list1, $list2, $sep) = $args;
3759        $list1 = $this->coerceList($list1, ' ');
3760        $list2 = $this->coerceList($list2, ' ');
3761        $sep = $this->listSeparatorForJoin($list1, $sep);
3762        return array(Type::T_LIST, $sep, array_merge($list1[2], $list2[2]));
3763    }
3764    protected static $libAppend = array('list', 'val', 'separator');
3765    protected function libAppend($args)
3766    {
3767        list($list1, $value, $sep) = $args;
3768        $list1 = $this->coerceList($list1, ' ');
3769        $sep = $this->listSeparatorForJoin($list1, $sep);
3770        return array(Type::T_LIST, $sep, array_merge($list1[2], array($value)));
3771    }
3772    protected function libZip($args)
3773    {
3774        foreach ($args as $arg) {
3775            $this->assertList($arg);
3776        }
3777        $lists = array();
3778        $firstList = array_shift($args);
3779        foreach ($firstList[2] as $key => $item) {
3780            $list = array(Type::T_LIST, '', array($item));
3781            foreach ($args as $arg) {
3782                if (isset($arg[2][$key])) {
3783                    $list[2][] = $arg[2][$key];
3784                } else {
3785                    break 2;
3786                }
3787            }
3788            $lists[] = $list;
3789        }
3790        return array(Type::T_LIST, ',', $lists);
3791    }
3792    protected static $libTypeOf = array('value');
3793    protected function libTypeOf($args)
3794    {
3795        $value = $args[0];
3796        switch ($value[0]) {
3797            case Type::T_KEYWORD:
3798                if ($value === self::$true || $value === self::$false) {
3799                    return 'bool';
3800                }
3801                if ($this->coerceColor($value)) {
3802                    return 'color';
3803                }
3804            // fall-thru
3805            case Type::T_FUNCTION:
3806                return 'string';
3807            case Type::T_LIST:
3808                if (isset($value[3]) && $value[3]) {
3809                    return 'arglist';
3810                }
3811            // fall-thru
3812            default:
3813                return $value[0];
3814        }
3815    }
3816    protected static $libUnit = array('number');
3817    protected function libUnit($args)
3818    {
3819        $num = $args[0];
3820        if ($num[0] === Type::T_NUMBER) {
3821            return array(Type::T_STRING, '"', array($num->unitStr()));
3822        }
3823        return '';
3824    }
3825    protected static $libUnitless = array('number');
3826    protected function libUnitless($args)
3827    {
3828        $value = $args[0];
3829        return $value[0] === Type::T_NUMBER && $value->unitless();
3830    }
3831    protected static $libComparable = array('number-1', 'number-2');
3832    protected function libComparable($args)
3833    {
3834        list($number1, $number2) = $args;
3835        if (!isset($number1[0]) || $number1[0] !== Type::T_NUMBER || !isset($number2[0]) || $number2[0] !== Type::T_NUMBER) {
3836            $this->throwError('Invalid argument(s) for "comparable"');
3837            return;
3838        }
3839        $number1 = $number1->normalize();
3840        $number2 = $number2->normalize();
3841        return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
3842    }
3843    protected static $libStrIndex = array('string', 'substring');
3844    protected function libStrIndex($args)
3845    {
3846        $string = $this->coerceString($args[0]);
3847        $stringContent = $this->compileStringContent($string);
3848        $substring = $this->coerceString($args[1]);
3849        $substringContent = $this->compileStringContent($substring);
3850        $result = strpos($stringContent, $substringContent);
3851        return $result === false ? self::$null : new Node\Number($result + 1, '');
3852    }
3853    protected static $libStrInsert = array('string', 'insert', 'index');
3854    protected function libStrInsert($args)
3855    {
3856        $string = $this->coerceString($args[0]);
3857        $stringContent = $this->compileStringContent($string);
3858        $insert = $this->coerceString($args[1]);
3859        $insertContent = $this->compileStringContent($insert);
3860        list(, $index) = $args[2];
3861        $string[2] = array(substr_replace($stringContent, $insertContent, $index - 1, 0));
3862        return $string;
3863    }
3864    protected static $libStrLength = array('string');
3865    protected function libStrLength($args)
3866    {
3867        $string = $this->coerceString($args[0]);
3868        $stringContent = $this->compileStringContent($string);
3869        return new Node\Number(strlen($stringContent), '');
3870    }
3871    protected static $libStrSlice = array('string', 'start-at', 'end-at');
3872    protected function libStrSlice($args)
3873    {
3874        if (isset($args[2]) && $args[2][1] == 0) {
3875            return self::$nullString;
3876        }
3877        $string = $this->coerceString($args[0]);
3878        $stringContent = $this->compileStringContent($string);
3879        $start = (int) $args[1][1];
3880        if ($start > 0) {
3881            $start--;
3882        }
3883        $end = (int) $args[2][1];
3884        $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
3885        $string[2] = $length ? array(substr($stringContent, $start, $length)) : array(substr($stringContent, $start));
3886        return $string;
3887    }
3888    protected static $libToLowerCase = array('string');
3889    protected function libToLowerCase($args)
3890    {
3891        $string = $this->coerceString($args[0]);
3892        $stringContent = $this->compileStringContent($string);
3893        $string[2] = array(mb_strtolower($stringContent));
3894        return $string;
3895    }
3896    protected static $libToUpperCase = array('string');
3897    protected function libToUpperCase($args)
3898    {
3899        $string = $this->coerceString($args[0]);
3900        $stringContent = $this->compileStringContent($string);
3901        $string[2] = array(mb_strtoupper($stringContent));
3902        return $string;
3903    }
3904    protected static $libFeatureExists = array('feature');
3905    protected function libFeatureExists($args)
3906    {
3907        $string = $this->coerceString($args[0]);
3908        $name = $this->compileStringContent($string);
3909        return $this->toBool(array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false);
3910    }
3911    protected static $libFunctionExists = array('name');
3912    protected function libFunctionExists($args)
3913    {
3914        $string = $this->coerceString($args[0]);
3915        $name = $this->compileStringContent($string);
3916        // user defined functions
3917        if ($this->has(self::$namespaces['function'] . $name)) {
3918            return true;
3919        }
3920        $name = $this->normalizeName($name);
3921        if (isset($this->userFunctions[$name])) {
3922            return true;
3923        }
3924        // built-in functions
3925        $f = $this->getBuiltinFunction($name);
3926        return $this->toBool(is_callable($f));
3927    }
3928    protected static $libGlobalVariableExists = array('name');
3929    protected function libGlobalVariableExists($args)
3930    {
3931        $string = $this->coerceString($args[0]);
3932        $name = $this->compileStringContent($string);
3933        return $this->has($name, $this->rootEnv);
3934    }
3935    protected static $libMixinExists = array('name');
3936    protected function libMixinExists($args)
3937    {
3938        $string = $this->coerceString($args[0]);
3939        $name = $this->compileStringContent($string);
3940        return $this->has(self::$namespaces['mixin'] . $name);
3941    }
3942    protected static $libVariableExists = array('name');
3943    protected function libVariableExists($args)
3944    {
3945        $string = $this->coerceString($args[0]);
3946        $name = $this->compileStringContent($string);
3947        return $this->has($name);
3948    }
3949    /**
3950     * Workaround IE7's content counter bug.
3951     *
3952     * @param array $args
3953     */
3954    protected function libCounter($args)
3955    {
3956        $list = array_map(array($this, 'compileValue'), $args);
3957        return array(Type::T_STRING, '', array('counter(' . implode(',', $list) . ')'));
3958    }
3959    protected static $libRandom = array('limit');
3960    protected function libRandom($args)
3961    {
3962        if (isset($args[0])) {
3963            $n = $this->assertNumber($args[0]);
3964            if ($n < 1) {
3965                $this->throwError('limit must be greater than or equal to 1');
3966                return;
3967            }
3968            return new Node\Number(mt_rand(1, $n), '');
3969        }
3970        return new Node\Number(mt_rand(1, mt_getrandmax()), '');
3971    }
3972    protected function libUniqueId()
3973    {
3974        static $id;
3975        if (!isset($id)) {
3976            $id = mt_rand(0, pow(36, 8));
3977        }
3978        $id += mt_rand(0, 10) + 1;
3979        return array(Type::T_STRING, '', array('u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)));
3980    }
3981    protected static $libInspect = array('value');
3982    protected function libInspect($args)
3983    {
3984        if ($args[0] === self::$null) {
3985            return array(Type::T_KEYWORD, 'null');
3986        }
3987        return $args[0];
3988    }
3989}
3990