fix image text
[pear] / HTML / Scss.php
1 <?php
2 /**
3  * SCSSPHP
4  *
5  * @copyright 2012-2018 Leaf Corcoran
6  *
7  * @license http://opensource.org/licenses/MIT MIT
8  *
9  * @link http://leafo.github.io/scssphp
10  */
11
12  
13  
14
15 /**
16  * The scss compiler and parser.
17  *
18  * Converting SCSS to CSS is a three stage process. The incoming file is parsed
19  * by `Parser` into a syntax tree, then it is compiled into another tree
20  * representing the CSS structure by `Compiler`. The CSS tree is fed into a
21  * formatter, like `Formatter` which then outputs CSS as a string.
22  *
23  * During the first compile, all values are *reduced*, which means that their
24  * types are brought to the lowest form before being dump as strings. This
25  * handles math equations, variable dereferences, and the like.
26  *
27  * The `compile` function of `Compiler` is the entry point.
28  *
29  * In summary:
30  *
31  * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
32  * then transforms the resulting tree to a CSS tree. This class also holds the
33  * evaluation context, such as all available mixins and variables at any given
34  * time.
35  *
36  * The `Parser` class is only concerned with parsing its input.
37  *
38  * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
39  * handling things like indentation.
40  */
41 require_once 'Scss/Type.php';
42 require_once 'Scss/Util.php';
43 require_once 'Scss/Block.php';
44 require_once 'Scss/Parser.php';
45 require_once 'Scss/Colors.php';
46 require_once 'Scss/Compiler/Environment.php';
47 require_once 'Scss/Node/Number.php';
48 require_once 'Scss/Formatter/OutputBlock.php';
49
50 /**
51  * SCSS compiler
52  *
53  * @author Leaf Corcoran <leafot@gmail.com>
54  */
55 class HTML_Scss
56 {
57     const LINE_COMMENTS = 1;
58     const DEBUG_INFO    = 2;
59
60     const WITH_RULE     = 1;
61     const WITH_MEDIA    = 2;
62     const WITH_SUPPORTS = 4;
63     const WITH_ALL      = 7;
64
65     const SOURCE_MAP_NONE   = 0;
66     const SOURCE_MAP_INLINE = 1;
67     const SOURCE_MAP_FILE   = 2;
68
69     /**
70      * @var array
71      */
72     static protected $operatorNames = [
73         '+'   => 'add',
74         '-'   => 'sub',
75         '*'   => 'mul',
76         '/'   => 'div',
77         '%'   => 'mod',
78
79         '=='  => 'eq',
80         '!='  => 'neq',
81         '<'   => 'lt',
82         '>'   => 'gt',
83
84         '<='  => 'lte',
85         '>='  => 'gte',
86         '<=>' => 'cmp',
87     ];
88
89     /**
90      * @var array
91      */
92     static protected $namespaces = [
93         'special'  => '%',
94         'mixin'    => '@',
95         'function' => '^',
96     ];
97
98     static public $true = [HTML_Scss_Type::T_KEYWORD, 'true'];
99     static public $false = [HTML_Scss_Type::T_KEYWORD, 'false'];
100     static public $null = [HTML_Scss_Type::T_NULL];
101     static public $nullString = [HTML_Scss_Type::T_STRING, '', []];
102     static public $defaultValue = [HTML_Scss_Type::T_KEYWORD, ''];
103     static public $selfSelector = [HTML_Scss_Type::T_SELF];
104     static public $emptyList = [HTML_Scss_Type::T_LIST, '', []];
105     static public $emptyMap = [HTML_Scss_Type::T_MAP, [], []];
106     static public $emptyString = [HTML_Scss_Type::T_STRING, '"', []];
107     static public $with = [HTML_Scss_Type::T_KEYWORD, 'with'];
108     static public $without = [HTML_Scss_Type::T_KEYWORD, 'without'];
109
110     protected $importPaths = [''];
111     protected $importCache = [];
112     protected $importedFiles = [];
113     protected $userFunctions = [];
114     protected $registeredVars = [];
115     protected $registeredFeatures = [
116         'extend-selector-pseudoclass' => false,
117         'at-error'                    => true,
118         'units-level-3'               => false,
119         'global-variable-shadowing'   => false,
120     ];
121
122     protected $encoding = null;
123     protected $lineNumberStyle = null;
124
125     protected $sourceMap = self::SOURCE_MAP_NONE;
126     protected $sourceMapOptions = [];
127
128     /**
129      * @var string|\Leafo\ScssPhp\Formatter
130      */
131     protected $formatter = 'Nested';
132
133     protected $rootEnv;
134     protected $rootBlock;
135
136     /**
137      * @var \Leafo\ScssPhp\Compiler\Environment
138      */
139     protected $env;
140     protected $scope;
141     protected $storeEnv;
142     protected $charsetSeen;
143     protected $sourceNames;
144
145     private $indentLevel;
146     private $commentsSeen;
147     private $extends;
148     private $extendsMap;
149     private $parsedFiles;
150     private $parser;
151     private $sourceIndex;
152     private $sourceLine;
153     private $sourceColumn;
154     private $stderr;
155     private $shouldEvaluate;
156     private $ignoreErrors;
157
158     /**
159      * Constructor
160      */
161     public function __construct()
162     {
163         $this->parsedFiles = [];
164         $this->sourceNames = [];
165     }
166
167     /**
168      * Compile scss
169      *
170      * @api
171      *
172      * @param string $code
173      * @param string $path
174      *
175      * @return string
176      */
177     public function compile($code, $path = null)
178     {
179         $this->indentLevel    = -1;
180         $this->commentsSeen   = [];
181         $this->extends        = [];
182         $this->extendsMap     = [];
183         $this->sourceIndex    = null;
184         $this->sourceLine     = null;
185         $this->sourceColumn   = null;
186         $this->env            = null;
187         $this->scope          = null;
188         $this->storeEnv       = null;
189         $this->charsetSeen    = null;
190         $this->shouldEvaluate = null;
191         $this->stderr         = fopen('php://stderr', 'w');
192
193         $this->parser = $this->parserFactory($path);
194         $tree = $this->parser->parse($code);
195         //print_R($tree);
196         $this->parser = null;
197
198         $formatter = 'HTML_Scss_Formatter_' . $this->formatter;
199         $fn = str_replace('_', '/', $formatter ) . '.php';
200         require_once $fn;
201         $this->formatter = new $formatter();
202         $this->rootBlock = null;
203         $this->rootEnv   = $this->pushEnv($tree);
204
205         $this->injectVariables($this->registeredVars);
206         $this->compileRoot($tree);
207         $this->popEnv();
208
209         $sourceMapGenerator = null;
210
211         if ($this->sourceMap) {
212             if (is_object($this->sourceMap) && $this->sourceMap instanceof HTML_Scss_SourceMap_SourceMapGenerator) {
213                 $sourceMapGenerator = $this->sourceMap;
214                 $this->sourceMap = self::SOURCE_MAP_FILE;
215             } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
216                 require_once 'Scss/SourceMap/SourceMapGenerator.php';
217                 $sourceMapGenerator = new HTML_Scss_SourceMap_SourceMapGenerator($this->sourceMapOptions);
218             }
219         }
220
221         $out = $this->formatter->format($this->scope, $sourceMapGenerator);
222
223         if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
224             $sourceMap    = $sourceMapGenerator->generateJson();
225             $sourceMapUrl = null;
226
227             switch ($this->sourceMap) {
228                 case self::SOURCE_MAP_INLINE:
229                     $sourceMapUrl = sprintf('data:application/json,%s', HTML_Scss_Util::encodeURIComponent($sourceMap));
230                     break;
231
232                 case self::SOURCE_MAP_FILE:
233                     $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
234                     break;
235             }
236
237             $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
238         }
239
240         return $out;
241     }
242
243     /**
244      * Instantiate parser
245      *
246      * @param string $path
247      *
248      * @return \Leafo\ScssPhp\Parser
249      */
250     protected function parserFactory($path)
251     {
252         $parser = new HTML_Scss_Parser($path, count($this->sourceNames), $this->encoding);
253
254         $this->sourceNames[] = $path;
255         $this->addParsedFile($path);
256
257         return $parser;
258     }
259
260     /**
261      * Is self extend?
262      *
263      * @param array $target
264      * @param array $origin
265      *
266      * @return boolean
267      */
268     protected function isSelfExtend($target, $origin)
269     {
270         foreach ($origin as $sel) {
271             if (in_array($target, $sel)) {
272                 return true;
273             }
274         }
275
276         return false;
277     }
278
279     /**
280      * Push extends
281      *
282      * @param array     $target
283      * @param array     $origin
284      * @param \stdClass $block
285      */
286     protected function pushExtends($target, $origin, $block)
287     {
288         if ($this->isSelfExtend($target, $origin)) {
289             return;
290         }
291
292         $i = count($this->extends);
293         $this->extends[] = [$target, $origin, $block];
294
295         foreach ($target as $part) {
296             if (isset($this->extendsMap[$part])) {
297                 $this->extendsMap[$part][] = $i;
298             } else {
299                 $this->extendsMap[$part] = [$i];
300             }
301         }
302     }
303
304     /**
305      * Make output block
306      *
307      * @param string $type
308      * @param array  $selectors
309      *
310      * @return \Leafo\ScssPhp\Formatter\OutputBlock
311      */
312     protected function makeOutputBlock($type, $selectors = null)
313     {
314         
315         $out = new HTML_Scss_Formatter_OutputBlock;
316         $out->type         = $type;
317         $out->lines        = [];
318         $out->children     = [];
319         $out->parent       = $this->scope;
320         $out->selectors    = $selectors;
321         $out->depth        = $this->env->depth;
322
323         if ($this->env->block instanceof HTML_Scss_Block) {
324             $out->sourceName   = $this->env->block->sourceName;
325             $out->sourceLine   = $this->env->block->sourceLine;
326             $out->sourceColumn = $this->env->block->sourceColumn;
327         } else {
328             $out->sourceName   = null;
329             $out->sourceLine   = null;
330             $out->sourceColumn = null;
331         }
332
333         return $out;
334     }
335
336     /**
337      * Compile root
338      *
339      * @param \Leafo\ScssPhp\Block $rootBlock
340      */
341     protected function compileRoot(HTML_Scss_Block $rootBlock)
342     {
343         $this->rootBlock = $this->scope = $this->makeOutputBlock(HTML_Scss_Type::T_ROOT);
344
345         $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
346         $this->flattenSelectors($this->scope);
347         $this->missingSelectors();
348     }
349
350     /**
351      * Report missing selectors
352      */
353     protected function missingSelectors()
354     {
355         foreach ($this->extends as $extend) {
356             if (isset($extend[3])) {
357                 continue;
358             }
359
360             list($target, $origin, $block) = $extend;
361
362             // ignore if !optional
363             if ($block[2]) {
364                 continue;
365             }
366
367             $target = implode(' ', $target);
368             $origin = $this->collapseSelectors($origin);
369
370             $this->sourceLine = $block[HTML_Scss_Parser::SOURCE_LINE];
371             $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
372         }
373     }
374
375     /**
376      * Flatten selectors
377      *
378      * @param \Leafo\ScssPhp\Formatter\OutputBlock $block
379      * @param string                               $parentKey
380      */
381     protected function flattenSelectors(HTML_Scss_Formatter_OutputBlock $block, $parentKey = null)
382     {
383         if ($block->selectors) {
384             $selectors = [];
385
386             foreach ($block->selectors as $s) {
387                 $selectors[] = $s;
388
389                 if (! is_array($s)) {
390                     continue;
391                 }
392
393                 // check extends
394                 if (! empty($this->extendsMap)) {
395                     $this->matchExtends($s, $selectors);
396
397                     // remove duplicates
398                     array_walk($selectors, function (&$value) {
399                         $value = serialize($value);
400                     });
401
402                     $selectors = array_unique($selectors);
403
404                     array_walk($selectors, function (&$value) {
405                         $value = unserialize($value);
406                     });
407                 }
408             }
409
410             $block->selectors = [];
411             $placeholderSelector = false;
412
413             foreach ($selectors as $selector) {
414                 if ($this->hasSelectorPlaceholder($selector)) {
415                     $placeholderSelector = true;
416                     continue;
417                 }
418
419                 $block->selectors[] = $this->compileSelector($selector);
420             }
421
422             if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
423                 unset($block->parent->children[$parentKey]);
424
425                 return;
426             }
427         }
428
429         foreach ($block->children as $key => $child) {
430             $this->flattenSelectors($child, $key);
431         }
432     }
433
434     /**
435      * Match extends
436      *
437      * @param array   $selector
438      * @param array   $out
439      * @param integer $from
440      * @param boolean $initial
441      */
442     protected function matchExtends($selector, &$out, $from = 0, $initial = true)
443     {
444         foreach ($selector as $i => $part) {
445             if ($i < $from) {
446                 continue;
447             }
448
449             if ($this->matchExtendsSingle($part, $origin)) {
450                 $after = array_slice($selector, $i + 1);
451                 $before = array_slice($selector, 0, $i);
452
453                 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
454
455                 foreach ($origin as $new) {
456                     $k = 0;
457
458                     // remove shared parts
459                     if ($initial) {
460                         while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
461                             $k++;
462                         }
463                     }
464
465                     $replacement = [];
466                     $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
467
468                     for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
469                         $slice = $tempReplacement[$l];
470                         array_unshift($replacement, $slice);
471
472                         if (! $this->isImmediateRelationshipCombinator(end($slice))) {
473                             break;
474                         }
475                     }
476
477                     $afterBefore = $l != 0 ? array_slice($tempReplacement, 0, $l) : [];
478
479                     // Merge shared direct relationships.
480                     $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
481
482                     $result = array_merge(
483                         $before,
484                         $mergedBefore,
485                         $replacement,
486                         $after
487                     );
488
489                     if ($result === $selector) {
490                         continue;
491                     }
492
493                     $out[] = $result;
494
495                     // recursively check for more matches
496                     $this->matchExtends($result, $out, count($before) + count($mergedBefore), false);
497
498                     // selector sequence merging
499                     if (! empty($before) && count($new) > 1) {
500                         $sharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
501                         $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
502
503                         list($injectBetweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
504
505                         $result2 = array_merge(
506                             $sharedParts,
507                             $injectBetweenSharedParts,
508                             $postSharedParts,
509                             $nonBreakable2,
510                             $nonBreakableBefore,
511                             $replacement,
512                             $after
513                         );
514
515                         $out[] = $result2;
516                     }
517                 }
518             }
519         }
520     }
521
522     /**
523      * Match extends single
524      *
525      * @param array $rawSingle
526      * @param array $outOrigin
527      *
528      * @return boolean
529      */
530     protected function matchExtendsSingle($rawSingle, &$outOrigin)
531     {
532         $counts = [];
533         $single = [];
534
535         foreach ($rawSingle as $part) {
536             // matches Number
537             if (! is_string($part)) {
538                 return false;
539             }
540
541             if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
542                 $single[count($single) - 1] .= $part;
543             } else {
544                 $single[] = $part;
545             }
546         }
547
548         $extendingDecoratedTag = false;
549
550         if (count($single) > 1) {
551             $matches = null;
552             $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
553         }
554
555         foreach ($single as $part) {
556             if (isset($this->extendsMap[$part])) {
557                 foreach ($this->extendsMap[$part] as $idx) {
558                     $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
559                 }
560             }
561         }
562
563         $outOrigin = [];
564         $found = false;
565
566         foreach ($counts as $idx => $count) {
567             list($target, $origin, /* $block */) = $this->extends[$idx];
568
569             // check count
570             if ($count !== count($target)) {
571                 continue;
572             }
573
574             $this->extends[$idx][3] = true;
575
576             $rem = array_diff($single, $target);
577
578             foreach ($origin as $j => $new) {
579                 // prevent infinite loop when target extends itself
580                 if ($this->isSelfExtend($single, $origin)) {
581                     return false;
582                 }
583
584                 $replacement = end($new);
585
586                 // Extending a decorated tag with another tag is not possible.
587                 if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
588                     preg_match('/^[a-z0-9]+$/i', $replacement[0])
589                 ) {
590                     unset($origin[$j]);
591                     continue;
592                 }
593
594                 $combined = $this->combineSelectorSingle($replacement, $rem);
595
596                 if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
597                     $origin[$j][count($origin[$j]) - 1] = $combined;
598                 }
599             }
600
601             $outOrigin = array_merge($outOrigin, $origin);
602
603             $found = true;
604         }
605
606         return $found;
607     }
608
609
610     /**
611      * Extract a relationship from the fragment.
612      *
613      * When extracting the last portion of a selector we will be left with a
614      * fragment which may end with a direction relationship combinator. This
615      * method will extract the relationship fragment and return it along side
616      * the rest.
617      *
618      * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
619      * @return array The selector without the relationship fragment if any, the relationship fragment.
620      */
621     protected function extractRelationshipFromFragment(array $fragment)
622     {
623         $parents = [];
624         $children = [];
625         $j = $i = count($fragment);
626
627         for (;;) {
628             $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
629             $parents = array_slice($fragment, 0, $j);
630             $slice = end($parents);
631
632             if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
633                 break;
634             }
635
636             $j -= 2;
637         }
638
639         return [$parents, $children];
640     }
641
642     /**
643      * Combine selector single
644      *
645      * @param array $base
646      * @param array $other
647      *
648      * @return array
649      */
650     protected function combineSelectorSingle($base, $other)
651     {
652         $tag = [];
653         $out = [];
654         $wasTag = true;
655
656         foreach ([$base, $other] as $single) {
657             foreach ($single as $part) {
658                 if (preg_match('/^[\[.:#]/', $part)) {
659                     $out[] = $part;
660                     $wasTag = false;
661                 } elseif (preg_match('/^[^_-]/', $part)) {
662                     $tag[] = $part;
663                     $wasTag = true;
664                 } elseif ($wasTag) {
665                     $tag[count($tag) - 1] .= $part;
666                 } else {
667                     $out[count($out) - 1] .= $part;
668                 }
669             }
670         }
671
672         if (count($tag)) {
673             array_unshift($out, $tag[0]);
674         }
675
676         return $out;
677     }
678
679     /**
680      * Compile media
681      *
682      * @param \Leafo\ScssPhp\Block $media
683      */
684     protected function compileMedia(HTML_Scss_Block $media)
685     {
686         $this->pushEnv($media);
687
688         $mediaQuery = $this->compileMediaQuery($this->multiplyMedia($this->env));
689
690         if (! empty($mediaQuery)) {
691             $this->scope = $this->makeOutputBlock(HTML_Scss_Type::T_MEDIA, [$mediaQuery]);
692
693             $parentScope = $this->mediaParent($this->scope);
694             $parentScope->children[] = $this->scope;
695
696             // top level properties in a media cause it to be wrapped
697             $needsWrap = false;
698
699             foreach ($media->children as $child) {
700                 $type = $child[0];
701
702                 if ($type !== HTML_Scss_Type::T_BLOCK &&
703                     $type !== HTML_Scss_Type::T_MEDIA &&
704                     $type !== HTML_Scss_Type::T_DIRECTIVE &&
705                     $type !== HTML_Scss_Type::T_IMPORT
706                 ) {
707                     $needsWrap = true;
708                     break;
709                 }
710             }
711
712             if ($needsWrap) {
713                 $wrapped = new HTML_Scss_Block;
714                 $wrapped->sourceName   = $media->sourceName;
715                 $wrapped->sourceIndex  = $media->sourceIndex;
716                 $wrapped->sourceLine   = $media->sourceLine;
717                 $wrapped->sourceColumn = $media->sourceColumn;
718                 $wrapped->selectors    = [];
719                 $wrapped->comments     = [];
720                 $wrapped->parent       = $media;
721                 $wrapped->children     = $media->children;
722
723                 $media->children = [[HTML_Scss_Type::T_BLOCK, $wrapped]];
724             }
725
726             $this->compileChildrenNoReturn($media->children, $this->scope);
727
728             $this->scope = $this->scope->parent;
729         }
730
731         $this->popEnv();
732     }
733
734     /**
735      * Media parent
736      *
737      * @param \Leafo\ScssPhp\Formatter\OutputBlock $scope
738      *
739      * @return \Leafo\ScssPhp\Formatter\OutputBlock
740      */
741     protected function mediaParent(HTML_Scss_Formatter_OutputBlock $scope)
742     {
743         while (! empty($scope->parent)) {
744             if (! empty($scope->type) && $scope->type !== HTML_Scss_Type::T_MEDIA) {
745                 break;
746             }
747
748             $scope = $scope->parent;
749         }
750
751         return $scope;
752     }
753
754     /**
755      * Compile directive
756      *
757      * @param \Leafo\ScssPhp\Block $block
758      */
759     protected function compileDirective(HTML_Scss_Block $block)
760     {
761         $s = '@' . $block->name;
762
763         if (! empty($block->value)) {
764             $s .= ' ' . $this->compileValue($block->value);
765         }
766
767         if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
768             $this->compileKeyframeBlock($block, [$s]);
769         } else {
770             $this->compileNestedBlock($block, [$s]);
771         }
772     }
773
774     /**
775      * Compile at-root
776      *
777      * @param \Leafo\ScssPhp\Block $block
778      */
779     protected function compileAtRoot(HTML_Scss_Block $block)
780     {
781         $env     = $this->pushEnv($block);
782         $envs    = $this->compactEnv($env);
783         $without = isset($block->with) ? $this->compileWith($block->with) : static::WITH_RULE;
784
785         // wrap inline selector
786         if ($block->selector) {
787             $wrapped = new HTML_Scss_Block;
788             $wrapped->sourceName   = $block->sourceName;
789             $wrapped->sourceIndex  = $block->sourceIndex;
790             $wrapped->sourceLine   = $block->sourceLine;
791             $wrapped->sourceColumn = $block->sourceColumn;
792             $wrapped->selectors    = $block->selector;
793             $wrapped->comments     = [];
794             $wrapped->parent       = $block;
795             $wrapped->children     = $block->children;
796
797             $block->children = [[HTML_Scss_Type::T_BLOCK, $wrapped]];
798         }
799
800         $this->env = $this->filterWithout($envs, $without);
801         $newBlock  = $this->spliceTree($envs, $block, $without);
802
803         $saveScope   = $this->scope;
804         $this->scope = $this->rootBlock;
805
806         $this->compileChild($newBlock, $this->scope);
807
808         $this->scope = $saveScope;
809         $this->env   = $this->extractEnv($envs);
810
811         $this->popEnv();
812     }
813
814     /**
815      * Splice parse tree
816      *
817      * @param array                $envs
818      * @param \Leafo\ScssPhp\Block $block
819      * @param integer              $without
820      *
821      * @return array
822      */
823     private function spliceTree($envs, HTML_Scss_Block $block, $without)
824     {
825         $newBlock = null;
826
827         foreach ($envs as $e) {
828             if (! isset($e->block)) {
829                 continue;
830             }
831
832             if ($e->block === $block) {
833                 continue;
834             }
835
836             if (isset($e->block->type) && $e->block->type === HTML_Scss_Type::T_AT_ROOT) {
837                 continue;
838             }
839
840             if ($e->block && $this->isWithout($without, $e->block)) {
841                 continue;
842             }
843
844             $b = new HTML_Scss_Block;
845             $b->sourceName   = $e->block->sourceName;
846             $b->sourceIndex  = $e->block->sourceIndex;
847             $b->sourceLine   = $e->block->sourceLine;
848             $b->sourceColumn = $e->block->sourceColumn;
849             $b->selectors    = [];
850             $b->comments     = $e->block->comments;
851             $b->parent       = null;
852
853             if ($newBlock) {
854                 $type = isset($newBlock->type) ? $newBlock->type : HTML_Scss_Type::T_BLOCK;
855
856                 $b->children = [[$type, $newBlock]];
857
858                 $newBlock->parent = $b;
859             } elseif (count($block->children)) {
860                 foreach ($block->children as $child) {
861                     if ($child[0] === HTML_Scss_Type::T_BLOCK) {
862                         $child[1]->parent = $b;
863                     }
864                 }
865
866                 $b->children = $block->children;
867             }
868
869             if (isset($e->block->type)) {
870                 $b->type = $e->block->type;
871             }
872
873             if (isset($e->block->name)) {
874                 $b->name = $e->block->name;
875             }
876
877             if (isset($e->block->queryList)) {
878                 $b->queryList = $e->block->queryList;
879             }
880
881             if (isset($e->block->value)) {
882                 $b->value = $e->block->value;
883             }
884
885             $newBlock = $b;
886         }
887
888         $type = isset($newBlock->type) ? $newBlock->type : HTML_Scss_Type::T_BLOCK;
889
890         return [$type, $newBlock];
891     }
892
893     /**
894      * Compile @at-root's with: inclusion / without: exclusion into filter flags
895      *
896      * @param array $with
897      *
898      * @return integer
899      */
900     private function compileWith($with)
901     {
902         static $mapping = [
903             'rule'     => self::WITH_RULE,
904             'media'    => self::WITH_MEDIA,
905             'supports' => self::WITH_SUPPORTS,
906             'all'      => self::WITH_ALL,
907         ];
908
909         // exclude selectors by default
910         $without = static::WITH_RULE;
911
912         if ($this->libMapHasKey([$with, static::$with])) {
913             $without = static::WITH_ALL;
914
915             $list = $this->coerceList($this->libMapGet([$with, static::$with]));
916
917             foreach ($list[2] as $item) {
918                 $keyword = $this->compileStringContent($this->coerceString($item));
919
920                 if (array_key_exists($keyword, $mapping)) {
921                     $without &= ~($mapping[$keyword]);
922                 }
923             }
924         }
925
926         if ($this->libMapHasKey([$with, static::$without])) {
927             $without = 0;
928
929             $list = $this->coerceList($this->libMapGet([$with, static::$without]));
930
931             foreach ($list[2] as $item) {
932                 $keyword = $this->compileStringContent($this->coerceString($item));
933
934                 if (array_key_exists($keyword, $mapping)) {
935                     $without |= $mapping[$keyword];
936                 }
937             }
938         }
939
940         return $without;
941     }
942
943     /**
944      * Filter env stack
945      *
946      * @param array   $envs
947      * @param integer $without
948      *
949      * @return \Leafo\ScssPhp\Compiler\Environment
950      */
951     private function filterWithout($envs, $without)
952     {
953         $filtered = [];
954
955         foreach ($envs as $e) {
956             if ($e->block && $this->isWithout($without, $e->block)) {
957                 continue;
958             }
959
960             $filtered[] = $e;
961         }
962
963         return $this->extractEnv($filtered);
964     }
965
966     /**
967      * Filter WITH rules
968      *
969      * @param integer              $without
970      * @param \Leafo\ScssPhp\Block $block
971      *
972      * @return boolean
973      */
974     private function isWithout($without, HTML_Scss_Block $block)
975     {
976         if ((($without & static::WITH_RULE) && isset($block->selectors)) ||
977             (($without & static::WITH_MEDIA) &&
978                 isset($block->type) && $block->type === HTML_Scss_Type::T_MEDIA) ||
979             (($without & static::WITH_SUPPORTS) &&
980                 isset($block->type) && $block->type === HTML_Scss_Type::T_DIRECTIVE &&
981                 isset($block->name) && $block->name === 'supports')
982         ) {
983             return true;
984         }
985
986         return false;
987     }
988
989     /**
990      * Compile keyframe block
991      *
992      * @param \Leafo\ScssPhp\Block $block
993      * @param array                $selectors
994      */
995     protected function compileKeyframeBlock(HTML_Scss_Block $block, $selectors)
996     {
997         $env = $this->pushEnv($block);
998
999         $envs = $this->compactEnv($env);
1000
1001         $this->env = $this->extractEnv(array_filter($envs, function (HTML_Scss_Compiler_Environment $e) {
1002             return ! isset($e->block->selectors);
1003         }));
1004
1005         $this->scope = $this->makeOutputBlock($block->type, $selectors);
1006         $this->scope->depth = 1;
1007         $this->scope->parent->children[] = $this->scope;
1008
1009         $this->compileChildrenNoReturn($block->children, $this->scope);
1010
1011         $this->scope = $this->scope->parent;
1012         $this->env   = $this->extractEnv($envs);
1013
1014         $this->popEnv();
1015     }
1016
1017     /**
1018      * Compile nested block
1019      *
1020      * @param \Leafo\ScssPhp\Block $block
1021      * @param array                $selectors
1022      */
1023     protected function compileNestedBlock(HTML_Scss_Block $block, $selectors)
1024     {
1025         $this->pushEnv($block);
1026
1027         $this->scope = $this->makeOutputBlock($block->type, $selectors);
1028         $this->scope->parent->children[] = $this->scope;
1029
1030         $this->compileChildrenNoReturn($block->children, $this->scope);
1031
1032         $this->scope = $this->scope->parent;
1033
1034         $this->popEnv();
1035     }
1036
1037     /**
1038      * Recursively compiles a block.
1039      *
1040      * A block is analogous to a CSS block in most cases. A single SCSS document
1041      * is encapsulated in a block when parsed, but it does not have parent tags
1042      * so all of its children appear on the root level when compiled.
1043      *
1044      * Blocks are made up of selectors and children.
1045      *
1046      * The children of a block are just all the blocks that are defined within.
1047      *
1048      * Compiling the block involves pushing a fresh environment on the stack,
1049      * and iterating through the props, compiling each one.
1050      *
1051      * @see Compiler::compileChild()
1052      *
1053      * @param \Leafo\ScssPhp\Block $block
1054      */
1055     protected function compileBlock(HTML_Scss_Block $block)
1056     {
1057         $env = $this->pushEnv($block);
1058         $env->selectors = $this->evalSelectors($block->selectors);
1059
1060         $out = $this->makeOutputBlock(null);
1061
1062         if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
1063             $annotation = $this->makeOutputBlock(HTML_Scss_Type::T_COMMENT);
1064             $annotation->depth = 0;
1065
1066             $file = $this->sourceNames[$block->sourceIndex];
1067             $line = $block->sourceLine;
1068
1069             switch ($this->lineNumberStyle) {
1070                 case static::LINE_COMMENTS:
1071                     $annotation->lines[] = '/* line ' . $line
1072                                          . ($file ? ', ' . $file : '')
1073                                          . ' */';
1074                     break;
1075
1076                 case static::DEBUG_INFO:
1077                     $annotation->lines[] = '@media -sass-debug-info{'
1078                                          . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1079                                          . 'line{font-family:' . $line . '}}';
1080                     break;
1081             }
1082
1083             $this->scope->children[] = $annotation;
1084         }
1085
1086         $this->scope->children[] = $out;
1087
1088         if (count($block->children)) {
1089             $out->selectors = $this->multiplySelectors($env);
1090
1091             $this->compileChildrenNoReturn($block->children, $out);
1092         }
1093
1094         $this->formatter->stripSemicolon($out->lines);
1095
1096         $this->popEnv();
1097     }
1098
1099     /**
1100      * Compile root level comment
1101      *
1102      * @param array $block
1103      */
1104     protected function compileComment($block)
1105     {
1106         $out = $this->makeOutputBlock(HTML_Scss_Type::T_COMMENT);
1107         $out->lines[] = $block[1];
1108         $this->scope->children[] = $out;
1109     }
1110
1111     /**
1112      * Evaluate selectors
1113      *
1114      * @param array $selectors
1115      *
1116      * @return array
1117      */
1118     protected function evalSelectors($selectors)
1119     {
1120         $this->shouldEvaluate = false;
1121
1122         $selectors = array_map([$this, 'evalSelector'], $selectors);
1123
1124         // after evaluating interpolates, we might need a second pass
1125         if ($this->shouldEvaluate) {
1126             $buffer = $this->collapseSelectors($selectors);
1127             $parser = $this->parserFactory(__METHOD__);
1128
1129             if ($parser->parseSelector($buffer, $newSelectors)) {
1130                 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1131             }
1132         }
1133
1134         return $selectors;
1135     }
1136
1137     /**
1138      * Evaluate selector
1139      *
1140      * @param array $selector
1141      *
1142      * @return array
1143      */
1144     protected function evalSelector($selector)
1145     {
1146         return array_map([$this, 'evalSelectorPart'], $selector);
1147     }
1148
1149     /**
1150      * Evaluate selector part; replaces all the interpolates, stripping quotes
1151      *
1152      * @param array $part
1153      *
1154      * @return array
1155      */
1156     protected function evalSelectorPart($part)
1157     {
1158         foreach ($part as &$p) {
1159             if (is_array($p) && ($p[0] === HTML_Scss_Type::T_INTERPOLATE || $p[0] === HTML_Scss_Type::T_STRING)) {
1160                 $p = $this->compileValue($p);
1161
1162                 // force re-evaluation
1163                 if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
1164                     $this->shouldEvaluate = true;
1165                 }
1166             } elseif (is_string($p) && strlen($p) >= 2 &&
1167                 ($first = $p[0]) && ($first === '"' || $first === "'") &&
1168                 substr($p, -1) === $first
1169             ) {
1170                 $p = substr($p, 1, -1);
1171             }
1172         }
1173
1174         return $this->flattenSelectorSingle($part);
1175     }
1176
1177     /**
1178      * Collapse selectors
1179      *
1180      * @param array $selectors
1181      *
1182      * @return string
1183      */
1184     protected function collapseSelectors($selectors)
1185     {
1186         $parts = [];
1187
1188         foreach ($selectors as $selector) {
1189             $output = '';
1190
1191             array_walk_recursive(
1192                 $selector,
1193                 function ($value, $key) use (&$output) {
1194                     $output .= $value;
1195                 }
1196             );
1197
1198             $parts[] = $output;
1199         }
1200
1201         return implode(', ', $parts);
1202     }
1203
1204     /**
1205      * Flatten selector single; joins together .classes and #ids
1206      *
1207      * @param array $single
1208      *
1209      * @return array
1210      */
1211     protected function flattenSelectorSingle($single)
1212     {
1213         $joined = [];
1214
1215         foreach ($single as $part) {
1216             if (empty($joined) ||
1217                 ! is_string($part) ||
1218                 preg_match('/[\[.:#%]/', $part)
1219             ) {
1220                 $joined[] = $part;
1221                 continue;
1222             }
1223
1224             if (is_array(end($joined))) {
1225                 $joined[] = $part;
1226             } else {
1227                 $joined[count($joined) - 1] .= $part;
1228             }
1229         }
1230
1231         return $joined;
1232     }
1233
1234     /**
1235      * Compile selector to string; self(&) should have been replaced by now
1236      *
1237      * @param string|array $selector
1238      *
1239      * @return string
1240      */
1241     protected function compileSelector($selector)
1242     {
1243         if (! is_array($selector)) {
1244             return $selector; // media and the like
1245         }
1246
1247         return implode(
1248             ' ',
1249             array_map(
1250                 [$this, 'compileSelectorPart'],
1251                 $selector
1252             )
1253         );
1254     }
1255
1256     /**
1257      * Compile selector part
1258      *
1259      * @param array $piece
1260      *
1261      * @return string
1262      */
1263     protected function compileSelectorPart($piece)
1264     {
1265         foreach ($piece as &$p) {
1266             if (! is_array($p)) {
1267                 continue;
1268             }
1269
1270             switch ($p[0]) {
1271                 case HTML_Scss_Type::T_SELF:
1272                     $p = '&';
1273                     break;
1274
1275                 default:
1276                     $p = $this->compileValue($p);
1277                     break;
1278             }
1279         }
1280
1281         return implode($piece);
1282     }
1283
1284     /**
1285      * Has selector placeholder?
1286      *
1287      * @param array $selector
1288      *
1289      * @return boolean
1290      */
1291     protected function hasSelectorPlaceholder($selector)
1292     {
1293         if (! is_array($selector)) {
1294             return false;
1295         }
1296
1297         foreach ($selector as $parts) {
1298             foreach ($parts as $part) {
1299                 if (strlen($part) && '%' === $part[0]) {
1300                     return true;
1301                 }
1302             }
1303         }
1304
1305         return false;
1306     }
1307
1308     /**
1309      * Compile children and return result
1310      *
1311      * @param array                                $stms
1312      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1313      *
1314      * @return array
1315      */
1316     protected function compileChildren($stms, HTML_Scss_Formatter_OutputBlock $out)
1317     {
1318         foreach ($stms as $stm) {
1319             $ret = $this->compileChild($stm, $out);
1320
1321             if (isset($ret)) {
1322                 return $ret;
1323             }
1324         }
1325     }
1326
1327     /**
1328      * Compile children and throw exception if unexpected @return
1329      *
1330      * @param array                                $stms
1331      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1332      *
1333      * @throws \Exception
1334      */
1335     protected function compileChildrenNoReturn($stms, HTML_Scss_Formatter_OutputBlock $out)
1336     {
1337         foreach ($stms as $stm) {
1338             $ret = $this->compileChild($stm, $out);
1339
1340             if (isset($ret)) {
1341                 $this->throwError('@return may only be used within a function');
1342
1343                 return;
1344             }
1345         }
1346     }
1347
1348     /**
1349      * Compile media query
1350      *
1351      * @param array $queryList
1352      *
1353      * @return string
1354      */
1355     protected function compileMediaQuery($queryList)
1356     {
1357         $out = '@media';
1358         $first = true;
1359
1360         foreach ($queryList as $query) {
1361             $type = null;
1362             $parts = [];
1363
1364             foreach ($query as $q) {
1365                 switch ($q[0]) {
1366                     case HTML_Scss_Type::T_MEDIA_TYPE:
1367                         if ($type) {
1368                             $type = $this->mergeMediaTypes(
1369                                 $type,
1370                                 array_map([$this, 'compileValue'], array_slice($q, 1))
1371                             );
1372
1373                             if (empty($type)) { // merge failed
1374                                 return null;
1375                             }
1376                         } else {
1377                             $type = array_map([$this, 'compileValue'], array_slice($q, 1));
1378                         }
1379                         break;
1380
1381                     case HTML_Scss_Type::T_MEDIA_EXPRESSION:
1382                         if (isset($q[2])) {
1383                             $parts[] = '('
1384                                 . $this->compileValue($q[1])
1385                                 . $this->formatter->assignSeparator
1386                                 . $this->compileValue($q[2])
1387                                 . ')';
1388                         } else {
1389                             $parts[] = '('
1390                                 . $this->compileValue($q[1])
1391                                 . ')';
1392                         }
1393                         break;
1394
1395                     case HTML_Scss_Type::T_MEDIA_VALUE:
1396                         $parts[] = $this->compileValue($q[1]);
1397                         break;
1398                 }
1399             }
1400
1401             if ($type) {
1402                 array_unshift($parts, implode(' ', array_filter($type)));
1403             }
1404
1405             if (! empty($parts)) {
1406                 if ($first) {
1407                     $first = false;
1408                     $out .= ' ';
1409                 } else {
1410                     $out .= $this->formatter->tagSeparator;
1411                 }
1412
1413                 $out .= implode(' and ', $parts);
1414             }
1415         }
1416
1417         return $out;
1418     }
1419
1420     protected function mergeDirectRelationships($selectors1, $selectors2)
1421     {
1422         if (empty($selectors1) || empty($selectors2)) {
1423             return array_merge($selectors1, $selectors2);
1424         }
1425
1426         $part1 = end($selectors1);
1427         $part2 = end($selectors2);
1428
1429         if (! $this->isImmediateRelationshipCombinator($part1[0]) || $part1 !== $part2) {
1430             return array_merge($selectors1, $selectors2);
1431         }
1432
1433         $merged = [];
1434
1435         do {
1436             $part1 = array_pop($selectors1);
1437             $part2 = array_pop($selectors2);
1438
1439             if ($this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
1440                 $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
1441                 break;
1442             }
1443
1444             array_unshift($merged, $part1);
1445             array_unshift($merged, [array_pop($selectors1)[0] . array_pop($selectors2)[0]]);
1446         } while (! empty($selectors1) && ! empty($selectors2));
1447
1448         return $merged;
1449     }
1450
1451     /**
1452      * Merge media types
1453      *
1454      * @param array $type1
1455      * @param array $type2
1456      *
1457      * @return array|null
1458      */
1459     protected function mergeMediaTypes($type1, $type2)
1460     {
1461         if (empty($type1)) {
1462             return $type2;
1463         }
1464
1465         if (empty($type2)) {
1466             return $type1;
1467         }
1468
1469         $m1 = '';
1470         $t1 = '';
1471
1472         if (count($type1) > 1) {
1473             $m1= strtolower($type1[0]);
1474             $t1= strtolower($type1[1]);
1475         } else {
1476             $t1 = strtolower($type1[0]);
1477         }
1478
1479         $m2 = '';
1480         $t2 = '';
1481
1482         if (count($type2) > 1) {
1483             $m2 = strtolower($type2[0]);
1484             $t2 = strtolower($type2[1]);
1485         } else {
1486             $t2 = strtolower($type2[0]);
1487         }
1488
1489         if (($m1 === HTML_Scss_Type::T_NOT) ^ ($m2 === HTML_Scss_Type::T_NOT)) {
1490             if ($t1 === $t2) {
1491                 return null;
1492             }
1493
1494             return [
1495                 $m1 === HTML_Scss_Type::T_NOT ? $m2 : $m1,
1496                 $m1 === HTML_Scss_Type::T_NOT ? $t2 : $t1,
1497             ];
1498         }
1499
1500         if ($m1 === HTML_Scss_Type::T_NOT && $m2 === HTML_Scss_Type::T_NOT) {
1501             // CSS has no way of representing "neither screen nor print"
1502             if ($t1 !== $t2) {
1503                 return null;
1504             }
1505
1506             return [HTML_Scss_Type::T_NOT, $t1];
1507         }
1508
1509         if ($t1 !== $t2) {
1510             return null;
1511         }
1512
1513         // t1 == t2, neither m1 nor m2 are "not"
1514         return [empty($m1)? $m2 : $m1, $t1];
1515     }
1516
1517     /**
1518      * Compile import; returns true if the value was something that could be imported
1519      *
1520      * @param array   $rawPath
1521      * @param array   $out
1522      * @param boolean $once
1523      *
1524      * @return boolean
1525      */
1526     protected function compileImport($rawPath, $out, $once = false)
1527     {
1528          
1529         if ($rawPath[0] === HTML_Scss_Type::T_STRING) {
1530             //print_R($rawPath);
1531             $path = $this->compileStringContent($rawPath);
1532             //var_dump($path);
1533             if ($npath = $this->findImport($path)) {
1534                 if (! $once || ! in_array($npath, $this->importedFiles)) {
1535                     $this->importFile($npath, $out);
1536                     $this->importedFiles[] = $npath;
1537                 }
1538
1539                 return true;
1540             }
1541             die("could not find @import : " . $path ."\n");
1542
1543             return false;
1544         }
1545
1546         if ($rawPath[0] === HTML_Scss_Type::T_LIST) {
1547             // handle a list of strings
1548             if (count($rawPath[2]) === 0) {
1549                 return false;
1550             }
1551
1552             foreach ($rawPath[2] as $path) {
1553                 if ($path[0] !== HTML_Scss_Type::T_STRING) {
1554                     return false;
1555                 }
1556             }
1557
1558             foreach ($rawPath[2] as $path) {
1559                 $this->compileImport($path, $out);
1560             }
1561
1562             return true;
1563         }
1564
1565         return false;
1566     }
1567
1568     /**
1569      * Compile child; returns a value to halt execution
1570      *
1571      * @param array                                $child
1572      * @param \Leafo\ScssPhp\Formatter\OutputBlock $out
1573      *
1574      * @return array
1575      */
1576     protected function compileChild($child, HTML_Scss_Formatter_OutputBlock $out)
1577     {
1578         $this->sourceIndex  = isset($child[HTML_Scss_Parser::SOURCE_INDEX]) ? $child[HTML_Scss_Parser::SOURCE_INDEX] : null;
1579         $this->sourceLine   = isset($child[HTML_Scss_Parser::SOURCE_LINE]) ? $child[HTML_Scss_Parser::SOURCE_LINE] : -1;
1580         $this->sourceColumn = isset($child[HTML_Scss_Parser::SOURCE_COLUMN]) ? $child[HTML_Scss_Parser::SOURCE_COLUMN] : -1;
1581
1582         switch ($child[0]) {
1583             case HTML_Scss_Type::T_SCSSPHP_IMPORT_ONCE:
1584                 list(, $rawPath) = $child;
1585
1586                 $rawPath = $this->reduce($rawPath);
1587
1588                 if (! $this->compileImport($rawPath, $out, true)) {
1589                     $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1590                 }
1591                 break;
1592
1593             case HTML_Scss_Type::T_IMPORT:
1594                 list(, $rawPath) = $child;
1595                 //echo "import:"; print_r($child);
1596                 $rawPath = $this->reduce($rawPath);
1597                 //echo "rawpath:"; print_r($rawPath);
1598                 if (! $this->compileImport($rawPath, $out)) {
1599                     $out->lines[] = '@import ' . $this->compileValue($rawPath) . ';';
1600                 }
1601                 break;
1602
1603             case HTML_Scss_Type::T_DIRECTIVE:
1604                 $this->compileDirective($child[1]);
1605                 break;
1606
1607             case HTML_Scss_Type::T_AT_ROOT:
1608                 $this->compileAtRoot($child[1]);
1609                 break;
1610
1611             case HTML_Scss_Type::T_MEDIA:
1612                 $this->compileMedia($child[1]);
1613                 break;
1614
1615             case HTML_Scss_Type::T_BLOCK:
1616                 $this->compileBlock($child[1]);
1617                 break;
1618
1619             case HTML_Scss_Type::T_CHARSET:
1620                 if (! $this->charsetSeen) {
1621                     $this->charsetSeen = true;
1622
1623                     $out->lines[] = '@charset ' . $this->compileValue($child[1]) . ';';
1624                 }
1625                 break;
1626
1627             case HTML_Scss_Type::T_ASSIGN:
1628                 list(, $name, $value) = $child;
1629
1630                 if ($name[0] === HTML_Scss_Type::T_VARIABLE) {
1631                     $flags = isset($child[3]) ? $child[3] : [];
1632                     $isDefault = in_array('!default', $flags);
1633                     $isGlobal = in_array('!global', $flags);
1634
1635                     if ($isGlobal) {
1636                         $this->set($name[1], $this->reduce($value), false, $this->rootEnv);
1637                         break;
1638                     }
1639
1640                     $shouldSet = $isDefault &&
1641                         (($result = $this->get($name[1], false)) === null
1642                         || $result === static::$null);
1643
1644                     if (! $isDefault || $shouldSet) {
1645                         $this->set($name[1], $this->reduce($value));
1646                     }
1647                     break;
1648                 }
1649
1650                 $compiledName = $this->compileValue($name);
1651
1652                 // handle shorthand syntax: size / line-height
1653                 if ($compiledName === 'font') {
1654                     if ($value[0] === HTML_Scss_Type::T_EXPRESSION && $value[1] === '/') {
1655                         $value = $this->expToString($value);
1656                     } elseif ($value[0] === HTML_Scss_Type::T_LIST) {
1657                         foreach ($value[2] as &$item) {
1658                             if ($item[0] === HTML_Scss_Type::T_EXPRESSION && $item[1] === '/') {
1659                                 $item = $this->expToString($item);
1660                             }
1661                         }
1662                     }
1663                 }
1664
1665                 // if the value reduces to null from something else then
1666                 // the property should be discarded
1667                 if ($value[0] !== HTML_Scss_Type::T_NULL) {
1668                     $value = $this->reduce($value);
1669
1670                     if ($value[0] === HTML_Scss_Type::T_NULL || $value === static::$nullString) {
1671                         break;
1672                     }
1673                 }
1674
1675                 $compiledValue = $this->compileValue($value);
1676
1677                 $out->lines[] = $this->formatter->property(
1678                     $compiledName,
1679                     $compiledValue
1680                 );
1681                 break;
1682
1683             case HTML_Scss_Type::T_COMMENT:
1684                 if ($out->type === HTML_Scss_Type::T_ROOT) {
1685                     $this->compileComment($child);
1686                     break;
1687                 }
1688
1689                 $out->lines[] = $child[1];
1690                 break;
1691
1692             case HTML_Scss_Type::T_MIXIN:
1693             case HTML_Scss_Type::T_FUNCTION:
1694                 list(, $block) = $child;
1695
1696                 $this->set(static::$namespaces[$block->type] . $block->name, $block);
1697                 break;
1698
1699             case HTML_Scss_Type::T_EXTEND:
1700                 list(, $selectors) = $child;
1701
1702                 foreach ($selectors as $sel) {
1703                     $results = $this->evalSelectors([$sel]);
1704
1705                     foreach ($results as $result) {
1706                         // only use the first one
1707                         $result = current($result);
1708
1709                         $this->pushExtends($result, $out->selectors, $child);
1710                     }
1711                 }
1712                 break;
1713
1714             case HTML_Scss_Type::T_IF:
1715                 list(, $if) = $child;
1716
1717                 if ($this->isTruthy($this->reduce($if->cond, true))) {
1718                     return $this->compileChildren($if->children, $out);
1719                 }
1720
1721                 foreach ($if->cases as $case) {
1722                     if ($case->type === HTML_Scss_Type::T_ELSE ||
1723                         $case->type === HTML_Scss_Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
1724                     ) {
1725                         return $this->compileChildren($case->children, $out);
1726                     }
1727                 }
1728                 break;
1729
1730             case HTML_Scss_Type::T_EACH:
1731                 list(, $each) = $child;
1732
1733                 $list = $this->coerceList($this->reduce($each->list));
1734
1735                 $this->pushEnv();
1736
1737                 foreach ($list[2] as $item) {
1738                     if (count($each->vars) === 1) {
1739                         $this->set($each->vars[0], $item, true);
1740                     } else {
1741                         list(,, $values) = $this->coerceList($item);
1742
1743                         foreach ($each->vars as $i => $var) {
1744                             $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
1745                         }
1746                     }
1747
1748                     $ret = $this->compileChildren($each->children, $out);
1749
1750                     if ($ret) {
1751                         if ($ret[0] !== HTML_Scss_Type::T_CONTROL) {
1752                             $this->popEnv();
1753
1754                             return $ret;
1755                         }
1756
1757                         if ($ret[1]) {
1758                             break;
1759                         }
1760                     }
1761                 }
1762
1763                 $this->popEnv();
1764                 break;
1765
1766             case HTML_Scss_Type::T_WHILE:
1767                 list(, $while) = $child;
1768
1769                 while ($this->isTruthy($this->reduce($while->cond, true))) {
1770                     $ret = $this->compileChildren($while->children, $out);
1771
1772                     if ($ret) {
1773                         if ($ret[0] !== HTML_Scss_Type::T_CONTROL) {
1774                             return $ret;
1775                         }
1776
1777                         if ($ret[1]) {
1778                             break;
1779                         }
1780                     }
1781                 }
1782                 break;
1783
1784             case HTML_Scss_Type::T_FOR:
1785                 list(, $for) = $child;
1786
1787                 $start = $this->reduce($for->start, true);
1788                 $end   = $this->reduce($for->end, true);
1789
1790                 if (! ($start[2] == $end[2] || $end->unitless())) {
1791                     $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
1792
1793                     break;
1794                 }
1795
1796                 $unit  = $start[2];
1797                 $start = $start[1];
1798                 $end   = $end[1];
1799
1800                 $d = $start < $end ? 1 : -1;
1801  
1802
1803                 for (;;) {
1804                     if ((! $for->until && $start - $d == $end) ||
1805                         ($for->until && $start == $end)
1806                     ) {
1807                         break;
1808                     }
1809                                                 
1810                     $this->set($for->var, new HTML_Scss_Node_Number($start, $unit));
1811                     $start += $d;
1812
1813                     $ret = $this->compileChildren($for->children, $out);
1814
1815                     if ($ret) {
1816                         if ($ret[0] !== HTML_Scss_Type::T_CONTROL) {
1817                             return $ret;
1818                         }
1819
1820                         if ($ret[1]) {
1821                             break;
1822                         }
1823                     }
1824                 }
1825                 break;
1826
1827             case HTML_Scss_Type::T_BREAK:
1828                 return [HTML_Scss_Type::T_CONTROL, true];
1829
1830             case HTML_Scss_Type::T_CONTINUE:
1831                 return [HTML_Scss_Type::T_CONTROL, false];
1832
1833             case HTML_Scss_Type::T_RETURN:
1834                 return $this->reduce($child[1], true);
1835
1836             case HTML_Scss_Type::T_NESTED_PROPERTY:
1837                 list(, $prop) = $child;
1838
1839                 $prefixed = [];
1840                 $prefix = $this->compileValue($prop->prefix) . '-';
1841
1842                 foreach ($prop->children as $child) {
1843                     switch ($child[0]) {
1844                         case HTML_Scss_Type::T_ASSIGN:
1845                             array_unshift($child[1][2], $prefix);
1846                             break;
1847
1848                         case HTML_Scss_Type::T_NESTED_PROPERTY:
1849                             array_unshift($child[1]->prefix[2], $prefix);
1850                             break;
1851                     }
1852
1853                     $prefixed[] = $child;
1854                 }
1855
1856                 $this->compileChildrenNoReturn($prefixed, $out);
1857                 break;
1858
1859             case HTML_Scss_Type::T_INCLUDE:
1860                 // including a mixin
1861                 list(, $name, $argValues, $content) = $child;
1862
1863                 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
1864
1865                 if (! $mixin) {
1866                     $this->throwError("Undefined mixin $name");
1867                     break;
1868                 }
1869
1870                 $callingScope = $this->getStoreEnv();
1871
1872                 // push scope, apply args
1873                 $this->pushEnv();
1874                 $this->env->depth--;
1875
1876                 $storeEnv = $this->storeEnv;
1877                 $this->storeEnv = $this->env;
1878
1879                 if (isset($content)) {
1880                     $content->scope = $callingScope;
1881
1882                     $this->setRaw(static::$namespaces['special'] . 'content', $content, $this->env);
1883                 }
1884
1885                 if (isset($mixin->args)) {
1886                     $this->applyArguments($mixin->args, $argValues);
1887                 }
1888
1889                 $this->env->marker = 'mixin';
1890
1891                 $this->compileChildrenNoReturn($mixin->children, $out);
1892
1893                 $this->storeEnv = $storeEnv;
1894
1895                 $this->popEnv();
1896                 break;
1897
1898             case HTML_Scss_Type::T_MIXIN_CONTENT:
1899                 $content = $this->get(static::$namespaces['special'] . 'content', false, $this->getStoreEnv())
1900                          ?: $this->get(static::$namespaces['special'] . 'content', false, $this->env);
1901
1902                 if (! $content) {
1903                     $content = new StdClass();
1904                     $content->scope = new StdClass();
1905                     $content->children = $this->storeEnv->parent->block->children;
1906                     break;
1907                 }
1908
1909                 $storeEnv = $this->storeEnv;
1910                 $this->storeEnv = $content->scope;
1911
1912                 $this->compileChildrenNoReturn($content->children, $out);
1913
1914                 $this->storeEnv = $storeEnv;
1915                 break;
1916
1917             case HTML_Scss_Type::T_DEBUG:
1918                 list(, $value) = $child;
1919
1920                 $line = $this->sourceLine;
1921                 $value = $this->compileValue($this->reduce($value, true));
1922                 fwrite($this->stderr, "Line $line DEBUG: $value\n");
1923                 break;
1924
1925             case HTML_Scss_Type::T_WARN:
1926                 list(, $value) = $child;
1927
1928                 $line = $this->sourceLine;
1929                 $value = $this->compileValue($this->reduce($value, true));
1930                 fwrite($this->stderr, "Line $line WARN: $value\n");
1931                 break;
1932
1933             case HTML_Scss_Type::T_ERROR:
1934                 list(, $value) = $child;
1935
1936                 $line = $this->sourceLine;
1937                 $value = $this->compileValue($this->reduce($value, true));
1938                 $this->throwError("Line $line ERROR: $value\n");
1939                 break;
1940
1941             case HTML_Scss_Type::T_CONTROL:
1942                 $this->throwError('@break/@continue not permitted in this scope');
1943                 break;
1944
1945             default:
1946                 $this->throwError("unknown child type: $child[0]");
1947         }
1948     }
1949
1950     /**
1951      * Reduce expression to string
1952      *
1953      * @param array $exp
1954      *
1955      * @return array
1956      */
1957     protected function expToString($exp)
1958     {
1959         list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
1960
1961         $content = [$this->reduce($left)];
1962
1963         if ($whiteLeft) {
1964             $content[] = ' ';
1965         }
1966
1967         $content[] = $op;
1968
1969         if ($whiteRight) {
1970             $content[] = ' ';
1971         }
1972
1973         $content[] = $this->reduce($right);
1974
1975         return [HTML_Scss_Type::T_STRING, '', $content];
1976     }
1977
1978     /**
1979      * Is truthy?
1980      *
1981      * @param array $value
1982      *
1983      * @return array
1984      */
1985     protected function isTruthy($value)
1986     {
1987         return $value !== static::$false && $value !== static::$null;
1988     }
1989
1990     /**
1991      * Is the value a direct relationship combinator?
1992      *
1993      * @param string $value
1994      *
1995      * @return boolean
1996      */
1997     protected function isImmediateRelationshipCombinator($value)
1998     {
1999         return $value === '>' || $value === '+' || $value === '~';
2000     }
2001
2002     /**
2003      * Should $value cause its operand to eval
2004      *
2005      * @param array $value
2006      *
2007      * @return boolean
2008      */
2009     protected function shouldEval($value)
2010     {
2011         switch ($value[0]) {
2012             case HTML_Scss_Type::T_EXPRESSION:
2013                 if ($value[1] === '/') {
2014                     return $this->shouldEval($value[2], $value[3]);
2015                 }
2016
2017                 // fall-thru
2018             case HTML_Scss_Type::T_VARIABLE:
2019             case HTML_Scss_Type::T_FUNCTION_CALL:
2020                 return true;
2021         }
2022
2023         return false;
2024     }
2025
2026     /**
2027      * Reduce value
2028      *
2029      * @param array   $value
2030      * @param boolean $inExp
2031      *
2032      * @return array|\Leafo\ScssPhp\Node\Number
2033      */
2034     protected function reduce($value, $inExp = false)
2035     {
2036         list($type) = $value;
2037
2038         switch ($type) {
2039             case HTML_Scss_Type::T_EXPRESSION:
2040                 list(, $op, $left, $right, $inParens) = $value;
2041
2042                 $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
2043                 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
2044
2045                 $left = $this->reduce($left, true);
2046
2047                 if ($op !== 'and' && $op !== 'or') {
2048                     $right = $this->reduce($right, true);
2049                 }
2050
2051                 // special case: looks like css shorthand
2052                 if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2])
2053                     && (($right[0] !== HTML_Scss_Type::T_NUMBER && $right[2] != '')
2054                     || ($right[0] === HTML_Scss_Type::T_NUMBER && ! $right->unitless()))
2055                 ) {
2056                     return $this->expToString($value);
2057                 }
2058
2059                 $left = $this->coerceForExpression($left);
2060                 $right = $this->coerceForExpression($right);
2061
2062                 $ltype = $left[0];
2063                 $rtype = $right[0];
2064
2065                 $ucOpName = ucfirst($opName);
2066                 $ucLType  = ucfirst($ltype);
2067                 $ucRType  = ucfirst($rtype);
2068
2069                 // this tries:
2070                 // 1. op[op name][left type][right type]
2071                 // 2. op[left type][right type] (passing the op as first arg
2072                 // 3. op[op name]
2073                 $fn = "op${ucOpName}${ucLType}${ucRType}";
2074
2075                 if (is_callable([$this, $fn]) ||
2076                     (($fn = "op${ucLType}${ucRType}") &&
2077                         is_callable([$this, $fn]) &&
2078                         $passOp = true) ||
2079                     (($fn = "op${ucOpName}") &&
2080                         is_callable([$this, $fn]) &&
2081                         $genOp = true)
2082                 ) {
2083                     $coerceUnit = false;
2084
2085                     if (! isset($genOp) &&
2086                         $left[0] === HTML_Scss_Type::T_NUMBER && $right[0] === HTML_Scss_Type::T_NUMBER
2087                     ) {
2088                         $coerceUnit = true;
2089
2090                         switch ($opName) {
2091                             case 'mul':
2092                                 $targetUnit = $left[2];
2093
2094                                 foreach ($right[2] as $unit => $exp) {
2095                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
2096                                 }
2097                                 break;
2098
2099                             case 'div':
2100                                 $targetUnit = $left[2];
2101
2102                                 foreach ($right[2] as $unit => $exp) {
2103                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
2104                                 }
2105                                 break;
2106
2107                             case 'mod':
2108                                 $targetUnit = $left[2];
2109                                 break;
2110
2111                             default:
2112                                 $targetUnit = $left->unitless() ? $right[2] : $left[2];
2113                         }
2114
2115                         if (! $left->unitless() && ! $right->unitless()) {
2116                             $left = $left->normalize();
2117                             $right = $right->normalize();
2118                         }
2119                     }
2120
2121                     $shouldEval = $inParens || $inExp;
2122
2123                     if (isset($passOp)) {
2124                         $out = $this->$fn($op, $left, $right, $shouldEval);
2125                     } else {
2126                         $out = $this->$fn($left, $right, $shouldEval);
2127                     }
2128
2129                     if (isset($out)) {
2130                         if ($coerceUnit && $out[0] === HTML_Scss_Type::T_NUMBER) {
2131                             $out = $out->coerce($targetUnit);
2132                         }
2133
2134                         return $out;
2135                     }
2136                 }
2137
2138                 return $this->expToString($value);
2139
2140             case HTML_Scss_Type::T_UNARY:
2141                 list(, $op, $exp, $inParens) = $value;
2142
2143                 $inExp = $inExp || $this->shouldEval($exp);
2144                 $exp = $this->reduce($exp);
2145
2146                 if ($exp[0] === HTML_Scss_Type::T_NUMBER) {
2147  
2148
2149                     switch ($op) {
2150                         case '+':
2151                             return new HTML_Scss_Node_Number($exp[1], $exp[2]);
2152
2153                         case '-':
2154                             return new HTML_Scss_Node_Number(-$exp[1], $exp[2]);
2155                     }
2156                 }
2157
2158                 if ($op === 'not') {
2159                     if ($inExp || $inParens) {
2160                         if ($exp === static::$false || $exp === static::$null) {
2161                             return static::$true;
2162                         }
2163
2164                         return static::$false;
2165                     }
2166
2167                     $op = $op . ' ';
2168                 }
2169
2170                 return [HTML_Scss_Type::T_STRING, '', [$op, $exp]];
2171
2172             case HTML_Scss_Type::T_VARIABLE:
2173                 list(, $name) = $value;
2174
2175                 return $this->reduce($this->get($name));
2176
2177             case HTML_Scss_Type::T_LIST:
2178                 foreach ($value[2] as &$item) {
2179                     $item = $this->reduce($item);
2180                 }
2181
2182                 return $value;
2183
2184             case HTML_Scss_Type::T_MAP:
2185                 foreach ($value[1] as &$item) {
2186                     $item = $this->reduce($item);
2187                 }
2188
2189                 foreach ($value[2] as &$item) {
2190                     $item = $this->reduce($item);
2191                 }
2192
2193                 return $value;
2194
2195             case HTML_Scss_Type::T_STRING:
2196                 foreach ($value[2] as &$item) {
2197                     if (is_array($item) || $item instanceof ArrayAccess) {
2198                         $item = $this->reduce($item);
2199                     }
2200                 }
2201                 return $value;
2202
2203             case HTML_Scss_Type::T_INTERPOLATE:
2204                 $value[1] = $this->reduce($value[1]);
2205
2206                 return $value;
2207
2208             case HTML_Scss_Type::T_FUNCTION_CALL:
2209                 list(, $name, $argValues) = $value;
2210
2211                 return $this->fncall($name, $argValues);
2212
2213             default:
2214                 return $value;
2215         }
2216     }
2217
2218     /**
2219      * Function caller
2220      *
2221      * @param string $name
2222      * @param array  $argValues
2223      *
2224      * @return array|null
2225      */
2226     private function fncall($name, $argValues)
2227     {
2228         // SCSS @function
2229         if ($this->callScssFunction($name, $argValues, $returnValue)) {
2230             return $returnValue;
2231         }
2232
2233         // native PHP functions
2234         if ($this->callNativeFunction($name, $argValues, $returnValue)) {
2235             return $returnValue;
2236         }
2237
2238         // for CSS functions, simply flatten the arguments into a list
2239         $listArgs = [];
2240
2241         foreach ((array) $argValues as $arg) {
2242             if (empty($arg[0])) {
2243                 $listArgs[] = $this->reduce($arg[1]);
2244             }
2245         }
2246
2247         return [HTML_Scss_Type::T_FUNCTION, $name, [HTML_Scss_Type::T_LIST, ',', $listArgs]];
2248     }
2249
2250     /**
2251      * Normalize name
2252      *
2253      * @param string $name
2254      *
2255      * @return string
2256      */
2257     protected function normalizeName($name)
2258     {
2259         return str_replace('-', '_', $name);
2260     }
2261
2262     /**
2263      * Normalize value
2264      *
2265      * @param array $value
2266      *
2267      * @return array
2268      */
2269     public function normalizeValue($value)
2270     {
2271         $value = $this->coerceForExpression($this->reduce($value));
2272         list($type) = $value;
2273
2274         switch ($type) {
2275             case HTML_Scss_Type::T_LIST:
2276                 $value = $this->extractInterpolation($value);
2277
2278                 if ($value[0] !== HTML_Scss_Type::T_LIST) {
2279                     return [HTML_Scss_Type::T_KEYWORD, $this->compileValue($value)];
2280                 }
2281
2282                 foreach ($value[2] as $key => $item) {
2283                     $value[2][$key] = $this->normalizeValue($item);
2284                 }
2285
2286                 return $value;
2287
2288             case HTML_Scss_Type::T_STRING:
2289                 return [$type, '"', [$this->compileStringContent($value)]];
2290
2291             case HTML_Scss_Type::T_NUMBER:
2292                 return $value->normalize();
2293
2294             case HTML_Scss_Type::T_INTERPOLATE:
2295                 return [HTML_Scss_Type::T_KEYWORD, $this->compileValue($value)];
2296
2297             default:
2298                 return $value;
2299         }
2300     }
2301
2302     /**
2303      * Add numbers
2304      *
2305      * @param array $left
2306      * @param array $right
2307      *
2308      * @return \Leafo\ScssPhp\Node\Number
2309      */
2310     protected function opAddNumberNumber($left, $right)
2311     {
2312         
2313         return new HTML_Scss_Node_Number($left[1] + $right[1], $left[2]);
2314     }
2315
2316     /**
2317      * Multiply numbers
2318      *
2319      * @param array $left
2320      * @param array $right
2321      *
2322      * @return \Leafo\ScssPhp\Node\Number
2323      */
2324     protected function opMulNumberNumber($left, $right)
2325     {
2326         return new HTML_Scss_Node_Number($left[1] * $right[1], $left[2]);
2327     }
2328
2329     /**
2330      * Subtract numbers
2331      *
2332      * @param array $left
2333      * @param array $right
2334      *
2335      * @return \Leafo\ScssPhp\Node\Number
2336      */
2337     protected function opSubNumberNumber($left, $right)
2338     {
2339         return new HTML_Scss_Node_Number($left[1] - $right[1], $left[2]);
2340     }
2341
2342     /**
2343      * Divide numbers
2344      *
2345      * @param array $left
2346      * @param array $right
2347      *
2348      * @return array|\Leafo\ScssPhp\Node\Number
2349      */
2350     protected function opDivNumberNumber($left, $right)
2351     {
2352         if ($right[1] == 0) {
2353             return [HTML_Scss_Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]];
2354         }
2355
2356         return new HTML_Scss_Node_Number($left[1] / $right[1], $left[2]);
2357     }
2358
2359     /**
2360      * Mod numbers
2361      *
2362      * @param array $left
2363      * @param array $right
2364      *
2365      * @return \Leafo\ScssPhp\Node\Number
2366      */
2367     protected function opModNumberNumber($left, $right)
2368     {
2369         return new HTML_Scss_Node_Number($left[1] % $right[1], $left[2]);
2370     }
2371
2372     /**
2373      * Add strings
2374      *
2375      * @param array $left
2376      * @param array $right
2377      *
2378      * @return array
2379      */
2380     protected function opAdd($left, $right)
2381     {
2382         if ($strLeft = $this->coerceString($left)) {
2383             if ($right[0] === HTML_Scss_Type::T_STRING) {
2384                 $right[1] = '';
2385             }
2386
2387             $strLeft[2][] = $right;
2388
2389             return $strLeft;
2390         }
2391
2392         if ($strRight = $this->coerceString($right)) {
2393             if ($left[0] === HTML_Scss_Type::T_STRING) {
2394                 $left[1] = '';
2395             }
2396
2397             array_unshift($strRight[2], $left);
2398
2399             return $strRight;
2400         }
2401     }
2402
2403     /**
2404      * Boolean and
2405      *
2406      * @param array   $left
2407      * @param array   $right
2408      * @param boolean $shouldEval
2409      *
2410      * @return array
2411      */
2412     protected function opAnd($left, $right, $shouldEval)
2413     {
2414         if (! $shouldEval) {
2415             return;
2416         }
2417
2418         if ($left !== static::$false and $left !== static::$null) {
2419             return $this->reduce($right, true);
2420         }
2421
2422         return $left;
2423     }
2424
2425     /**
2426      * Boolean or
2427      *
2428      * @param array   $left
2429      * @param array   $right
2430      * @param boolean $shouldEval
2431      *
2432      * @return array
2433      */
2434     protected function opOr($left, $right, $shouldEval)
2435     {
2436         if (! $shouldEval) {
2437             return;
2438         }
2439
2440         if ($left !== static::$false and $left !== static::$null) {
2441             return $left;
2442         }
2443
2444         return $this->reduce($right, true);
2445     }
2446
2447     /**
2448      * Compare colors
2449      *
2450      * @param string $op
2451      * @param array  $left
2452      * @param array  $right
2453      *
2454      * @return array
2455      */
2456     protected function opColorColor($op, $left, $right)
2457     {
2458         $out = [HTML_Scss_Type::T_COLOR];
2459
2460         foreach ([1, 2, 3] as $i) {
2461             $lval = isset($left[$i]) ? $left[$i] : 0;
2462             $rval = isset($right[$i]) ? $right[$i] : 0;
2463
2464             switch ($op) {
2465                 case '+':
2466                     $out[] = $lval + $rval;
2467                     break;
2468
2469                 case '-':
2470                     $out[] = $lval - $rval;
2471                     break;
2472
2473                 case '*':
2474                     $out[] = $lval * $rval;
2475                     break;
2476
2477                 case '%':
2478                     $out[] = $lval % $rval;
2479                     break;
2480
2481                 case '/':
2482                     if ($rval == 0) {
2483                         $this->throwError("color: Can't divide by zero");
2484                         break 2;
2485                     }
2486
2487                     $out[] = (int) ($lval / $rval);
2488                     break;
2489
2490                 case '==':
2491                     return $this->opEq($left, $right);
2492
2493                 case '!=':
2494                     return $this->opNeq($left, $right);
2495
2496                 default:
2497                     $this->throwError("color: unknown op $op");
2498                     break 2;
2499             }
2500         }
2501
2502         if (isset($left[4])) {
2503             $out[4] = $left[4];
2504         } elseif (isset($right[4])) {
2505             $out[4] = $right[4];
2506         }
2507
2508         return $this->fixColor($out);
2509     }
2510
2511     /**
2512      * Compare color and number
2513      *
2514      * @param string $op
2515      * @param array  $left
2516      * @param array  $right
2517      *
2518      * @return array
2519      */
2520     protected function opColorNumber($op, $left, $right)
2521     {
2522         $value = $right[1];
2523
2524         return $this->opColorColor(
2525             $op,
2526             $left,
2527             [HTML_Scss_Type::T_COLOR, $value, $value, $value]
2528         );
2529     }
2530
2531     /**
2532      * Compare number and color
2533      *
2534      * @param string $op
2535      * @param array  $left
2536      * @param array  $right
2537      *
2538      * @return array
2539      */
2540     protected function opNumberColor($op, $left, $right)
2541     {
2542         $value = $left[1];
2543
2544         return $this->opColorColor(
2545             $op,
2546             [HTML_Scss_Type::T_COLOR, $value, $value, $value],
2547             $right
2548         );
2549     }
2550
2551     /**
2552      * Compare number1 == number2
2553      *
2554      * @param array $left
2555      * @param array $right
2556      *
2557      * @return array
2558      */
2559     protected function opEq($left, $right)
2560     {
2561         if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2562             $lStr[1] = '';
2563             $rStr[1] = '';
2564
2565             $left = $this->compileValue($lStr);
2566             $right = $this->compileValue($rStr);
2567         }
2568
2569         return $this->toBool($left === $right);
2570     }
2571
2572     /**
2573      * Compare number1 != number2
2574      *
2575      * @param array $left
2576      * @param array $right
2577      *
2578      * @return array
2579      */
2580     protected function opNeq($left, $right)
2581     {
2582         if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
2583             $lStr[1] = '';
2584             $rStr[1] = '';
2585
2586             $left = $this->compileValue($lStr);
2587             $right = $this->compileValue($rStr);
2588         }
2589
2590         return $this->toBool($left !== $right);
2591     }
2592
2593     /**
2594      * Compare number1 >= number2
2595      *
2596      * @param array $left
2597      * @param array $right
2598      *
2599      * @return array
2600      */
2601     protected function opGteNumberNumber($left, $right)
2602     {
2603         return $this->toBool($left[1] >= $right[1]);
2604     }
2605
2606     /**
2607      * Compare number1 > number2
2608      *
2609      * @param array $left
2610      * @param array $right
2611      *
2612      * @return array
2613      */
2614     protected function opGtNumberNumber($left, $right)
2615     {
2616         return $this->toBool($left[1] > $right[1]);
2617     }
2618
2619     /**
2620      * Compare number1 <= number2
2621      *
2622      * @param array $left
2623      * @param array $right
2624      *
2625      * @return array
2626      */
2627     protected function opLteNumberNumber($left, $right)
2628     {
2629         return $this->toBool($left[1] <= $right[1]);
2630     }
2631
2632     /**
2633      * Compare number1 < number2
2634      *
2635      * @param array $left
2636      * @param array $right
2637      *
2638      * @return array
2639      */
2640     protected function opLtNumberNumber($left, $right)
2641     {
2642         return $this->toBool($left[1] < $right[1]);
2643     }
2644
2645     /**
2646      * Three-way comparison, aka spaceship operator
2647      *
2648      * @param array $left
2649      * @param array $right
2650      *
2651      * @return \Leafo\ScssPhp\Node\Number
2652      */
2653     protected function opCmpNumberNumber($left, $right)
2654     {
2655         $n = $left[1] - $right[1];
2656
2657         return new HTML_Scss_Node_Number($n ? $n / abs($n) : 0, '');
2658     }
2659
2660     /**
2661      * Cast to boolean
2662      *
2663      * @api
2664      *
2665      * @param mixed $thing
2666      *
2667      * @return array
2668      */
2669     public function toBool($thing)
2670     {
2671         return $thing ? static::$true : static::$false;
2672     }
2673
2674     /**
2675      * Compiles a primitive value into a CSS property value.
2676      *
2677      * Values in scssphp are typed by being wrapped in arrays, their format is
2678      * typically:
2679      *
2680      *     array(type, contents [, additional_contents]*)
2681      *
2682      * The input is expected to be reduced. This function will not work on
2683      * things like expressions and variables.
2684      *
2685      * @api
2686      *
2687      * @param array $value
2688      *
2689      * @return string
2690      */
2691     public function compileValue($value)
2692     {
2693         //echo "compileValue: "; print_r($value);
2694         
2695         $value = $this->reduce($value);
2696
2697         list($type) = $value;
2698
2699         switch ($type) {
2700             case HTML_Scss_Type::T_KEYWORD:
2701                 return $value[1];
2702
2703             case HTML_Scss_Type::T_COLOR:
2704                 // [1] - red component (either number for a %)
2705                 // [2] - green component
2706                 // [3] - blue component
2707                 // [4] - optional alpha component
2708                 list(, $r, $g, $b) = $value;
2709
2710                 $r = round($r);
2711                 $g = round($g);
2712                 $b = round($b);
2713
2714                 if (count($value) === 5 && $value[4] !== 1) { // rgba
2715                     $a = new HTML_Scss_Node_Number($value[4], '');
2716
2717                     return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
2718                 }
2719
2720                 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
2721
2722                 // Converting hex color to short notation (e.g. #003399 to #039)
2723                 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
2724                     $h = '#' . $h[1] . $h[3] . $h[5];
2725                 }
2726
2727                 return $h;
2728
2729             case HTML_Scss_Type::T_NUMBER:
2730                 return $value->output($this);
2731
2732             case HTML_Scss_Type::T_STRING:
2733                 return $value[1] . $this->compileStringContent($value) . $value[1];
2734
2735             case HTML_Scss_Type::T_FUNCTION:
2736                 $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
2737
2738                 return "$value[1]($args)";
2739
2740             case HTML_Scss_Type::T_LIST:
2741                 $value = $this->extractInterpolation($value);
2742
2743                 if ($value[0] !== HTML_Scss_Type::T_LIST) {
2744                     return $this->compileValue($value);
2745                 }
2746
2747                 list(, $delim, $items) = $value;
2748
2749                 if ($delim !== ' ') {
2750                     $delim .= ' ';
2751                 }
2752
2753                 $filtered = [];
2754
2755                 foreach ($items as $item) {
2756                     if ($item[0] === HTML_Scss_Type::T_NULL) {
2757                         continue;
2758                     }
2759
2760                     $filtered[] = $this->compileValue($item);
2761                 }
2762
2763                 return implode("$delim", $filtered);
2764
2765             case HTML_Scss_Type::T_MAP:
2766                 $keys = $value[1];
2767                 $values = $value[2];
2768                 $filtered = [];
2769
2770                 for ($i = 0, $s = count($keys); $i < $s; $i++) {
2771                     $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
2772                 }
2773
2774                 array_walk($filtered, function (&$value, $key) {
2775                     $value = $key . ': ' . $value;
2776                 });
2777
2778                 return '(' . implode(', ', $filtered) . ')';
2779
2780             case HTML_Scss_Type::T_INTERPOLATED:
2781                 // node created by extractInterpolation
2782                 list(, $interpolate, $left, $right) = $value;
2783                 list(,, $whiteLeft, $whiteRight) = $interpolate;
2784
2785                 $left = count($left[2]) > 0 ?
2786                     $this->compileValue($left) . $whiteLeft : '';
2787
2788                 $right = count($right[2]) > 0 ?
2789                     $whiteRight . $this->compileValue($right) : '';
2790
2791                 return $left . $this->compileValue($interpolate) . $right;
2792
2793             case HTML_Scss_Type::T_INTERPOLATE:
2794                 // raw parse node
2795                 list(, $exp) = $value;
2796
2797                 // strip quotes if it's a string
2798                 $reduced = $this->reduce($exp);
2799
2800                 switch ($reduced[0]) {
2801                     case HTML_Scss_Type::T_LIST:
2802                         $reduced = $this->extractInterpolation($reduced);
2803
2804                         if ($reduced[0] !== HTML_Scss_Type::T_LIST) {
2805                             break;
2806                         }
2807
2808                         list(, $delim, $items) = $reduced;
2809
2810                         if ($delim !== ' ') {
2811                             $delim .= ' ';
2812                         }
2813
2814                         $filtered = [];
2815
2816                         foreach ($items as $item) {
2817                             if ($item[0] === HTML_Scss_Type::T_NULL) {
2818                                 continue;
2819                             }
2820
2821                             $temp = $this->compileValue([HTML_Scss_Type::T_KEYWORD, $item]);
2822                             if ($temp[0] === HTML_Scss_Type::T_STRING) {
2823                                 $filtered[] = $this->compileStringContent($temp);
2824                             } elseif ($temp[0] === HTML_Scss_Type::T_KEYWORD) {
2825                                 $filtered[] = $temp[1];
2826                             } else {
2827                                 $filtered[] = $this->compileValue($temp);
2828                             }
2829                         }
2830
2831                         $reduced = [HTML_Scss_Type::T_KEYWORD, implode("$delim", $filtered)];
2832                         break;
2833
2834                     case HTML_Scss_Type::T_STRING:
2835                         $reduced = [HTML_Scss_Type::T_KEYWORD, $this->compileStringContent($reduced)];
2836                         break;
2837
2838                     case HTML_Scss_Type::T_NULL:
2839                         $reduced = [HTML_Scss_Type::T_KEYWORD, ''];
2840                 }
2841
2842                 return $this->compileValue($reduced);
2843
2844             case HTML_Scss_Type::T_NULL:
2845                 return 'null';
2846
2847             default:
2848                 $this->throwError("unknown value type: $type");
2849         }
2850     }
2851
2852     /**
2853      * Flatten list
2854      *
2855      * @param array $list
2856      *
2857      * @return string
2858      */
2859     protected function flattenList($list)
2860     {
2861         return $this->compileValue($list);
2862     }
2863
2864     /**
2865      * Compile string content
2866      *
2867      * @param array $string
2868      *
2869      * @return string
2870      */
2871     protected function compileStringContent($string)
2872     {
2873         $parts = [];
2874         //echo "compileStringContent: "; print_r($string);
2875         foreach ($string[2] as $part) {
2876             if (is_array($part) || $part instanceof  ArrayAccess) {
2877                 $parts[] = $this->compileValue($part);
2878             } else {
2879                 $parts[] = $part;
2880             }
2881         }
2882
2883         return implode($parts);
2884     }
2885
2886     /**
2887      * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
2888      *
2889      * @param array $list
2890      *
2891      * @return array
2892      */
2893     protected function extractInterpolation($list)
2894     {
2895         $items = $list[2];
2896
2897         foreach ($items as $i => $item) {
2898             if ($item[0] === HTML_Scss_Type::T_INTERPOLATE) {
2899                 $before = [HTML_Scss_Type::T_LIST, $list[1], array_slice($items, 0, $i)];
2900                 $after  = [HTML_Scss_Type::T_LIST, $list[1], array_slice($items, $i + 1)];
2901
2902                 return [HTML_Scss_Type::T_INTERPOLATED, $item, $before, $after];
2903             }
2904         }
2905
2906         return $list;
2907     }
2908
2909     /**
2910      * Find the final set of selectors
2911      *
2912      * @param \Leafo\ScssPhp\Compiler\Environment $env
2913      *
2914      * @return array
2915      */
2916     protected function multiplySelectors(HTML_Scss_Compiler_Environment $env)
2917     {
2918         $envs            = $this->compactEnv($env);
2919         $selectors       = [];
2920         $parentSelectors = [[]];
2921
2922         while ($env = array_pop($envs)) {
2923             if (empty($env->selectors)) {
2924                 continue;
2925             }
2926
2927             $selectors = [];
2928
2929             foreach ($env->selectors as $selector) {
2930                 foreach ($parentSelectors as $parent) {
2931                     $selectors[] = $this->joinSelectors($parent, $selector);
2932                 }
2933             }
2934
2935             $parentSelectors = $selectors;
2936         }
2937
2938         return $selectors;
2939     }
2940
2941     /**
2942      * Join selectors; looks for & to replace, or append parent before child
2943      *
2944      * @param array $parent
2945      * @param array $child
2946      *
2947      * @return array
2948      */
2949     protected function joinSelectors($parent, $child)
2950     {
2951         $setSelf = false;
2952         $out = [];
2953
2954         foreach ($child as $part) {
2955             $newPart = [];
2956
2957             foreach ($part as $p) {
2958                 if ($p === static::$selfSelector) {
2959                     $setSelf = true;
2960
2961                     foreach ($parent as $i => $parentPart) {
2962                         if ($i > 0) {
2963                             $out[] = $newPart;
2964                             $newPart = [];
2965                         }
2966
2967                         foreach ($parentPart as $pp) {
2968                             $newPart[] = $pp;
2969                         }
2970                     }
2971                 } else {
2972                     $newPart[] = $p;
2973                 }
2974             }
2975
2976             $out[] = $newPart;
2977         }
2978
2979         return $setSelf ? $out : array_merge($parent, $child);
2980     }
2981
2982     /**
2983      * Multiply media
2984      *
2985      * @param \Leafo\ScssPhp\Compiler\Environment $env
2986      * @param array                               $childQueries
2987      *
2988      * @return array
2989      */
2990     protected function multiplyMedia(HTML_Scss_Compiler_Environment $env = null, $childQueries = null)
2991     {
2992         if (! isset($env) ||
2993             ! empty($env->block->type) && $env->block->type !== HTML_Scss_Type::T_MEDIA
2994         ) {
2995             return $childQueries;
2996         }
2997
2998         // plain old block, skip
2999         if (empty($env->block->type)) {
3000             return $this->multiplyMedia($env->parent, $childQueries);
3001         }
3002
3003         $parentQueries = isset($env->block->queryList)
3004             ? $env->block->queryList
3005             : [[[HTML_Scss_Type::T_MEDIA_VALUE, $env->block->value]]];
3006
3007         if ($childQueries === null) {
3008             $childQueries = $parentQueries;
3009         } else {
3010             $originalQueries = $childQueries;
3011             $childQueries = [];
3012
3013             foreach ($parentQueries as $parentQuery) {
3014                 foreach ($originalQueries as $childQuery) {
3015                     $childQueries []= array_merge($parentQuery, $childQuery);
3016                 }
3017             }
3018         }
3019
3020         return $this->multiplyMedia($env->parent, $childQueries);
3021     }
3022
3023     /**
3024      * Convert env linked list to stack
3025      *
3026      * @param \Leafo\ScssPhp\Compiler\Environment $env
3027      *
3028      * @return array
3029      */
3030     private function compactEnv(HTML_Scss_Compiler_Environment $env)
3031     {
3032         for ($envs = []; $env; $env = $env->parent) {
3033             $envs[] = $env;
3034         }
3035
3036         return $envs;
3037     }
3038
3039     /**
3040      * Convert env stack to singly linked list
3041      *
3042      * @param array $envs
3043      *
3044      * @return \Leafo\ScssPhp\Compiler\Environment
3045      */
3046     private function extractEnv($envs)
3047     {
3048         for ($env = null; $e = array_pop($envs);) {
3049             $e->parent = $env;
3050             $env = $e;
3051         }
3052
3053         return $env;
3054     }
3055
3056     /**
3057      * Push environment
3058      *
3059      * @param \Leafo\ScssPhp\Block $block
3060      *
3061      * @return \Leafo\ScssPhp\Compiler\Environment
3062      */
3063     protected function pushEnv(HTML_Scss_Block $block = null)
3064     {
3065         $env = new HTML_Scss_Compiler_Environment;
3066         $env->parent = $this->env;
3067         $env->store  = [];
3068         $env->block  = $block;
3069         $env->depth  = isset($this->env->depth) ? $this->env->depth + 1 : 0;
3070
3071         $this->env = $env;
3072
3073         return $env;
3074     }
3075
3076     /**
3077      * Pop environment
3078      */
3079     protected function popEnv()
3080     {
3081         $this->env = $this->env->parent;
3082     }
3083
3084     /**
3085      * Get store environment
3086      *
3087      * @return \Leafo\ScssPhp\Compiler\Environment
3088      */
3089     protected function getStoreEnv()
3090     {
3091         return isset($this->storeEnv) ? $this->storeEnv : $this->env;
3092     }
3093
3094     /**
3095      * Set variable
3096      *
3097      * @param string                              $name
3098      * @param mixed                               $value
3099      * @param boolean                             $shadow
3100      * @param \Leafo\ScssPhp\Compiler\Environment $env
3101      */
3102     protected function set($name, $value, $shadow = false, HTML_Scss_Compiler_Environment  $env = null)
3103     {
3104         $name = $this->normalizeName($name);
3105
3106         if (! isset($env)) {
3107             $env = $this->getStoreEnv();
3108         }
3109
3110         if ($shadow) {
3111             $this->setRaw($name, $value, $env);
3112         } else {
3113             $this->setExisting($name, $value, $env);
3114         }
3115     }
3116
3117     /**
3118      * Set existing variable
3119      *
3120      * @param string                              $name
3121      * @param mixed                               $value
3122      * @param \Leafo\ScssPhp\Compiler\Environment $env
3123      */
3124     protected function setExisting($name, $value, HTML_Scss_Compiler_Environment $env)
3125     {
3126         $storeEnv = $env;
3127
3128         $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
3129
3130         for (;;) {
3131             if (array_key_exists($name, $env->store)) {
3132                 break;
3133             }
3134
3135             if (! $hasNamespace && isset($env->marker)) {
3136                 $env = $storeEnv;
3137                 break;
3138             }
3139
3140             if (! isset($env->parent)) {
3141                 $env = $storeEnv;
3142                 break;
3143             }
3144
3145             $env = $env->parent;
3146         }
3147
3148         $env->store[$name] = $value;
3149     }
3150
3151     /**
3152      * Set raw variable
3153      *
3154      * @param string                              $name
3155      * @param mixed                               $value
3156      * @param \Leafo\ScssPhp\Compiler\Environment $env
3157      */
3158     protected function setRaw($name, $value, HTML_Scss_Compiler_Environment $env)
3159     {
3160         $env->store[$name] = $value;
3161     }
3162
3163     /**
3164      * Get variable
3165      *
3166      * @api
3167      *
3168      * @param string                              $name
3169      * @param boolean                             $shouldThrow
3170      * @param \Leafo\ScssPhp\Compiler\Environment $env
3171      *
3172      * @return mixed
3173      */
3174     public function get($name, $shouldThrow = true, HTML_Scss_Compiler_Environment $env = null)
3175     {
3176         $normalizedName = $this->normalizeName($name);
3177         $specialContentKey = static::$namespaces['special'] . 'content';
3178
3179         if (! isset($env)) {
3180             $env = $this->getStoreEnv();
3181         }
3182
3183         $nextIsRoot = false;
3184         $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
3185
3186         for (;;) {
3187             if (array_key_exists($normalizedName, $env->store)) {
3188                 return $env->store[$normalizedName];
3189             }
3190
3191             if (! $hasNamespace && isset($env->marker)) {
3192                 if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
3193                     $env = $env->store[$specialContentKey]->scope;
3194                     $nextIsRoot = true;
3195                     continue;
3196                 }
3197
3198                 $env = $this->rootEnv;
3199                 continue;
3200             }
3201
3202             if (! isset($env->parent)) {
3203                 break;
3204             }
3205
3206             $env = $env->parent;
3207         }
3208
3209         if ($shouldThrow) {
3210             $this->throwError("Undefined variable \$$name");
3211         }
3212
3213         // found nothing
3214     }
3215
3216     /**
3217      * Has variable?
3218      *
3219      * @param string                              $name
3220      * @param \Leafo\ScssPhp\Compiler\Environment $env
3221      *
3222      * @return boolean
3223      */
3224     protected function has($name, HTML_Scss_Compiler_Environment $env = null)
3225     {
3226         return $this->get($name, false, $env) !== null;
3227     }
3228
3229     /**
3230      * Inject variables
3231      *
3232      * @param array $args
3233      */
3234     protected function injectVariables(array $args)
3235     {
3236         if (empty($args)) {
3237             return;
3238         }
3239
3240         $parser = $this->parserFactory(__METHOD__);
3241
3242         foreach ($args as $name => $strValue) {
3243             if ($name[0] === '$') {
3244                 $name = substr($name, 1);
3245             }
3246
3247             if (! $parser->parseValue($strValue, $value)) {
3248                 $value = $this->coerceValue($strValue);
3249             }
3250
3251             $this->set($name, $value);
3252         }
3253     }
3254
3255     /**
3256      * Set variables
3257      *
3258      * @api
3259      *
3260      * @param array $variables
3261      */
3262     public function setVariables(array $variables)
3263     {
3264         $this->registeredVars = array_merge($this->registeredVars, $variables);
3265     }
3266
3267     /**
3268      * Unset variable
3269      *
3270      * @api
3271      *
3272      * @param string $name
3273      */
3274     public function unsetVariable($name)
3275     {
3276         unset($this->registeredVars[$name]);
3277     }
3278
3279     /**
3280      * Returns list of variables
3281      *
3282      * @api
3283      *
3284      * @return array
3285      */
3286     public function getVariables()
3287     {
3288         return $this->registeredVars;
3289     }
3290
3291     /**
3292      * Adds to list of parsed files
3293      *
3294      * @api
3295      *
3296      * @param string $path
3297      */
3298     public function addParsedFile($path)
3299     {
3300         if (isset($path) && file_exists($path)) {
3301             $this->parsedFiles[realpath($path)] = filemtime($path);
3302         }
3303     }
3304
3305     /**
3306      * Returns list of parsed files
3307      *
3308      * @api
3309      *
3310      * @return array
3311      */
3312     public function getParsedFiles()
3313     {
3314         return $this->parsedFiles;
3315     }
3316
3317     /**
3318      * Add import path
3319      *
3320      * @api
3321      *
3322      * @param string|callable $path
3323      */
3324     public function addImportPath($path)
3325     {
3326         if (! in_array($path, $this->importPaths)) {
3327             $this->importPaths[] = $path;
3328         }
3329     }
3330
3331     /**
3332      * Set import paths
3333      *
3334      * @api
3335      *
3336      * @param string|array $path
3337      */
3338     public function setImportPaths($path)
3339     {
3340         $this->importPaths = (array) $path;
3341     }
3342
3343     /**
3344      * Set number precision
3345      *
3346      * @api
3347      *
3348      * @param integer $numberPrecision
3349      */
3350     public function setNumberPrecision($numberPrecision)
3351     {
3352         HTML_Scss_Node_Number::$precision = $numberPrecision;
3353     }
3354
3355     /**
3356      * Set formatter
3357      *
3358      * @api
3359      *
3360      * @param string $formatterName
3361      */
3362     public function setFormatter($formatterName)
3363     {
3364         $this->formatter = $formatterName;
3365     }
3366
3367     /**
3368      * Set line number style
3369      *
3370      * @api
3371      *
3372      * @param string $lineNumberStyle
3373      */
3374     public function setLineNumberStyle($lineNumberStyle)
3375     {
3376         $this->lineNumberStyle = $lineNumberStyle;
3377     }
3378
3379     /**
3380      * Enable/disable source maps
3381      *
3382      * @api
3383      *
3384      * @param integer $sourceMap
3385      */
3386     public function setSourceMap($sourceMap)
3387     {
3388         $this->sourceMap = $sourceMap;
3389     }
3390
3391     /**
3392      * Set source map options
3393      *
3394      * @api
3395      *
3396      * @param array $sourceMapOptions
3397      */
3398     public function setSourceMapOptions($sourceMapOptions)
3399     {
3400         $this->sourceMapOptions = $sourceMapOptions;
3401     }
3402
3403     /**
3404      * Register function
3405      *
3406      * @api
3407      *
3408      * @param string   $name
3409      * @param callable $func
3410      * @param array    $prototype
3411      */
3412     public function registerFunction($name, $func, $prototype = null)
3413     {
3414         $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
3415     }
3416
3417     /**
3418      * Unregister function
3419      *
3420      * @api
3421      *
3422      * @param string $name
3423      */
3424     public function unregisterFunction($name)
3425     {
3426         unset($this->userFunctions[$this->normalizeName($name)]);
3427     }
3428
3429     /**
3430      * Add feature
3431      *
3432      * @api
3433      *
3434      * @param string $name
3435      */
3436     public function addFeature($name)
3437     {
3438         $this->registeredFeatures[$name] = true;
3439     }
3440
3441     /**
3442      * Import file
3443      *
3444      * @param string $path
3445      * @param array  $out
3446      */
3447     protected function importFile($path, $out)
3448     {
3449         //echo "Import file: $path\n";
3450         // see if tree is cached
3451         $realPath = realpath($path);
3452
3453         if (isset($this->importCache[$realPath])) {
3454             $this->handleImportLoop($realPath);
3455
3456             $tree = $this->importCache[$realPath];
3457         } else {
3458             $code   = file_get_contents($path);
3459             $parser = $this->parserFactory($path);
3460             $tree   = $parser->parse($code);
3461
3462             $this->importCache[$realPath] = $tree;
3463         }
3464
3465         $pi = pathinfo($path);
3466         array_unshift($this->importPaths, $pi['dirname']);
3467         $this->compileChildrenNoReturn($tree->children, $out);
3468         array_shift($this->importPaths);
3469     }
3470
3471     /**
3472      * Return the file path for an import url if it exists
3473      *
3474      * @api
3475      *
3476      * @param string $url
3477      *
3478      * @return string|null
3479      */
3480     public function findImport($url)
3481     {
3482         $urls = [];
3483
3484         // for "normal" scss imports (ignore vanilla css and external requests)
3485         if (! preg_match('/\.css$|^https?:\/\//', $url)) {
3486             // try both normal and the _partial filename
3487             $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
3488         }
3489
3490         $hasExtension = preg_match('/[.]s?css$/', $url);
3491
3492         foreach ($this->importPaths as $dir) {
3493             if (is_string($dir)) {
3494                 // check urls for normal import paths
3495                 foreach ($urls as $full) {
3496                     $full = $dir
3497                         . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
3498                         . $full;
3499
3500                     if ($this->fileExists($file = $full . '.scss') ||
3501                         ($hasExtension && $this->fileExists($file = $full))
3502                     ) {
3503                         return $file;
3504                     }
3505                 }
3506             } elseif (is_callable($dir)) {
3507                 // check custom callback for import path
3508                 $file = call_user_func($dir, $url);
3509
3510                 if ($file !== null) {
3511                     return $file;
3512                 }
3513             }
3514         }
3515
3516         return null;
3517     }
3518
3519     /**
3520      * Set encoding
3521      *
3522      * @api
3523      *
3524      * @param string $encoding
3525      */
3526     public function setEncoding($encoding)
3527     {
3528         $this->encoding = $encoding;
3529     }
3530
3531     /**
3532      * Ignore errors?
3533      *
3534      * @api
3535      *
3536      * @param boolean $ignoreErrors
3537      *
3538      * @return \Leafo\ScssPhp\Compiler
3539      */
3540     public function setIgnoreErrors($ignoreErrors)
3541     {
3542         $this->ignoreErrors = $ignoreErrors;
3543     }
3544
3545     /**
3546      * Throw error (exception)
3547      *
3548      * @api
3549      *
3550      * @param string $msg Message with optional sprintf()-style vararg parameters
3551      *
3552      * @throws \Leafo\ScssPhp\Exception\CompilerException
3553      */
3554     public function throwError($msg)
3555     {
3556         if ($this->ignoreErrors) {
3557             return;
3558         }
3559
3560         if (func_num_args() > 1) {
3561             $msg = call_user_func_array('sprintf', func_get_args());
3562         }
3563
3564         $line = $this->sourceLine;
3565         $msg = "$msg: line: $line";
3566         require_once 'Scss/Exception/CompilerException.php';
3567         throw new HTML_Scss_Exception_CompilerException($msg);
3568     }
3569
3570     /**
3571      * Handle import loop
3572      *
3573      * @param string $name
3574      *
3575      * @throws \Exception
3576      */
3577     protected function handleImportLoop($name)
3578     {
3579         for ($env = $this->env; $env; $env = $env->parent) {
3580             $file = $this->sourceNames[$env->block->sourceIndex];
3581
3582             if (realpath($file) === $name) {
3583                 $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
3584                 break;
3585             }
3586         }
3587     }
3588
3589     /**
3590      * Does file exist?
3591      *
3592      * @param string $name
3593      *
3594      * @return boolean
3595      */
3596     protected function fileExists($name)
3597     {
3598         return file_exists($name) && is_file($name);
3599     }
3600
3601     /**
3602      * Call SCSS @function
3603      *
3604      * @param string $name
3605      * @param array  $argValues
3606      * @param array  $returnValue
3607      *
3608      * @return boolean Returns true if returnValue is set; otherwise, false
3609      */
3610     protected function callScssFunction($name, $argValues, &$returnValue)
3611     {
3612         $func = $this->get(static::$namespaces['function'] . $name, false);
3613
3614         if (! $func) {
3615             return false;
3616         }
3617
3618         $this->pushEnv();
3619
3620         $storeEnv = $this->storeEnv;
3621         $this->storeEnv = $this->env;
3622
3623         // set the args
3624         if (isset($func->args)) {
3625             $this->applyArguments($func->args, $argValues);
3626         }
3627
3628         // throw away lines and children
3629         $tmp = new HTML_Scss_Formatter_OutputBlock;
3630         $tmp->lines    = [];
3631         $tmp->children = [];
3632
3633         $this->env->marker = 'function';
3634
3635         $ret = $this->compileChildren($func->children, $tmp);
3636
3637         $this->storeEnv = $storeEnv;
3638
3639         $this->popEnv();
3640
3641         $returnValue = ! isset($ret) ? static::$defaultValue : $ret;
3642
3643         return true;
3644     }
3645
3646     /**
3647      * Call built-in and registered (PHP) functions
3648      *
3649      * @param string $name
3650      * @param array  $args
3651      * @param array  $returnValue
3652      *
3653      * @return boolean Returns true if returnValue is set; otherwise, false
3654      */
3655     protected function callNativeFunction($name, $args, &$returnValue)
3656     {
3657         // try a lib function
3658         $name = $this->normalizeName($name);
3659
3660         if (isset($this->userFunctions[$name])) {
3661             // see if we can find a user function
3662             list($f, $prototype) = $this->userFunctions[$name];
3663         } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
3664             $libName   = $f[1];
3665             $prototype = isset(static::$$libName) ? static::$$libName : null;
3666         } else {
3667             return false;
3668         }
3669
3670         @list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
3671
3672         if ($name !== 'if' && $name !== 'call') {
3673             foreach ($sorted as &$val) {
3674                 $val = $this->reduce($val, true);
3675             }
3676         }
3677
3678         $returnValue = call_user_func($f, $sorted, $kwargs);
3679
3680         if (! isset($returnValue)) {
3681             return false;
3682         }
3683
3684         $returnValue = $this->coerceValue($returnValue);
3685
3686         return true;
3687     }
3688
3689     /**
3690      * Get built-in function
3691      *
3692      * @param string $name Normalized name
3693      *
3694      * @return array
3695      */
3696     protected function getBuiltinFunction($name)
3697     {
3698         $libName = 'lib' . preg_replace_callback(
3699             '/_(.)/',
3700             function ($m) {
3701                 return ucfirst($m[1]);
3702             },
3703             ucfirst($name)
3704         );
3705
3706         return [$this, $libName];
3707     }
3708
3709     /**
3710      * Sorts keyword arguments
3711      *
3712      * @param array $prototype
3713      * @param array $args
3714      *
3715      * @return array
3716      */
3717     protected function sortArgs($prototype, $args)
3718     {
3719         $keyArgs = [];
3720         $posArgs = [];
3721
3722         // separate positional and keyword arguments
3723         foreach ($args as $arg) {
3724             list($key, $value) = $arg;
3725
3726             $key = $key[1];
3727
3728             if (empty($key)) {
3729                 $posArgs[] = empty($arg[2]) ? $value : $arg;
3730             } else {
3731                 $keyArgs[$key] = $value;
3732             }
3733         }
3734
3735         if (! isset($prototype)) {
3736             return [$posArgs, $keyArgs];
3737         }
3738
3739         // copy positional args
3740         $finalArgs = array_pad($posArgs, count($prototype), null);
3741
3742         // overwrite positional args with keyword args
3743         foreach ($prototype as $i => $names) {
3744             foreach ((array) $names as $name) {
3745                 if (isset($keyArgs[$name])) {
3746                     $finalArgs[$i] = $keyArgs[$name];
3747                 }
3748             }
3749         }
3750
3751         return [$finalArgs, $keyArgs];
3752     }
3753
3754     /**
3755      * Apply argument values per definition
3756      *
3757      * @param array $argDef
3758      * @param array $argValues
3759      *
3760      * @throws \Exception
3761      */
3762     protected function applyArguments($argDef, $argValues)
3763     {
3764         $storeEnv = $this->getStoreEnv();
3765
3766         $env = new HTML_Scss_Compiler_Environment;
3767         $env->store = $storeEnv->store;
3768
3769         $hasVariable = false;
3770         $args = [];
3771
3772         foreach ($argDef as $i => $arg) {
3773             list($name, $default, $isVariable) = $argDef[$i];
3774
3775             $args[$name] = [$i, $name, $default, $isVariable];
3776             $hasVariable |= $isVariable;
3777         }
3778
3779         $keywordArgs = [];
3780         $deferredKeywordArgs = [];
3781         $remaining = [];
3782
3783         // assign the keyword args
3784         foreach ((array) $argValues as $arg) {
3785             if (! empty($arg[0])) {
3786                 if (! isset($args[$arg[0][1]])) {
3787                     if ($hasVariable) {
3788                         $deferredKeywordArgs[$arg[0][1]] = $arg[1];
3789                     } else {
3790                         $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
3791                         break;
3792                     }
3793                 } elseif ($args[$arg[0][1]][0] < count($remaining)) {
3794                     $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
3795                     break;
3796                 } else {
3797                     $keywordArgs[$arg[0][1]] = $arg[1];
3798                 }
3799             } elseif (count($keywordArgs)) {
3800                 $this->throwError('Positional arguments must come before keyword arguments.');
3801                 break;
3802             } elseif ($arg[2] === true) {
3803                 $val = $this->reduce($arg[1], true);
3804
3805                 if ($val[0] === HTML_Scss_Type::T_LIST) {
3806                     foreach ($val[2] as $name => $item) {
3807                         if (! is_numeric($name)) {
3808                             $keywordArgs[$name] = $item;
3809                         } else {
3810                             $remaining[] = $item;
3811                         }
3812                     }
3813                 } elseif ($val[0] === HTML_Scss_Type::T_MAP) {
3814                     foreach ($val[1] as $i => $name) {
3815                         $name = $this->compileStringContent($this->coerceString($name));
3816                         $item = $val[2][$i];
3817
3818                         if (! is_numeric($name)) {
3819                             $keywordArgs[$name] = $item;
3820                         } else {
3821                             $remaining[] = $item;
3822                         }
3823                     }
3824                 } else {
3825                     $remaining[] = $val;
3826                 }
3827             } else {
3828                 $remaining[] = $arg[1];
3829             }
3830         }
3831
3832         foreach ($args as $arg) {
3833             list($i, $name, $default, $isVariable) = $arg;
3834
3835             if ($isVariable) {
3836                 $val = [HTML_Scss_Type::T_LIST, ',', [], $isVariable];
3837
3838                 for ($count = count($remaining); $i < $count; $i++) {
3839                     $val[2][] = $remaining[$i];
3840                 }
3841
3842                 foreach ($deferredKeywordArgs as $itemName => $item) {
3843                     $val[2][$itemName] = $item;
3844                 }
3845             } elseif (isset($remaining[$i])) {
3846                 $val = $remaining[$i];
3847             } elseif (isset($keywordArgs[$name])) {
3848                 $val = $keywordArgs[$name];
3849             } elseif (! empty($default)) {
3850                 continue;
3851             } else {
3852                 $this->throwError("Missing argument $name");
3853                 break;
3854             }
3855
3856             $this->set($name, $this->reduce($val, true), true, $env);
3857         }
3858
3859         $storeEnv->store = $env->store;
3860
3861         foreach ($args as $arg) {
3862             list($i, $name, $default, $isVariable) = $arg;
3863
3864             if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
3865                 continue;
3866             }
3867
3868             $this->set($name, $this->reduce($default, true), true);
3869         }
3870     }
3871
3872     /**
3873      * Coerce a php value into a scss one
3874      *
3875      * @param mixed $value
3876      *
3877      * @return array|\Leafo\ScssPhp\Node\Number
3878      */
3879     private function coerceValue($value)
3880     {
3881         if (is_array($value) || $value instanceof  ArrayAccess) {
3882             return $value;
3883         }
3884
3885         if (is_bool($value)) {
3886             return $this->toBool($value);
3887         }
3888
3889         if ($value === null) {
3890             return static::$null;
3891         }
3892
3893         if (is_numeric($value)) {
3894             return new HTML_Scss_Node_Number($value, '');
3895         }
3896
3897         if ($value === '') {
3898             return static::$emptyString;
3899         }
3900
3901         if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
3902             $color = [HTML_Scss_Type::T_COLOR];
3903
3904             if (isset($m[3])) {
3905                 $num = hexdec($m[3]);
3906
3907                 foreach ([3, 2, 1] as $i) {
3908                     $t = $num & 0xf;
3909                     $color[$i] = $t << 4 | $t;
3910                     $num >>= 4;
3911                 }
3912             } else {
3913                 $num = hexdec($m[2]);
3914
3915                 foreach ([3, 2, 1] as $i) {
3916                     $color[$i] = $num & 0xff;
3917                     $num >>= 8;
3918                 }
3919             }
3920
3921             return $color;
3922         }
3923
3924         return [HTML_Scss_Type::T_KEYWORD, $value];
3925     }
3926
3927     /**
3928      * Coerce something to map
3929      *
3930      * @param array $item
3931      *
3932      * @return array
3933      */
3934     protected function coerceMap($item)
3935     {
3936         if ($item[0] === HTML_Scss_Type::T_MAP) {
3937             return $item;
3938         }
3939
3940         if ($item === static::$emptyList) {
3941             return static::$emptyMap;
3942         }
3943
3944         return [HTML_Scss_Type::T_MAP, [$item], [static::$null]];
3945     }
3946
3947     /**
3948      * Coerce something to list
3949      *
3950      * @param array  $item
3951      * @param string $delim
3952      *
3953      * @return array
3954      */
3955     protected function coerceList($item, $delim = ',')
3956     {
3957         if (isset($item) && $item[0] === HTML_Scss_Type::T_LIST) {
3958             return $item;
3959         }
3960
3961         if (isset($item) && $item[0] === HTML_Scss_Type::T_MAP) {
3962             $keys = $item[1];
3963             $values = $item[2];
3964             $list = [];
3965
3966             for ($i = 0, $s = count($keys); $i < $s; $i++) {
3967                 $key = $keys[$i];
3968                 $value = $values[$i];
3969
3970                 $list[] = [
3971                     HTML_Scss_Type::T_LIST,
3972                     '',
3973                     [[HTML_Scss_Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
3974                 ];
3975             }
3976
3977             return [HTML_Scss_Type::T_LIST, ',', $list];
3978         }
3979
3980         return [HTML_Scss_Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
3981     }
3982
3983     /**
3984      * Coerce color for expression
3985      *
3986      * @param array $value
3987      *
3988      * @return array|null
3989      */
3990     protected function coerceForExpression($value)
3991     {
3992         if ($color = $this->coerceColor($value)) {
3993             return $color;
3994         }
3995
3996         return $value;
3997     }
3998
3999     /**
4000      * Coerce value to color
4001      *
4002      * @param array $value
4003      *
4004      * @return array|null
4005      */
4006     protected function coerceColor($value)
4007     {
4008         switch ($value[0]) {
4009             case HTML_Scss_Type::T_COLOR:
4010                 return $value;
4011
4012             case HTML_Scss_Type::T_KEYWORD:
4013                 $name = strtolower($value[1]);
4014
4015                 if (isset(HTML_Scss_Colors::$cssColors[$name])) {
4016                     $rgba = explode(',', HTML_Scss_Colors::$cssColors[$name]);
4017
4018                     return isset($rgba[3])
4019                         ? [HTML_Scss_Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
4020                         : [HTML_Scss_Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
4021                 }
4022
4023                 return null;
4024         }
4025
4026         return null;
4027     }
4028
4029     /**
4030      * Coerce value to string
4031      *
4032      * @param array $value
4033      *
4034      * @return array|null
4035      */
4036     protected function coerceString($value)
4037     {
4038         if ($value[0] === HTML_Scss_Type::T_STRING) {
4039             return $value;
4040         }
4041
4042         return [HTML_Scss_Type::T_STRING, '', [$this->compileValue($value)]];
4043     }
4044
4045     /**
4046      * Coerce value to a percentage
4047      *
4048      * @param array $value
4049      *
4050      * @return integer|float
4051      */
4052     protected function coercePercent($value)
4053     {
4054         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4055             if (! empty($value[2]['%'])) {
4056                 return $value[1] / 100;
4057             }
4058
4059             return $value[1];
4060         }
4061
4062         return 0;
4063     }
4064
4065     /**
4066      * Assert value is a map
4067      *
4068      * @api
4069      *
4070      * @param array $value
4071      *
4072      * @return array
4073      *
4074      * @throws \Exception
4075      */
4076     public function assertMap($value)
4077     {
4078         $value = $this->coerceMap($value);
4079
4080         if ($value[0] !== HTML_Scss_Type::T_MAP) {
4081             $this->throwError('expecting map');
4082         }
4083
4084         return $value;
4085     }
4086
4087     /**
4088      * Assert value is a list
4089      *
4090      * @api
4091      *
4092      * @param array $value
4093      *
4094      * @return array
4095      *
4096      * @throws \Exception
4097      */
4098     public function assertList($value)
4099     {
4100         if ($value[0] !== HTML_Scss_Type::T_LIST) {
4101             $this->throwError('expecting list');
4102         }
4103
4104         return $value;
4105     }
4106
4107     /**
4108      * Assert value is a color
4109      *
4110      * @api
4111      *
4112      * @param array $value
4113      *
4114      * @return array
4115      *
4116      * @throws \Exception
4117      */
4118     public function assertColor($value)
4119     {
4120         if ($color = $this->coerceColor($value)) {
4121             return $color;
4122         }
4123
4124         $this->throwError('expecting color');
4125     }
4126
4127     /**
4128      * Assert value is a number
4129      *
4130      * @api
4131      *
4132      * @param array $value
4133      *
4134      * @return integer|float
4135      *
4136      * @throws \Exception
4137      */
4138     public function assertNumber($value)
4139     {
4140         if ($value[0] !== HTML_Scss_Type::T_NUMBER) {
4141             $this->throwError('expecting number');
4142         }
4143
4144         return $value[1];
4145     }
4146
4147     /**
4148      * Make sure a color's components don't go out of bounds
4149      *
4150      * @param array $c
4151      *
4152      * @return array
4153      */
4154     protected function fixColor($c)
4155     {
4156         foreach ([1, 2, 3] as $i) {
4157             if ($c[$i] < 0) {
4158                 $c[$i] = 0;
4159             }
4160
4161             if ($c[$i] > 255) {
4162                 $c[$i] = 255;
4163             }
4164         }
4165
4166         return $c;
4167     }
4168
4169     /**
4170      * Convert RGB to HSL
4171      *
4172      * @api
4173      *
4174      * @param integer $red
4175      * @param integer $green
4176      * @param integer $blue
4177      *
4178      * @return array
4179      */
4180     public function toHSL($red, $green, $blue)
4181     {
4182         $min = min($red, $green, $blue);
4183         $max = max($red, $green, $blue);
4184
4185         $l = $min + $max;
4186         $d = $max - $min;
4187
4188         if ((int) $d === 0) {
4189             $h = $s = 0;
4190         } else {
4191             if ($l < 255) {
4192                 $s = $d / $l;
4193             } else {
4194                 $s = $d / (510 - $l);
4195             }
4196
4197             if ($red == $max) {
4198                 $h = 60 * ($green - $blue) / $d;
4199             } elseif ($green == $max) {
4200                 $h = 60 * ($blue - $red) / $d + 120;
4201             } elseif ($blue == $max) {
4202                 $h = 60 * ($red - $green) / $d + 240;
4203             }
4204         }
4205
4206         return [HTML_Scss_Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
4207     }
4208
4209     /**
4210      * Hue to RGB helper
4211      *
4212      * @param float $m1
4213      * @param float $m2
4214      * @param float $h
4215      *
4216      * @return float
4217      */
4218     private function hueToRGB($m1, $m2, $h)
4219     {
4220         if ($h < 0) {
4221             $h += 1;
4222         } elseif ($h > 1) {
4223             $h -= 1;
4224         }
4225
4226         if ($h * 6 < 1) {
4227             return $m1 + ($m2 - $m1) * $h * 6;
4228         }
4229
4230         if ($h * 2 < 1) {
4231             return $m2;
4232         }
4233
4234         if ($h * 3 < 2) {
4235             return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
4236         }
4237
4238         return $m1;
4239     }
4240
4241     /**
4242      * Convert HSL to RGB
4243      *
4244      * @api
4245      *
4246      * @param integer $hue        H from 0 to 360
4247      * @param integer $saturation S from 0 to 100
4248      * @param integer $lightness  L from 0 to 100
4249      *
4250      * @return array
4251      */
4252     public function toRGB($hue, $saturation, $lightness)
4253     {
4254         if ($hue < 0) {
4255             $hue += 360;
4256         }
4257
4258         $h = $hue / 360;
4259         $s = min(100, max(0, $saturation)) / 100;
4260         $l = min(100, max(0, $lightness)) / 100;
4261
4262         $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
4263         $m1 = $l * 2 - $m2;
4264
4265         $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
4266         $g = $this->hueToRGB($m1, $m2, $h) * 255;
4267         $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
4268
4269         $out = [HTML_Scss_Type::T_COLOR, $r, $g, $b];
4270
4271         return $out;
4272     }
4273
4274     // Built in functions
4275
4276     //protected static $libCall = ['name', 'args...'];
4277     protected function libCall($args, $kwargs)
4278     {
4279         $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
4280
4281         $posArgs = [];
4282
4283         foreach ($args as $arg) {
4284             if (empty($arg[0])) {
4285                 if ($arg[2] === true) {
4286                     $tmp = $this->reduce($arg[1]);
4287
4288                     if ($tmp[0] === HTML_Scss_Type::T_LIST) {
4289                         foreach ($tmp[2] as $item) {
4290                             $posArgs[] = [null, $item, false];
4291                         }
4292                     } else {
4293                         $posArgs[] = [null, $tmp, true];
4294                     }
4295
4296                     continue;
4297                 }
4298
4299                 $posArgs[] = [null, $this->reduce($arg), false];
4300                 continue;
4301             }
4302
4303             $posArgs[] = [null, $arg, false];
4304         }
4305
4306         if (count($kwargs)) {
4307             foreach ($kwargs as $key => $value) {
4308                 $posArgs[] = [[HTML_Scss_Type::T_VARIABLE, $key], $value, false];
4309             }
4310         }
4311
4312         return $this->reduce([HTML_Scss_Type::T_FUNCTION_CALL, $name, $posArgs]);
4313     }
4314
4315     protected static $libIf = ['condition', 'if-true', 'if-false'];
4316     protected function libIf($args)
4317     {
4318         list($cond, $t, $f) = $args;
4319
4320         if (! $this->isTruthy($this->reduce($cond, true))) {
4321             return $this->reduce($f, true);
4322         }
4323
4324         return $this->reduce($t, true);
4325     }
4326
4327     protected static $libIndex = ['list', 'value'];
4328     protected function libIndex($args)
4329     {
4330         list($list, $value) = $args;
4331
4332         if ($value[0] === HTML_Scss_Type::T_MAP) {
4333             return static::$null;
4334         }
4335
4336         if ($list[0] === HTML_Scss_Type::T_MAP ||
4337             $list[0] === HTML_Scss_Type::T_STRING ||
4338             $list[0] === HTML_Scss_Type::T_KEYWORD ||
4339             $list[0] === HTML_Scss_Type::T_INTERPOLATE
4340         ) {
4341             $list = $this->coerceList($list, ' ');
4342         }
4343
4344         if ($list[0] !== HTML_Scss_Type::T_LIST) {
4345             return static::$null;
4346         }
4347
4348         $values = [];
4349
4350         foreach ($list[2] as $item) {
4351             $values[] = $this->normalizeValue($item);
4352         }
4353
4354         $key = array_search($this->normalizeValue($value), $values);
4355
4356         return false === $key ? static::$null : $key + 1;
4357     }
4358
4359     protected static $libRgb = ['red', 'green', 'blue'];
4360     protected function libRgb($args)
4361     {
4362         list($r, $g, $b) = $args;
4363
4364         return [HTML_Scss_Type::T_COLOR, $r[1], $g[1], $b[1]];
4365     }
4366
4367     protected static $libRgba = [
4368         ['red', 'color'],
4369         'green', 'blue', 'alpha'];
4370     protected function libRgba($args)
4371     {
4372         if ($color = $this->coerceColor($args[0])) {
4373             $num = isset($args[3]) ? $args[3] : $args[1];
4374             $alpha = $this->assertNumber($num);
4375             $color[4] = $alpha;
4376
4377             return $color;
4378         }
4379
4380         list($r, $g, $b, $a) = $args;
4381
4382         return [HTML_Scss_Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
4383     }
4384
4385     // helper function for adjust_color, change_color, and scale_color
4386     protected function alterColor($args, $fn)
4387     {
4388         $color = $this->assertColor($args[0]);
4389
4390         foreach ([1, 2, 3, 7] as $i) {
4391             if (isset($args[$i])) {
4392                 $val = $this->assertNumber($args[$i]);
4393                 $ii = $i === 7 ? 4 : $i; // alpha
4394                 $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
4395             }
4396         }
4397
4398         if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
4399             $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4400
4401             foreach ([4, 5, 6] as $i) {
4402                 if (isset($args[$i])) {
4403                     $val = $this->assertNumber($args[$i]);
4404                     $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
4405                 }
4406             }
4407
4408             $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
4409
4410             if (isset($color[4])) {
4411                 $rgb[4] = $color[4];
4412             }
4413
4414             $color = $rgb;
4415         }
4416
4417         return $color;
4418     }
4419
4420     protected static $libAdjustColor = [
4421         'color', 'red', 'green', 'blue',
4422         'hue', 'saturation', 'lightness', 'alpha'
4423     ];
4424     protected function libAdjustColor($args)
4425     {
4426         return $this->alterColor($args, function ($base, $alter, $i) {
4427             return $base + $alter;
4428         });
4429     }
4430
4431     protected static $libChangeColor = [
4432         'color', 'red', 'green', 'blue',
4433         'hue', 'saturation', 'lightness', 'alpha'
4434     ];
4435     protected function libChangeColor($args)
4436     {
4437         return $this->alterColor($args, function ($base, $alter, $i) {
4438             return $alter;
4439         });
4440     }
4441
4442     protected static $libScaleColor = [
4443         'color', 'red', 'green', 'blue',
4444         'hue', 'saturation', 'lightness', 'alpha'
4445     ];
4446     protected function libScaleColor($args)
4447     {
4448         return $this->alterColor($args, function ($base, $scale, $i) {
4449             // 1, 2, 3 - rgb
4450             // 4, 5, 6 - hsl
4451             // 7 - a
4452             switch ($i) {
4453                 case 1:
4454                 case 2:
4455                 case 3:
4456                     $max = 255;
4457                     break;
4458
4459                 case 4:
4460                     $max = 360;
4461                     break;
4462
4463                 case 7:
4464                     $max = 1;
4465                     break;
4466
4467                 default:
4468                     $max = 100;
4469             }
4470
4471             $scale = $scale / 100;
4472
4473             if ($scale < 0) {
4474                 return $base * $scale + $base;
4475             }
4476
4477             return ($max - $base) * $scale + $base;
4478         });
4479     }
4480
4481     protected static $libIeHexStr = ['color'];
4482     protected function libIeHexStr($args)
4483     {
4484         $color = $this->coerceColor($args[0]);
4485         $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
4486
4487         return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
4488     }
4489
4490     protected static $libRed = ['color'];
4491     protected function libRed($args)
4492     {
4493         $color = $this->coerceColor($args[0]);
4494
4495         return $color[1];
4496     }
4497
4498     protected static $libGreen = ['color'];
4499     protected function libGreen($args)
4500     {
4501         $color = $this->coerceColor($args[0]);
4502
4503         return $color[2];
4504     }
4505
4506     protected static $libBlue = ['color'];
4507     protected function libBlue($args)
4508     {
4509         $color = $this->coerceColor($args[0]);
4510
4511         return $color[3];
4512     }
4513
4514     protected static $libAlpha = ['color'];
4515     protected function libAlpha($args)
4516     {
4517         if ($color = $this->coerceColor($args[0])) {
4518             return isset($color[4]) ? $color[4] : 1;
4519         }
4520
4521         // this might be the IE function, so return value unchanged
4522         return null;
4523     }
4524
4525     protected static $libOpacity = ['color'];
4526     protected function libOpacity($args)
4527     {
4528         $value = $args[0];
4529
4530         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4531             return null;
4532         }
4533
4534         return $this->libAlpha($args);
4535     }
4536
4537     // mix two colors
4538     protected static $libMix = ['color-1', 'color-2', 'weight'];
4539     protected function libMix($args)
4540     {
4541         list($first, $second, $weight) = $args;
4542
4543         $first = $this->assertColor($first);
4544         $second = $this->assertColor($second);
4545
4546         if (! isset($weight)) {
4547             $weight = 0.5;
4548         } else {
4549             $weight = $this->coercePercent($weight);
4550         }
4551
4552         $firstAlpha = isset($first[4]) ? $first[4] : 1;
4553         $secondAlpha = isset($second[4]) ? $second[4] : 1;
4554
4555         $w = $weight * 2 - 1;
4556         $a = $firstAlpha - $secondAlpha;
4557
4558         $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
4559         $w2 = 1.0 - $w1;
4560
4561         $new = [HTML_Scss_Type::T_COLOR,
4562             $w1 * $first[1] + $w2 * $second[1],
4563             $w1 * $first[2] + $w2 * $second[2],
4564             $w1 * $first[3] + $w2 * $second[3],
4565         ];
4566
4567         if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
4568             $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight);
4569         }
4570
4571         return $this->fixColor($new);
4572     }
4573
4574     protected static $libHsl = ['hue', 'saturation', 'lightness'];
4575     protected function libHsl($args)
4576     {
4577         list($h, $s, $l) = $args;
4578
4579         return $this->toRGB($h[1], $s[1], $l[1]);
4580     }
4581
4582     protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
4583     protected function libHsla($args)
4584     {
4585         list($h, $s, $l, $a) = $args;
4586
4587         $color = $this->toRGB($h[1], $s[1], $l[1]);
4588         $color[4] = $a[1];
4589
4590         return $color;
4591     }
4592
4593     protected static $libHue = ['color'];
4594     protected function libHue($args)
4595     {
4596         $color = $this->assertColor($args[0]);
4597         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4598
4599         return new HTML_Scss_Node_Number($hsl[1], 'deg');
4600     }
4601
4602     protected static $libSaturation = ['color'];
4603     protected function libSaturation($args)
4604     {
4605         $color = $this->assertColor($args[0]);
4606         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4607
4608         return new HTML_Scss_Node_Number($hsl[2], '%');
4609     }
4610
4611     protected static $libLightness = ['color'];
4612     protected function libLightness($args)
4613     {
4614         $color = $this->assertColor($args[0]);
4615         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4616
4617         return new HTML_Scss_Node_Number($hsl[3], '%');
4618     }
4619
4620     protected function adjustHsl($color, $idx, $amount)
4621     {
4622         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4623         $hsl[$idx] += $amount;
4624         $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
4625
4626         if (isset($color[4])) {
4627             $out[4] = $color[4];
4628         }
4629
4630         return $out;
4631     }
4632
4633     protected static $libAdjustHue = ['color', 'degrees'];
4634     protected function libAdjustHue($args)
4635     {
4636         $color = $this->assertColor($args[0]);
4637         $degrees = $this->assertNumber($args[1]);
4638
4639         return $this->adjustHsl($color, 1, $degrees);
4640     }
4641
4642     protected static $libLighten = ['color', 'amount'];
4643     protected function libLighten($args)
4644     {
4645         $color = $this->assertColor($args[0]);
4646         require_once 'Scss/Base/Range.php';
4647         $amount = HTML_Scss_Util::checkRange('amount', new HTML_Scss_Base_Range(0, 100), $args[1], '%');
4648
4649         return $this->adjustHsl($color, 3, $amount);
4650     }
4651
4652     protected static $libDarken = ['color', 'amount'];
4653     protected function libDarken($args)
4654     {
4655         $color = $this->assertColor($args[0]);
4656         require_once 'Scss/Base/Range.php';
4657         $amount = HTML_Scss_Util::checkRange('amount', new HTML_Scss_Base_Range(0, 100), $args[1], '%');
4658
4659         return $this->adjustHsl($color, 3, -$amount);
4660     }
4661
4662     protected static $libSaturate = ['color', 'amount'];
4663     protected function libSaturate($args)
4664     {
4665         $value = $args[0];
4666
4667         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4668             return null;
4669         }
4670
4671         $color = $this->assertColor($value);
4672         $amount = 100 * $this->coercePercent($args[1]);
4673
4674         return $this->adjustHsl($color, 2, $amount);
4675     }
4676
4677     protected static $libDesaturate = ['color', 'amount'];
4678     protected function libDesaturate($args)
4679     {
4680         $color = $this->assertColor($args[0]);
4681         $amount = 100 * $this->coercePercent($args[1]);
4682
4683         return $this->adjustHsl($color, 2, -$amount);
4684     }
4685
4686     protected static $libGrayscale = ['color'];
4687     protected function libGrayscale($args)
4688     {
4689         $value = $args[0];
4690
4691         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4692             return null;
4693         }
4694
4695         return $this->adjustHsl($this->assertColor($value), 2, -100);
4696     }
4697
4698     protected static $libComplement = ['color'];
4699     protected function libComplement($args)
4700     {
4701         return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
4702     }
4703
4704     protected static $libInvert = ['color'];
4705     protected function libInvert($args)
4706     {
4707         $value = $args[0];
4708
4709         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4710             return null;
4711         }
4712
4713         $color = $this->assertColor($value);
4714         $color[1] = 255 - $color[1];
4715         $color[2] = 255 - $color[2];
4716         $color[3] = 255 - $color[3];
4717
4718         return $color;
4719     }
4720
4721     // increases opacity by amount
4722     protected static $libOpacify = ['color', 'amount'];
4723     protected function libOpacify($args)
4724     {
4725         $color = $this->assertColor($args[0]);
4726         $amount = $this->coercePercent($args[1]);
4727
4728         $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
4729         $color[4] = min(1, max(0, $color[4]));
4730
4731         return $color;
4732     }
4733
4734     protected static $libFadeIn = ['color', 'amount'];
4735     protected function libFadeIn($args)
4736     {
4737         return $this->libOpacify($args);
4738     }
4739
4740     // decreases opacity by amount
4741     protected static $libTransparentize = ['color', 'amount'];
4742     protected function libTransparentize($args)
4743     {
4744         $color = $this->assertColor($args[0]);
4745         $amount = $this->coercePercent($args[1]);
4746
4747         $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
4748         $color[4] = min(1, max(0, $color[4]));
4749
4750         return $color;
4751     }
4752
4753     protected static $libFadeOut = ['color', 'amount'];
4754     protected function libFadeOut($args)
4755     {
4756         return $this->libTransparentize($args);
4757     }
4758
4759     protected static $libUnquote = ['string'];
4760     protected function libUnquote($args)
4761     {
4762         $str = $args[0];
4763
4764         if ($str[0] === HTML_Scss_Type::T_STRING) {
4765             $str[1] = '';
4766         }
4767
4768         return $str;
4769     }
4770
4771     protected static $libQuote = ['string'];
4772     protected function libQuote($args)
4773     {
4774         $value = $args[0];
4775
4776         if ($value[0] === HTML_Scss_Type::T_STRING && ! empty($value[1])) {
4777             return $value;
4778         }
4779
4780         return [HTML_Scss_Type::T_STRING, '"', [$value]];
4781     }
4782
4783     protected static $libPercentage = ['value'];
4784     protected function libPercentage($args)
4785     {
4786         return new HTML_Scss_Node_Number($this->coercePercent($args[0]) * 100, '%');
4787     }
4788
4789     protected static $libRound = ['value'];
4790     protected function libRound($args)
4791     {
4792         $num = $args[0];
4793
4794         return new HTML_Scss_Node_Number(round($num[1]), $num[2]);
4795     }
4796
4797     protected static $libFloor = ['value'];
4798     protected function libFloor($args)
4799     {
4800         $num = $args[0];
4801
4802         return new HTML_Scss_Node_Number(floor($num[1]), $num[2]);
4803     }
4804
4805     protected static $libCeil = ['value'];
4806     protected function libCeil($args)
4807     {
4808         $num = $args[0];
4809
4810         return new HTML_Scss_Node_Number(ceil($num[1]), $num[2]);
4811     }
4812
4813     protected static $libAbs = ['value'];
4814     protected function libAbs($args)
4815     {
4816         $num = $args[0];
4817
4818         return new HTML_Scss_Node_Number(abs($num[1]), $num[2]);
4819     }
4820
4821     protected function libMin($args)
4822     {
4823         $numbers = $this->getNormalizedNumbers($args);
4824         $min = null;
4825
4826         foreach ($numbers as $key => $number) {
4827             if (null === $min || $number[1] <= $min[1]) {
4828                 $min = [$key, $number[1]];
4829             }
4830         }
4831
4832         return $args[$min[0]];
4833     }
4834
4835     protected function libMax($args)
4836     {
4837         $numbers = $this->getNormalizedNumbers($args);
4838         $max = null;
4839
4840         foreach ($numbers as $key => $number) {
4841             if (null === $max || $number[1] >= $max[1]) {
4842                 $max = [$key, $number[1]];
4843             }
4844         }
4845
4846         return $args[$max[0]];
4847     }
4848
4849     /**
4850      * Helper to normalize args containing numbers
4851      *
4852      * @param array $args
4853      *
4854      * @return array
4855      */
4856     protected function getNormalizedNumbers($args)
4857     {
4858         $unit = null;
4859         $originalUnit = null;
4860         $numbers = [];
4861
4862         foreach ($args as $key => $item) {
4863             if ($item[0] !== HTML_Scss_Type::T_NUMBER) {
4864                 $this->throwError('%s is not a number', $item[0]);
4865                 break;
4866             }
4867
4868             $number = $item->normalize();
4869
4870             if (null === $unit) {
4871                 $unit = $number[2];
4872                 $originalUnit = $item->unitStr();
4873             } elseif ($number[1] && $unit !== $number[2]) {
4874                 $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
4875                 break;
4876             }
4877
4878             $numbers[$key] = $number;
4879         }
4880
4881         return $numbers;
4882     }
4883
4884     protected static $libLength = ['list'];
4885     protected function libLength($args)
4886     {
4887         $list = $this->coerceList($args[0]);
4888
4889         return count($list[2]);
4890     }
4891
4892     //protected static $libListSeparator = ['list...'];
4893     protected function libListSeparator($args)
4894     {
4895         if (count($args) > 1) {
4896             return 'comma';
4897         }
4898
4899         $list = $this->coerceList($args[0]);
4900
4901         if (count($list[2]) <= 1) {
4902             return 'space';
4903         }
4904
4905         if ($list[1] === ',') {
4906             return 'comma';
4907         }
4908
4909         return 'space';
4910     }
4911
4912     protected static $libNth = ['list', 'n'];
4913     protected function libNth($args)
4914     {
4915         $list = $this->coerceList($args[0]);
4916         $n = $this->assertNumber($args[1]);
4917
4918         if ($n > 0) {
4919             $n--;
4920         } elseif ($n < 0) {
4921             $n += count($list[2]);
4922         }
4923
4924         return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
4925     }
4926
4927     protected static $libSetNth = ['list', 'n', 'value'];
4928     protected function libSetNth($args)
4929     {
4930         $list = $this->coerceList($args[0]);
4931         $n = $this->assertNumber($args[1]);
4932
4933         if ($n > 0) {
4934             $n--;
4935         } elseif ($n < 0) {
4936             $n += count($list[2]);
4937         }
4938
4939         if (! isset($list[2][$n])) {
4940             $this->throwError('Invalid argument for "n"');
4941
4942             return;
4943         }
4944
4945         $list[2][$n] = $args[2];
4946
4947         return $list;
4948     }
4949
4950     protected static $libMapGet = ['map', 'key'];
4951     protected function libMapGet($args)
4952     {
4953         $map = $this->assertMap($args[0]);
4954         $key = $this->compileStringContent($this->coerceString($args[1]));
4955
4956         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4957             if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4958                 return $map[2][$i];
4959             }
4960         }
4961
4962         return static::$null;
4963     }
4964
4965     protected static $libMapKeys = ['map'];
4966     protected function libMapKeys($args)
4967     {
4968         $map = $this->assertMap($args[0]);
4969         $keys = $map[1];
4970
4971         return [HTML_Scss_Type::T_LIST, ',', $keys];
4972     }
4973
4974     protected static $libMapValues = ['map'];
4975     protected function libMapValues($args)
4976     {
4977         $map = $this->assertMap($args[0]);
4978         $values = $map[2];
4979
4980         return [HTML_Scss_Type::T_LIST, ',', $values];
4981     }
4982
4983     protected static $libMapRemove = ['map', 'key'];
4984     protected function libMapRemove($args)
4985     {
4986         $map = $this->assertMap($args[0]);
4987         $key = $this->compileStringContent($this->coerceString($args[1]));
4988
4989         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4990             if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4991                 array_splice($map[1], $i, 1);
4992                 array_splice($map[2], $i, 1);
4993             }
4994         }
4995
4996         return $map;
4997     }
4998
4999     protected static $libMapHasKey = ['map', 'key'];
5000     protected function libMapHasKey($args)
5001     {
5002         $map = $this->assertMap($args[0]);
5003         $key = $this->compileStringContent($this->coerceString($args[1]));
5004
5005         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
5006             if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
5007                 return true;
5008             }
5009         }
5010
5011         return false;
5012     }
5013
5014     protected static $libMapMerge = ['map-1', 'map-2'];
5015     protected function libMapMerge($args)
5016     {
5017         $map1 = $this->assertMap($args[0]);
5018         $map2 = $this->assertMap($args[1]);
5019
5020         foreach ($map2[1] as $i2 => $key2) {
5021             $key = $this->compileStringContent($this->coerceString($key2));
5022
5023             foreach ($map1[1] as $i1 => $key1) {
5024                 if ($key === $this->compileStringContent($this->coerceString($key1))) {
5025                     $map1[2][$i1] = $map2[2][$i2];
5026                     continue 2;
5027                 }
5028             }
5029
5030             $map1[1][] = $map2[1][$i2];
5031             $map1[2][] = $map2[2][$i2];
5032         }
5033
5034         return $map1;
5035     }
5036
5037     protected static $libKeywords = ['args'];
5038     protected function libKeywords($args)
5039     {
5040         $this->assertList($args[0]);
5041
5042         $keys = [];
5043         $values = [];
5044
5045         foreach ($args[0][2] as $name => $arg) {
5046             $keys[] = [HTML_Scss_Type::T_KEYWORD, $name];
5047             $values[] = $arg;
5048         }
5049
5050         return [HTML_Scss_Type::T_MAP, $keys, $values];
5051     }
5052
5053     protected function listSeparatorForJoin($list1, $sep)
5054     {
5055         if (! isset($sep)) {
5056             return $list1[1];
5057         }
5058
5059         switch ($this->compileValue($sep)) {
5060             case 'comma':
5061                 return ',';
5062
5063             case 'space':
5064                 return '';
5065
5066             default:
5067                 return $list1[1];
5068         }
5069     }
5070
5071     protected static $libJoin = ['list1', 'list2', 'separator'];
5072     protected function libJoin($args)
5073     {
5074         list($list1, $list2, $sep) = $args;
5075
5076         $list1 = $this->coerceList($list1, ' ');
5077         $list2 = $this->coerceList($list2, ' ');
5078         $sep = $this->listSeparatorForJoin($list1, $sep);
5079
5080         return [HTML_Scss_Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
5081     }
5082
5083     protected static $libAppend = ['list', 'val', 'separator'];
5084     protected function libAppend($args)
5085     {
5086         list($list1, $value, $sep) = $args;
5087
5088         $list1 = $this->coerceList($list1, ' ');
5089         $sep = $this->listSeparatorForJoin($list1, $sep);
5090
5091         return [HTML_Scss_Type::T_LIST, $sep, array_merge($list1[2], [$value])];
5092     }
5093
5094     protected function libZip($args)
5095     {
5096         foreach ($args as $arg) {
5097             $this->assertList($arg);
5098         }
5099
5100         $lists = [];
5101         $firstList = array_shift($args);
5102
5103         foreach ($firstList[2] as $key => $item) {
5104             $list = [HTML_Scss_Type::T_LIST, '', [$item]];
5105
5106             foreach ($args as $arg) {
5107                 if (isset($arg[2][$key])) {
5108                     $list[2][] = $arg[2][$key];
5109                 } else {
5110                     break 2;
5111                 }
5112             }
5113
5114             $lists[] = $list;
5115         }
5116
5117         return [HTML_Scss_Type::T_LIST, ',', $lists];
5118     }
5119
5120     protected static $libTypeOf = ['value'];
5121     protected function libTypeOf($args)
5122     {
5123         $value = $args[0];
5124
5125         switch ($value[0]) {
5126             case HTML_Scss_Type::T_KEYWORD:
5127                 if ($value === static::$true || $value === static::$false) {
5128                     return 'bool';
5129                 }
5130
5131                 if ($this->coerceColor($value)) {
5132                     return 'color';
5133                 }
5134
5135                 // fall-thru
5136             case HTML_Scss_Type::T_FUNCTION:
5137                 return 'string';
5138
5139             case HTML_Scss_Type::T_LIST:
5140                 if (isset($value[3]) && $value[3]) {
5141                     return 'arglist';
5142                 }
5143
5144                 // fall-thru
5145             default:
5146                 return $value[0];
5147         }
5148     }
5149
5150     protected static $libUnit = ['number'];
5151     protected function libUnit($args)
5152     {
5153         $num = $args[0];
5154
5155         if ($num[0] === HTML_Scss_Type::T_NUMBER) {
5156             return [HTML_Scss_Type::T_STRING, '"', [$num->unitStr()]];
5157         }
5158
5159         return '';
5160     }
5161
5162     protected static $libUnitless = ['number'];
5163     protected function libUnitless($args)
5164     {
5165         $value = $args[0];
5166
5167         return $value[0] === HTML_Scss_Type::T_NUMBER && $value->unitless();
5168     }
5169
5170     protected static $libComparable = ['number-1', 'number-2'];
5171     protected function libComparable($args)
5172     {
5173         list($number1, $number2) = $args;
5174
5175         if (! isset($number1[0]) || $number1[0] !== HTML_Scss_Type::T_NUMBER ||
5176             ! isset($number2[0]) || $number2[0] !== HTML_Scss_Type::T_NUMBER
5177         ) {
5178             $this->throwError('Invalid argument(s) for "comparable"');
5179
5180             return;
5181         }
5182
5183         $number1 = $number1->normalize();
5184         $number2 = $number2->normalize();
5185
5186         return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
5187     }
5188
5189     protected static $libStrIndex = ['string', 'substring'];
5190     protected function libStrIndex($args)
5191     {
5192         $string = $this->coerceString($args[0]);
5193         $stringContent = $this->compileStringContent($string);
5194
5195         $substring = $this->coerceString($args[1]);
5196         $substringContent = $this->compileStringContent($substring);
5197
5198         $result = strpos($stringContent, $substringContent);
5199
5200         return $result === false ? static::$null : new HTML_Scss_Node_Number($result + 1, '');
5201     }
5202
5203     protected static $libStrInsert = ['string', 'insert', 'index'];
5204     protected function libStrInsert($args)
5205     {
5206         $string = $this->coerceString($args[0]);
5207         $stringContent = $this->compileStringContent($string);
5208
5209         $insert = $this->coerceString($args[1]);
5210         $insertContent = $this->compileStringContent($insert);
5211
5212         list(, $index) = $args[2];
5213
5214         $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
5215
5216         return $string;
5217     }
5218
5219     protected static $libStrLength = ['string'];
5220     protected function libStrLength($args)
5221     {
5222         $string = $this->coerceString($args[0]);
5223         $stringContent = $this->compileStringContent($string);
5224
5225         return new HTML_Scss_Node_Number(strlen($stringContent), '');
5226     }
5227
5228     protected static $libStrSlice = ['string', 'start-at', 'end-at'];
5229     protected function libStrSlice($args)
5230     {
5231         if (isset($args[2]) && $args[2][1] == 0) {
5232             return static::$nullString;
5233         }
5234
5235         $string = $this->coerceString($args[0]);
5236         $stringContent = $this->compileStringContent($string);
5237
5238         $start = (int) $args[1][1];
5239
5240         if ($start > 0) {
5241             $start--;
5242         }
5243
5244         $end    = (int) isset($args[2][1]) ? $args[2][1] : 0;
5245         $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
5246
5247         $string[2] = $length
5248             ? [substr($stringContent, $start, $length)]
5249             : [substr($stringContent, $start)];
5250
5251         return $string;
5252     }
5253
5254     protected static $libToLowerCase = ['string'];
5255     protected function libToLowerCase($args)
5256     {
5257         $string = $this->coerceString($args[0]);
5258         $stringContent = $this->compileStringContent($string);
5259
5260         $string[2] = [function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)];
5261
5262         return $string;
5263     }
5264
5265     protected static $libToUpperCase = ['string'];
5266     protected function libToUpperCase($args)
5267     {
5268         $string = $this->coerceString($args[0]);
5269         $stringContent = $this->compileStringContent($string);
5270
5271         $string[2] = [function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)];
5272
5273         return $string;
5274     }
5275
5276     protected static $libFeatureExists = ['feature'];
5277     protected function libFeatureExists($args)
5278     {
5279         $string = $this->coerceString($args[0]);
5280         $name = $this->compileStringContent($string);
5281
5282         return $this->toBool(
5283             array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
5284         );
5285     }
5286
5287     protected static $libFunctionExists = ['name'];
5288     protected function libFunctionExists($args)
5289     {
5290         $string = $this->coerceString($args[0]);
5291         $name = $this->compileStringContent($string);
5292
5293         // user defined functions
5294         if ($this->has(static::$namespaces['function'] . $name)) {
5295             return true;
5296         }
5297
5298         $name = $this->normalizeName($name);
5299
5300         if (isset($this->userFunctions[$name])) {
5301             return true;
5302         }
5303
5304         // built-in functions
5305         $f = $this->getBuiltinFunction($name);
5306
5307         return $this->toBool(is_callable($f));
5308     }
5309
5310     protected static $libGlobalVariableExists = ['name'];
5311     protected function libGlobalVariableExists($args)
5312     {
5313         $string = $this->coerceString($args[0]);
5314         $name = $this->compileStringContent($string);
5315
5316         return $this->has($name, $this->rootEnv);
5317     }
5318
5319     protected static $libMixinExists = ['name'];
5320     protected function libMixinExists($args)
5321     {
5322         $string = $this->coerceString($args[0]);
5323         $name = $this->compileStringContent($string);
5324
5325         return $this->has(static::$namespaces['mixin'] . $name);
5326     }
5327
5328     protected static $libVariableExists = ['name'];
5329     protected function libVariableExists($args)
5330     {
5331         $string = $this->coerceString($args[0]);
5332         $name = $this->compileStringContent($string);
5333
5334         return $this->has($name);
5335     }
5336
5337     /**
5338      * Workaround IE7's content counter bug.
5339      *
5340      * @param array $args
5341      *
5342      * @return array
5343      */
5344     protected function libCounter($args)
5345     {
5346         $list = array_map([$this, 'compileValue'], $args);
5347
5348         return [HTML_Scss_Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
5349     }
5350
5351     protected static $libRandom = ['limit'];
5352     protected function libRandom($args)
5353     {
5354         if (isset($args[0])) {
5355             $n = $this->assertNumber($args[0]);
5356
5357             if ($n < 1) {
5358                 $this->throwError("limit must be greater than or equal to 1");
5359
5360                 return;
5361             }
5362
5363             return new HTML_Scss_Node_Number(mt_rand(1, $n), '');
5364         }
5365
5366         return new HTML_Scss_Node_Number(mt_rand(1, mt_getrandmax()), '');
5367     }
5368
5369     protected function libUniqueId()
5370     {
5371         static $id;
5372
5373         if (! isset($id)) {
5374             $id = mt_rand(0, pow(36, 8));
5375         }
5376
5377         $id += mt_rand(0, 10) + 1;
5378
5379         return [HTML_Scss_Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
5380     }
5381
5382     protected static $libInspect = ['value'];
5383     protected function libInspect($args)
5384     {
5385         if ($args[0] === static::$null) {
5386             return [HTML_Scss_Type::T_KEYWORD, 'null'];
5387         }
5388
5389         return $args[0];
5390     }
5391 }