HTML/Scss.php
[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         // see if tree is cached
3450         $realPath = realpath($path);
3451
3452         if (isset($this->importCache[$realPath])) {
3453             $this->handleImportLoop($realPath);
3454
3455             $tree = $this->importCache[$realPath];
3456         } else {
3457             $code   = file_get_contents($path);
3458             $parser = $this->parserFactory($path);
3459             $tree   = $parser->parse($code);
3460
3461             $this->importCache[$realPath] = $tree;
3462         }
3463
3464         $pi = pathinfo($path);
3465         array_unshift($this->importPaths, $pi['dirname']);
3466         $this->compileChildrenNoReturn($tree->children, $out);
3467         array_shift($this->importPaths);
3468     }
3469
3470     /**
3471      * Return the file path for an import url if it exists
3472      *
3473      * @api
3474      *
3475      * @param string $url
3476      *
3477      * @return string|null
3478      */
3479     public function findImport($url)
3480     {
3481         $urls = [];
3482
3483         // for "normal" scss imports (ignore vanilla css and external requests)
3484         if (! preg_match('/\.css$|^https?:\/\//', $url)) {
3485             // try both normal and the _partial filename
3486             $urls = [$url, preg_replace('/[^\/]+$/', '_\0', $url)];
3487         }
3488
3489         $hasExtension = preg_match('/[.]s?css$/', $url);
3490
3491         foreach ($this->importPaths as $dir) {
3492             if (is_string($dir)) {
3493                 // check urls for normal import paths
3494                 foreach ($urls as $full) {
3495                     $full = $dir
3496                         . (! empty($dir) && substr($dir, -1) !== '/' ? '/' : '')
3497                         . $full;
3498
3499                     if ($this->fileExists($file = $full . '.scss') ||
3500                         ($hasExtension && $this->fileExists($file = $full))
3501                     ) {
3502                         return $file;
3503                     }
3504                 }
3505             } elseif (is_callable($dir)) {
3506                 // check custom callback for import path
3507                 $file = call_user_func($dir, $url);
3508
3509                 if ($file !== null) {
3510                     return $file;
3511                 }
3512             }
3513         }
3514
3515         return null;
3516     }
3517
3518     /**
3519      * Set encoding
3520      *
3521      * @api
3522      *
3523      * @param string $encoding
3524      */
3525     public function setEncoding($encoding)
3526     {
3527         $this->encoding = $encoding;
3528     }
3529
3530     /**
3531      * Ignore errors?
3532      *
3533      * @api
3534      *
3535      * @param boolean $ignoreErrors
3536      *
3537      * @return \Leafo\ScssPhp\Compiler
3538      */
3539     public function setIgnoreErrors($ignoreErrors)
3540     {
3541         $this->ignoreErrors = $ignoreErrors;
3542     }
3543
3544     /**
3545      * Throw error (exception)
3546      *
3547      * @api
3548      *
3549      * @param string $msg Message with optional sprintf()-style vararg parameters
3550      *
3551      * @throws \Leafo\ScssPhp\Exception\CompilerException
3552      */
3553     public function throwError($msg)
3554     {
3555         if ($this->ignoreErrors) {
3556             return;
3557         }
3558
3559         if (func_num_args() > 1) {
3560             $msg = call_user_func_array('sprintf', func_get_args());
3561         }
3562
3563         $line = $this->sourceLine;
3564         $msg = "$msg: line: $line";
3565         require_once 'Scss/Exception/CompilerException.php';
3566         throw new HTML_Scss_Exception_CompilerException($msg);
3567     }
3568
3569     /**
3570      * Handle import loop
3571      *
3572      * @param string $name
3573      *
3574      * @throws \Exception
3575      */
3576     protected function handleImportLoop($name)
3577     {
3578         for ($env = $this->env; $env; $env = $env->parent) {
3579             $file = $this->sourceNames[$env->block->sourceIndex];
3580
3581             if (realpath($file) === $name) {
3582                 $this->throwError('An @import loop has been found: %s imports %s', $file, basename($file));
3583                 break;
3584             }
3585         }
3586     }
3587
3588     /**
3589      * Does file exist?
3590      *
3591      * @param string $name
3592      *
3593      * @return boolean
3594      */
3595     protected function fileExists($name)
3596     {
3597         return file_exists($name) && is_file($name);
3598     }
3599
3600     /**
3601      * Call SCSS @function
3602      *
3603      * @param string $name
3604      * @param array  $argValues
3605      * @param array  $returnValue
3606      *
3607      * @return boolean Returns true if returnValue is set; otherwise, false
3608      */
3609     protected function callScssFunction($name, $argValues, &$returnValue)
3610     {
3611         $func = $this->get(static::$namespaces['function'] . $name, false);
3612
3613         if (! $func) {
3614             return false;
3615         }
3616
3617         $this->pushEnv();
3618
3619         $storeEnv = $this->storeEnv;
3620         $this->storeEnv = $this->env;
3621
3622         // set the args
3623         if (isset($func->args)) {
3624             $this->applyArguments($func->args, $argValues);
3625         }
3626
3627         // throw away lines and children
3628         $tmp = new HTML_Scss_Formatter_OutputBlock;
3629         $tmp->lines    = [];
3630         $tmp->children = [];
3631
3632         $this->env->marker = 'function';
3633
3634         $ret = $this->compileChildren($func->children, $tmp);
3635
3636         $this->storeEnv = $storeEnv;
3637
3638         $this->popEnv();
3639
3640         $returnValue = ! isset($ret) ? static::$defaultValue : $ret;
3641
3642         return true;
3643     }
3644
3645     /**
3646      * Call built-in and registered (PHP) functions
3647      *
3648      * @param string $name
3649      * @param array  $args
3650      * @param array  $returnValue
3651      *
3652      * @return boolean Returns true if returnValue is set; otherwise, false
3653      */
3654     protected function callNativeFunction($name, $args, &$returnValue)
3655     {
3656         // try a lib function
3657         $name = $this->normalizeName($name);
3658
3659         if (isset($this->userFunctions[$name])) {
3660             // see if we can find a user function
3661             list($f, $prototype) = $this->userFunctions[$name];
3662         } elseif (($f = $this->getBuiltinFunction($name)) && is_callable($f)) {
3663             $libName   = $f[1];
3664             $prototype = isset(static::$$libName) ? static::$$libName : null;
3665         } else {
3666             return false;
3667         }
3668
3669         @list($sorted, $kwargs) = $this->sortArgs($prototype, $args);
3670
3671         if ($name !== 'if' && $name !== 'call') {
3672             foreach ($sorted as &$val) {
3673                 $val = $this->reduce($val, true);
3674             }
3675         }
3676
3677         $returnValue = call_user_func($f, $sorted, $kwargs);
3678
3679         if (! isset($returnValue)) {
3680             return false;
3681         }
3682
3683         $returnValue = $this->coerceValue($returnValue);
3684
3685         return true;
3686     }
3687
3688     /**
3689      * Get built-in function
3690      *
3691      * @param string $name Normalized name
3692      *
3693      * @return array
3694      */
3695     protected function getBuiltinFunction($name)
3696     {
3697         $libName = 'lib' . preg_replace_callback(
3698             '/_(.)/',
3699             function ($m) {
3700                 return ucfirst($m[1]);
3701             },
3702             ucfirst($name)
3703         );
3704
3705         return [$this, $libName];
3706     }
3707
3708     /**
3709      * Sorts keyword arguments
3710      *
3711      * @param array $prototype
3712      * @param array $args
3713      *
3714      * @return array
3715      */
3716     protected function sortArgs($prototype, $args)
3717     {
3718         $keyArgs = [];
3719         $posArgs = [];
3720
3721         // separate positional and keyword arguments
3722         foreach ($args as $arg) {
3723             list($key, $value) = $arg;
3724
3725             $key = $key[1];
3726
3727             if (empty($key)) {
3728                 $posArgs[] = empty($arg[2]) ? $value : $arg;
3729             } else {
3730                 $keyArgs[$key] = $value;
3731             }
3732         }
3733
3734         if (! isset($prototype)) {
3735             return [$posArgs, $keyArgs];
3736         }
3737
3738         // copy positional args
3739         $finalArgs = array_pad($posArgs, count($prototype), null);
3740
3741         // overwrite positional args with keyword args
3742         foreach ($prototype as $i => $names) {
3743             foreach ((array) $names as $name) {
3744                 if (isset($keyArgs[$name])) {
3745                     $finalArgs[$i] = $keyArgs[$name];
3746                 }
3747             }
3748         }
3749
3750         return [$finalArgs, $keyArgs];
3751     }
3752
3753     /**
3754      * Apply argument values per definition
3755      *
3756      * @param array $argDef
3757      * @param array $argValues
3758      *
3759      * @throws \Exception
3760      */
3761     protected function applyArguments($argDef, $argValues)
3762     {
3763         $storeEnv = $this->getStoreEnv();
3764
3765         $env = new HTML_Scss_Compiler_Environment;
3766         $env->store = $storeEnv->store;
3767
3768         $hasVariable = false;
3769         $args = [];
3770
3771         foreach ($argDef as $i => $arg) {
3772             list($name, $default, $isVariable) = $argDef[$i];
3773
3774             $args[$name] = [$i, $name, $default, $isVariable];
3775             $hasVariable |= $isVariable;
3776         }
3777
3778         $keywordArgs = [];
3779         $deferredKeywordArgs = [];
3780         $remaining = [];
3781
3782         // assign the keyword args
3783         foreach ((array) $argValues as $arg) {
3784             if (! empty($arg[0])) {
3785                 if (! isset($args[$arg[0][1]])) {
3786                     if ($hasVariable) {
3787                         $deferredKeywordArgs[$arg[0][1]] = $arg[1];
3788                     } else {
3789                         $this->throwError("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
3790                         break;
3791                     }
3792                 } elseif ($args[$arg[0][1]][0] < count($remaining)) {
3793                     $this->throwError("The argument $%s was passed both by position and by name.", $arg[0][1]);
3794                     break;
3795                 } else {
3796                     $keywordArgs[$arg[0][1]] = $arg[1];
3797                 }
3798             } elseif (count($keywordArgs)) {
3799                 $this->throwError('Positional arguments must come before keyword arguments.');
3800                 break;
3801             } elseif ($arg[2] === true) {
3802                 $val = $this->reduce($arg[1], true);
3803
3804                 if ($val[0] === HTML_Scss_Type::T_LIST) {
3805                     foreach ($val[2] as $name => $item) {
3806                         if (! is_numeric($name)) {
3807                             $keywordArgs[$name] = $item;
3808                         } else {
3809                             $remaining[] = $item;
3810                         }
3811                     }
3812                 } elseif ($val[0] === HTML_Scss_Type::T_MAP) {
3813                     foreach ($val[1] as $i => $name) {
3814                         $name = $this->compileStringContent($this->coerceString($name));
3815                         $item = $val[2][$i];
3816
3817                         if (! is_numeric($name)) {
3818                             $keywordArgs[$name] = $item;
3819                         } else {
3820                             $remaining[] = $item;
3821                         }
3822                     }
3823                 } else {
3824                     $remaining[] = $val;
3825                 }
3826             } else {
3827                 $remaining[] = $arg[1];
3828             }
3829         }
3830
3831         foreach ($args as $arg) {
3832             list($i, $name, $default, $isVariable) = $arg;
3833
3834             if ($isVariable) {
3835                 $val = [HTML_Scss_Type::T_LIST, ',', [], $isVariable];
3836
3837                 for ($count = count($remaining); $i < $count; $i++) {
3838                     $val[2][] = $remaining[$i];
3839                 }
3840
3841                 foreach ($deferredKeywordArgs as $itemName => $item) {
3842                     $val[2][$itemName] = $item;
3843                 }
3844             } elseif (isset($remaining[$i])) {
3845                 $val = $remaining[$i];
3846             } elseif (isset($keywordArgs[$name])) {
3847                 $val = $keywordArgs[$name];
3848             } elseif (! empty($default)) {
3849                 continue;
3850             } else {
3851                 $this->throwError("Missing argument $name");
3852                 break;
3853             }
3854
3855             $this->set($name, $this->reduce($val, true), true, $env);
3856         }
3857
3858         $storeEnv->store = $env->store;
3859
3860         foreach ($args as $arg) {
3861             list($i, $name, $default, $isVariable) = $arg;
3862
3863             if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
3864                 continue;
3865             }
3866
3867             $this->set($name, $this->reduce($default, true), true);
3868         }
3869     }
3870
3871     /**
3872      * Coerce a php value into a scss one
3873      *
3874      * @param mixed $value
3875      *
3876      * @return array|\Leafo\ScssPhp\Node\Number
3877      */
3878     private function coerceValue($value)
3879     {
3880         if (is_array($value) || $value instanceof  ArrayAccess) {
3881             return $value;
3882         }
3883
3884         if (is_bool($value)) {
3885             return $this->toBool($value);
3886         }
3887
3888         if ($value === null) {
3889             return static::$null;
3890         }
3891
3892         if (is_numeric($value)) {
3893             return new HTML_Scss_Node_Number($value, '');
3894         }
3895
3896         if ($value === '') {
3897             return static::$emptyString;
3898         }
3899
3900         if (preg_match('/^(#([0-9a-f]{6})|#([0-9a-f]{3}))$/i', $value, $m)) {
3901             $color = [HTML_Scss_Type::T_COLOR];
3902
3903             if (isset($m[3])) {
3904                 $num = hexdec($m[3]);
3905
3906                 foreach ([3, 2, 1] as $i) {
3907                     $t = $num & 0xf;
3908                     $color[$i] = $t << 4 | $t;
3909                     $num >>= 4;
3910                 }
3911             } else {
3912                 $num = hexdec($m[2]);
3913
3914                 foreach ([3, 2, 1] as $i) {
3915                     $color[$i] = $num & 0xff;
3916                     $num >>= 8;
3917                 }
3918             }
3919
3920             return $color;
3921         }
3922
3923         return [HTML_Scss_Type::T_KEYWORD, $value];
3924     }
3925
3926     /**
3927      * Coerce something to map
3928      *
3929      * @param array $item
3930      *
3931      * @return array
3932      */
3933     protected function coerceMap($item)
3934     {
3935         if ($item[0] === HTML_Scss_Type::T_MAP) {
3936             return $item;
3937         }
3938
3939         if ($item === static::$emptyList) {
3940             return static::$emptyMap;
3941         }
3942
3943         return [HTML_Scss_Type::T_MAP, [$item], [static::$null]];
3944     }
3945
3946     /**
3947      * Coerce something to list
3948      *
3949      * @param array  $item
3950      * @param string $delim
3951      *
3952      * @return array
3953      */
3954     protected function coerceList($item, $delim = ',')
3955     {
3956         if (isset($item) && $item[0] === HTML_Scss_Type::T_LIST) {
3957             return $item;
3958         }
3959
3960         if (isset($item) && $item[0] === HTML_Scss_Type::T_MAP) {
3961             $keys = $item[1];
3962             $values = $item[2];
3963             $list = [];
3964
3965             for ($i = 0, $s = count($keys); $i < $s; $i++) {
3966                 $key = $keys[$i];
3967                 $value = $values[$i];
3968
3969                 $list[] = [
3970                     HTML_Scss_Type::T_LIST,
3971                     '',
3972                     [[HTML_Scss_Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))], $value]
3973                 ];
3974             }
3975
3976             return [HTML_Scss_Type::T_LIST, ',', $list];
3977         }
3978
3979         return [HTML_Scss_Type::T_LIST, $delim, ! isset($item) ? []: [$item]];
3980     }
3981
3982     /**
3983      * Coerce color for expression
3984      *
3985      * @param array $value
3986      *
3987      * @return array|null
3988      */
3989     protected function coerceForExpression($value)
3990     {
3991         if ($color = $this->coerceColor($value)) {
3992             return $color;
3993         }
3994
3995         return $value;
3996     }
3997
3998     /**
3999      * Coerce value to color
4000      *
4001      * @param array $value
4002      *
4003      * @return array|null
4004      */
4005     protected function coerceColor($value)
4006     {
4007         switch ($value[0]) {
4008             case HTML_Scss_Type::T_COLOR:
4009                 return $value;
4010
4011             case HTML_Scss_Type::T_KEYWORD:
4012                 $name = strtolower($value[1]);
4013
4014                 if (isset(HTML_Scss_Colors::$cssColors[$name])) {
4015                     $rgba = explode(',', HTML_Scss_Colors::$cssColors[$name]);
4016
4017                     return isset($rgba[3])
4018                         ? [HTML_Scss_Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2], (int) $rgba[3]]
4019                         : [HTML_Scss_Type::T_COLOR, (int) $rgba[0], (int) $rgba[1], (int) $rgba[2]];
4020                 }
4021
4022                 return null;
4023         }
4024
4025         return null;
4026     }
4027
4028     /**
4029      * Coerce value to string
4030      *
4031      * @param array $value
4032      *
4033      * @return array|null
4034      */
4035     protected function coerceString($value)
4036     {
4037         if ($value[0] === HTML_Scss_Type::T_STRING) {
4038             return $value;
4039         }
4040
4041         return [HTML_Scss_Type::T_STRING, '', [$this->compileValue($value)]];
4042     }
4043
4044     /**
4045      * Coerce value to a percentage
4046      *
4047      * @param array $value
4048      *
4049      * @return integer|float
4050      */
4051     protected function coercePercent($value)
4052     {
4053         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4054             if (! empty($value[2]['%'])) {
4055                 return $value[1] / 100;
4056             }
4057
4058             return $value[1];
4059         }
4060
4061         return 0;
4062     }
4063
4064     /**
4065      * Assert value is a map
4066      *
4067      * @api
4068      *
4069      * @param array $value
4070      *
4071      * @return array
4072      *
4073      * @throws \Exception
4074      */
4075     public function assertMap($value)
4076     {
4077         $value = $this->coerceMap($value);
4078
4079         if ($value[0] !== HTML_Scss_Type::T_MAP) {
4080             $this->throwError('expecting map');
4081         }
4082
4083         return $value;
4084     }
4085
4086     /**
4087      * Assert value is a list
4088      *
4089      * @api
4090      *
4091      * @param array $value
4092      *
4093      * @return array
4094      *
4095      * @throws \Exception
4096      */
4097     public function assertList($value)
4098     {
4099         if ($value[0] !== HTML_Scss_Type::T_LIST) {
4100             $this->throwError('expecting list');
4101         }
4102
4103         return $value;
4104     }
4105
4106     /**
4107      * Assert value is a color
4108      *
4109      * @api
4110      *
4111      * @param array $value
4112      *
4113      * @return array
4114      *
4115      * @throws \Exception
4116      */
4117     public function assertColor($value)
4118     {
4119         if ($color = $this->coerceColor($value)) {
4120             return $color;
4121         }
4122
4123         $this->throwError('expecting color');
4124     }
4125
4126     /**
4127      * Assert value is a number
4128      *
4129      * @api
4130      *
4131      * @param array $value
4132      *
4133      * @return integer|float
4134      *
4135      * @throws \Exception
4136      */
4137     public function assertNumber($value)
4138     {
4139         if ($value[0] !== HTML_Scss_Type::T_NUMBER) {
4140             $this->throwError('expecting number');
4141         }
4142
4143         return $value[1];
4144     }
4145
4146     /**
4147      * Make sure a color's components don't go out of bounds
4148      *
4149      * @param array $c
4150      *
4151      * @return array
4152      */
4153     protected function fixColor($c)
4154     {
4155         foreach ([1, 2, 3] as $i) {
4156             if ($c[$i] < 0) {
4157                 $c[$i] = 0;
4158             }
4159
4160             if ($c[$i] > 255) {
4161                 $c[$i] = 255;
4162             }
4163         }
4164
4165         return $c;
4166     }
4167
4168     /**
4169      * Convert RGB to HSL
4170      *
4171      * @api
4172      *
4173      * @param integer $red
4174      * @param integer $green
4175      * @param integer $blue
4176      *
4177      * @return array
4178      */
4179     public function toHSL($red, $green, $blue)
4180     {
4181         $min = min($red, $green, $blue);
4182         $max = max($red, $green, $blue);
4183
4184         $l = $min + $max;
4185         $d = $max - $min;
4186
4187         if ((int) $d === 0) {
4188             $h = $s = 0;
4189         } else {
4190             if ($l < 255) {
4191                 $s = $d / $l;
4192             } else {
4193                 $s = $d / (510 - $l);
4194             }
4195
4196             if ($red == $max) {
4197                 $h = 60 * ($green - $blue) / $d;
4198             } elseif ($green == $max) {
4199                 $h = 60 * ($blue - $red) / $d + 120;
4200             } elseif ($blue == $max) {
4201                 $h = 60 * ($red - $green) / $d + 240;
4202             }
4203         }
4204
4205         return [HTML_Scss_Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
4206     }
4207
4208     /**
4209      * Hue to RGB helper
4210      *
4211      * @param float $m1
4212      * @param float $m2
4213      * @param float $h
4214      *
4215      * @return float
4216      */
4217     private function hueToRGB($m1, $m2, $h)
4218     {
4219         if ($h < 0) {
4220             $h += 1;
4221         } elseif ($h > 1) {
4222             $h -= 1;
4223         }
4224
4225         if ($h * 6 < 1) {
4226             return $m1 + ($m2 - $m1) * $h * 6;
4227         }
4228
4229         if ($h * 2 < 1) {
4230             return $m2;
4231         }
4232
4233         if ($h * 3 < 2) {
4234             return $m1 + ($m2 - $m1) * (2/3 - $h) * 6;
4235         }
4236
4237         return $m1;
4238     }
4239
4240     /**
4241      * Convert HSL to RGB
4242      *
4243      * @api
4244      *
4245      * @param integer $hue        H from 0 to 360
4246      * @param integer $saturation S from 0 to 100
4247      * @param integer $lightness  L from 0 to 100
4248      *
4249      * @return array
4250      */
4251     public function toRGB($hue, $saturation, $lightness)
4252     {
4253         if ($hue < 0) {
4254             $hue += 360;
4255         }
4256
4257         $h = $hue / 360;
4258         $s = min(100, max(0, $saturation)) / 100;
4259         $l = min(100, max(0, $lightness)) / 100;
4260
4261         $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
4262         $m1 = $l * 2 - $m2;
4263
4264         $r = $this->hueToRGB($m1, $m2, $h + 1/3) * 255;
4265         $g = $this->hueToRGB($m1, $m2, $h) * 255;
4266         $b = $this->hueToRGB($m1, $m2, $h - 1/3) * 255;
4267
4268         $out = [HTML_Scss_Type::T_COLOR, $r, $g, $b];
4269
4270         return $out;
4271     }
4272
4273     // Built in functions
4274
4275     //protected static $libCall = ['name', 'args...'];
4276     protected function libCall($args, $kwargs)
4277     {
4278         $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
4279
4280         $posArgs = [];
4281
4282         foreach ($args as $arg) {
4283             if (empty($arg[0])) {
4284                 if ($arg[2] === true) {
4285                     $tmp = $this->reduce($arg[1]);
4286
4287                     if ($tmp[0] === HTML_Scss_Type::T_LIST) {
4288                         foreach ($tmp[2] as $item) {
4289                             $posArgs[] = [null, $item, false];
4290                         }
4291                     } else {
4292                         $posArgs[] = [null, $tmp, true];
4293                     }
4294
4295                     continue;
4296                 }
4297
4298                 $posArgs[] = [null, $this->reduce($arg), false];
4299                 continue;
4300             }
4301
4302             $posArgs[] = [null, $arg, false];
4303         }
4304
4305         if (count($kwargs)) {
4306             foreach ($kwargs as $key => $value) {
4307                 $posArgs[] = [[HTML_Scss_Type::T_VARIABLE, $key], $value, false];
4308             }
4309         }
4310
4311         return $this->reduce([HTML_Scss_Type::T_FUNCTION_CALL, $name, $posArgs]);
4312     }
4313
4314     protected static $libIf = ['condition', 'if-true', 'if-false'];
4315     protected function libIf($args)
4316     {
4317         list($cond, $t, $f) = $args;
4318
4319         if (! $this->isTruthy($this->reduce($cond, true))) {
4320             return $this->reduce($f, true);
4321         }
4322
4323         return $this->reduce($t, true);
4324     }
4325
4326     protected static $libIndex = ['list', 'value'];
4327     protected function libIndex($args)
4328     {
4329         list($list, $value) = $args;
4330
4331         if ($value[0] === HTML_Scss_Type::T_MAP) {
4332             return static::$null;
4333         }
4334
4335         if ($list[0] === HTML_Scss_Type::T_MAP ||
4336             $list[0] === HTML_Scss_Type::T_STRING ||
4337             $list[0] === HTML_Scss_Type::T_KEYWORD ||
4338             $list[0] === HTML_Scss_Type::T_INTERPOLATE
4339         ) {
4340             $list = $this->coerceList($list, ' ');
4341         }
4342
4343         if ($list[0] !== HTML_Scss_Type::T_LIST) {
4344             return static::$null;
4345         }
4346
4347         $values = [];
4348
4349         foreach ($list[2] as $item) {
4350             $values[] = $this->normalizeValue($item);
4351         }
4352
4353         $key = array_search($this->normalizeValue($value), $values);
4354
4355         return false === $key ? static::$null : $key + 1;
4356     }
4357
4358     protected static $libRgb = ['red', 'green', 'blue'];
4359     protected function libRgb($args)
4360     {
4361         list($r, $g, $b) = $args;
4362
4363         return [HTML_Scss_Type::T_COLOR, $r[1], $g[1], $b[1]];
4364     }
4365
4366     protected static $libRgba = [
4367         ['red', 'color'],
4368         'green', 'blue', 'alpha'];
4369     protected function libRgba($args)
4370     {
4371         if ($color = $this->coerceColor($args[0])) {
4372             $num = isset($args[3]) ? $args[3] : $args[1];
4373             $alpha = $this->assertNumber($num);
4374             $color[4] = $alpha;
4375
4376             return $color;
4377         }
4378
4379         list($r, $g, $b, $a) = $args;
4380
4381         return [HTML_Scss_Type::T_COLOR, $r[1], $g[1], $b[1], $a[1]];
4382     }
4383
4384     // helper function for adjust_color, change_color, and scale_color
4385     protected function alterColor($args, $fn)
4386     {
4387         $color = $this->assertColor($args[0]);
4388
4389         foreach ([1, 2, 3, 7] as $i) {
4390             if (isset($args[$i])) {
4391                 $val = $this->assertNumber($args[$i]);
4392                 $ii = $i === 7 ? 4 : $i; // alpha
4393                 $color[$ii] = call_user_func($fn, isset($color[$ii]) ? $color[$ii] : 0, $val, $i);
4394             }
4395         }
4396
4397         if (isset($args[4]) || isset($args[5]) || isset($args[6])) {
4398             $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4399
4400             foreach ([4, 5, 6] as $i) {
4401                 if (isset($args[$i])) {
4402                     $val = $this->assertNumber($args[$i]);
4403                     $hsl[$i - 3] = call_user_func($fn, $hsl[$i - 3], $val, $i);
4404                 }
4405             }
4406
4407             $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
4408
4409             if (isset($color[4])) {
4410                 $rgb[4] = $color[4];
4411             }
4412
4413             $color = $rgb;
4414         }
4415
4416         return $color;
4417     }
4418
4419     protected static $libAdjustColor = [
4420         'color', 'red', 'green', 'blue',
4421         'hue', 'saturation', 'lightness', 'alpha'
4422     ];
4423     protected function libAdjustColor($args)
4424     {
4425         return $this->alterColor($args, function ($base, $alter, $i) {
4426             return $base + $alter;
4427         });
4428     }
4429
4430     protected static $libChangeColor = [
4431         'color', 'red', 'green', 'blue',
4432         'hue', 'saturation', 'lightness', 'alpha'
4433     ];
4434     protected function libChangeColor($args)
4435     {
4436         return $this->alterColor($args, function ($base, $alter, $i) {
4437             return $alter;
4438         });
4439     }
4440
4441     protected static $libScaleColor = [
4442         'color', 'red', 'green', 'blue',
4443         'hue', 'saturation', 'lightness', 'alpha'
4444     ];
4445     protected function libScaleColor($args)
4446     {
4447         return $this->alterColor($args, function ($base, $scale, $i) {
4448             // 1, 2, 3 - rgb
4449             // 4, 5, 6 - hsl
4450             // 7 - a
4451             switch ($i) {
4452                 case 1:
4453                 case 2:
4454                 case 3:
4455                     $max = 255;
4456                     break;
4457
4458                 case 4:
4459                     $max = 360;
4460                     break;
4461
4462                 case 7:
4463                     $max = 1;
4464                     break;
4465
4466                 default:
4467                     $max = 100;
4468             }
4469
4470             $scale = $scale / 100;
4471
4472             if ($scale < 0) {
4473                 return $base * $scale + $base;
4474             }
4475
4476             return ($max - $base) * $scale + $base;
4477         });
4478     }
4479
4480     protected static $libIeHexStr = ['color'];
4481     protected function libIeHexStr($args)
4482     {
4483         $color = $this->coerceColor($args[0]);
4484         $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
4485
4486         return sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3]);
4487     }
4488
4489     protected static $libRed = ['color'];
4490     protected function libRed($args)
4491     {
4492         $color = $this->coerceColor($args[0]);
4493
4494         return $color[1];
4495     }
4496
4497     protected static $libGreen = ['color'];
4498     protected function libGreen($args)
4499     {
4500         $color = $this->coerceColor($args[0]);
4501
4502         return $color[2];
4503     }
4504
4505     protected static $libBlue = ['color'];
4506     protected function libBlue($args)
4507     {
4508         $color = $this->coerceColor($args[0]);
4509
4510         return $color[3];
4511     }
4512
4513     protected static $libAlpha = ['color'];
4514     protected function libAlpha($args)
4515     {
4516         if ($color = $this->coerceColor($args[0])) {
4517             return isset($color[4]) ? $color[4] : 1;
4518         }
4519
4520         // this might be the IE function, so return value unchanged
4521         return null;
4522     }
4523
4524     protected static $libOpacity = ['color'];
4525     protected function libOpacity($args)
4526     {
4527         $value = $args[0];
4528
4529         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4530             return null;
4531         }
4532
4533         return $this->libAlpha($args);
4534     }
4535
4536     // mix two colors
4537     protected static $libMix = ['color-1', 'color-2', 'weight'];
4538     protected function libMix($args)
4539     {
4540         list($first, $second, $weight) = $args;
4541
4542         $first = $this->assertColor($first);
4543         $second = $this->assertColor($second);
4544
4545         if (! isset($weight)) {
4546             $weight = 0.5;
4547         } else {
4548             $weight = $this->coercePercent($weight);
4549         }
4550
4551         $firstAlpha = isset($first[4]) ? $first[4] : 1;
4552         $secondAlpha = isset($second[4]) ? $second[4] : 1;
4553
4554         $w = $weight * 2 - 1;
4555         $a = $firstAlpha - $secondAlpha;
4556
4557         $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
4558         $w2 = 1.0 - $w1;
4559
4560         $new = [HTML_Scss_Type::T_COLOR,
4561             $w1 * $first[1] + $w2 * $second[1],
4562             $w1 * $first[2] + $w2 * $second[2],
4563             $w1 * $first[3] + $w2 * $second[3],
4564         ];
4565
4566         if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
4567             $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight);
4568         }
4569
4570         return $this->fixColor($new);
4571     }
4572
4573     protected static $libHsl = ['hue', 'saturation', 'lightness'];
4574     protected function libHsl($args)
4575     {
4576         list($h, $s, $l) = $args;
4577
4578         return $this->toRGB($h[1], $s[1], $l[1]);
4579     }
4580
4581     protected static $libHsla = ['hue', 'saturation', 'lightness', 'alpha'];
4582     protected function libHsla($args)
4583     {
4584         list($h, $s, $l, $a) = $args;
4585
4586         $color = $this->toRGB($h[1], $s[1], $l[1]);
4587         $color[4] = $a[1];
4588
4589         return $color;
4590     }
4591
4592     protected static $libHue = ['color'];
4593     protected function libHue($args)
4594     {
4595         $color = $this->assertColor($args[0]);
4596         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4597
4598         return new HTML_Scss_Node_Number($hsl[1], 'deg');
4599     }
4600
4601     protected static $libSaturation = ['color'];
4602     protected function libSaturation($args)
4603     {
4604         $color = $this->assertColor($args[0]);
4605         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4606
4607         return new HTML_Scss_Node_Number($hsl[2], '%');
4608     }
4609
4610     protected static $libLightness = ['color'];
4611     protected function libLightness($args)
4612     {
4613         $color = $this->assertColor($args[0]);
4614         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4615
4616         return new HTML_Scss_Node_Number($hsl[3], '%');
4617     }
4618
4619     protected function adjustHsl($color, $idx, $amount)
4620     {
4621         $hsl = $this->toHSL($color[1], $color[2], $color[3]);
4622         $hsl[$idx] += $amount;
4623         $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
4624
4625         if (isset($color[4])) {
4626             $out[4] = $color[4];
4627         }
4628
4629         return $out;
4630     }
4631
4632     protected static $libAdjustHue = ['color', 'degrees'];
4633     protected function libAdjustHue($args)
4634     {
4635         $color = $this->assertColor($args[0]);
4636         $degrees = $this->assertNumber($args[1]);
4637
4638         return $this->adjustHsl($color, 1, $degrees);
4639     }
4640
4641     protected static $libLighten = ['color', 'amount'];
4642     protected function libLighten($args)
4643     {
4644         $color = $this->assertColor($args[0]);
4645         require_once 'Scss/Base/Range.php';
4646         $amount = HTML_Scss_Util::checkRange('amount', new HTML_Scss_Base_Range(0, 100), $args[1], '%');
4647
4648         return $this->adjustHsl($color, 3, $amount);
4649     }
4650
4651     protected static $libDarken = ['color', 'amount'];
4652     protected function libDarken($args)
4653     {
4654         $color = $this->assertColor($args[0]);
4655         require_once 'Scss/Base/Range.php';
4656         $amount = HTML_Scss_Util::checkRange('amount', new HTML_Scss_Base_Range(0, 100), $args[1], '%');
4657
4658         return $this->adjustHsl($color, 3, -$amount);
4659     }
4660
4661     protected static $libSaturate = ['color', 'amount'];
4662     protected function libSaturate($args)
4663     {
4664         $value = $args[0];
4665
4666         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4667             return null;
4668         }
4669
4670         $color = $this->assertColor($value);
4671         $amount = 100 * $this->coercePercent($args[1]);
4672
4673         return $this->adjustHsl($color, 2, $amount);
4674     }
4675
4676     protected static $libDesaturate = ['color', 'amount'];
4677     protected function libDesaturate($args)
4678     {
4679         $color = $this->assertColor($args[0]);
4680         $amount = 100 * $this->coercePercent($args[1]);
4681
4682         return $this->adjustHsl($color, 2, -$amount);
4683     }
4684
4685     protected static $libGrayscale = ['color'];
4686     protected function libGrayscale($args)
4687     {
4688         $value = $args[0];
4689
4690         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4691             return null;
4692         }
4693
4694         return $this->adjustHsl($this->assertColor($value), 2, -100);
4695     }
4696
4697     protected static $libComplement = ['color'];
4698     protected function libComplement($args)
4699     {
4700         return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
4701     }
4702
4703     protected static $libInvert = ['color'];
4704     protected function libInvert($args)
4705     {
4706         $value = $args[0];
4707
4708         if ($value[0] === HTML_Scss_Type::T_NUMBER) {
4709             return null;
4710         }
4711
4712         $color = $this->assertColor($value);
4713         $color[1] = 255 - $color[1];
4714         $color[2] = 255 - $color[2];
4715         $color[3] = 255 - $color[3];
4716
4717         return $color;
4718     }
4719
4720     // increases opacity by amount
4721     protected static $libOpacify = ['color', 'amount'];
4722     protected function libOpacify($args)
4723     {
4724         $color = $this->assertColor($args[0]);
4725         $amount = $this->coercePercent($args[1]);
4726
4727         $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
4728         $color[4] = min(1, max(0, $color[4]));
4729
4730         return $color;
4731     }
4732
4733     protected static $libFadeIn = ['color', 'amount'];
4734     protected function libFadeIn($args)
4735     {
4736         return $this->libOpacify($args);
4737     }
4738
4739     // decreases opacity by amount
4740     protected static $libTransparentize = ['color', 'amount'];
4741     protected function libTransparentize($args)
4742     {
4743         $color = $this->assertColor($args[0]);
4744         $amount = $this->coercePercent($args[1]);
4745
4746         $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
4747         $color[4] = min(1, max(0, $color[4]));
4748
4749         return $color;
4750     }
4751
4752     protected static $libFadeOut = ['color', 'amount'];
4753     protected function libFadeOut($args)
4754     {
4755         return $this->libTransparentize($args);
4756     }
4757
4758     protected static $libUnquote = ['string'];
4759     protected function libUnquote($args)
4760     {
4761         $str = $args[0];
4762
4763         if ($str[0] === HTML_Scss_Type::T_STRING) {
4764             $str[1] = '';
4765         }
4766
4767         return $str;
4768     }
4769
4770     protected static $libQuote = ['string'];
4771     protected function libQuote($args)
4772     {
4773         $value = $args[0];
4774
4775         if ($value[0] === HTML_Scss_Type::T_STRING && ! empty($value[1])) {
4776             return $value;
4777         }
4778
4779         return [HTML_Scss_Type::T_STRING, '"', [$value]];
4780     }
4781
4782     protected static $libPercentage = ['value'];
4783     protected function libPercentage($args)
4784     {
4785         return new HTML_Scss_Node_Number($this->coercePercent($args[0]) * 100, '%');
4786     }
4787
4788     protected static $libRound = ['value'];
4789     protected function libRound($args)
4790     {
4791         $num = $args[0];
4792
4793         return new HTML_Scss_Node_Number(round($num[1]), $num[2]);
4794     }
4795
4796     protected static $libFloor = ['value'];
4797     protected function libFloor($args)
4798     {
4799         $num = $args[0];
4800
4801         return new HTML_Scss_Node_Number(floor($num[1]), $num[2]);
4802     }
4803
4804     protected static $libCeil = ['value'];
4805     protected function libCeil($args)
4806     {
4807         $num = $args[0];
4808
4809         return new HTML_Scss_Node_Number(ceil($num[1]), $num[2]);
4810     }
4811
4812     protected static $libAbs = ['value'];
4813     protected function libAbs($args)
4814     {
4815         $num = $args[0];
4816
4817         return new HTML_Scss_Node_Number(abs($num[1]), $num[2]);
4818     }
4819
4820     protected function libMin($args)
4821     {
4822         $numbers = $this->getNormalizedNumbers($args);
4823         $min = null;
4824
4825         foreach ($numbers as $key => $number) {
4826             if (null === $min || $number[1] <= $min[1]) {
4827                 $min = [$key, $number[1]];
4828             }
4829         }
4830
4831         return $args[$min[0]];
4832     }
4833
4834     protected function libMax($args)
4835     {
4836         $numbers = $this->getNormalizedNumbers($args);
4837         $max = null;
4838
4839         foreach ($numbers as $key => $number) {
4840             if (null === $max || $number[1] >= $max[1]) {
4841                 $max = [$key, $number[1]];
4842             }
4843         }
4844
4845         return $args[$max[0]];
4846     }
4847
4848     /**
4849      * Helper to normalize args containing numbers
4850      *
4851      * @param array $args
4852      *
4853      * @return array
4854      */
4855     protected function getNormalizedNumbers($args)
4856     {
4857         $unit = null;
4858         $originalUnit = null;
4859         $numbers = [];
4860
4861         foreach ($args as $key => $item) {
4862             if ($item[0] !== HTML_Scss_Type::T_NUMBER) {
4863                 $this->throwError('%s is not a number', $item[0]);
4864                 break;
4865             }
4866
4867             $number = $item->normalize();
4868
4869             if (null === $unit) {
4870                 $unit = $number[2];
4871                 $originalUnit = $item->unitStr();
4872             } elseif ($number[1] && $unit !== $number[2]) {
4873                 $this->throwError('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
4874                 break;
4875             }
4876
4877             $numbers[$key] = $number;
4878         }
4879
4880         return $numbers;
4881     }
4882
4883     protected static $libLength = ['list'];
4884     protected function libLength($args)
4885     {
4886         $list = $this->coerceList($args[0]);
4887
4888         return count($list[2]);
4889     }
4890
4891     //protected static $libListSeparator = ['list...'];
4892     protected function libListSeparator($args)
4893     {
4894         if (count($args) > 1) {
4895             return 'comma';
4896         }
4897
4898         $list = $this->coerceList($args[0]);
4899
4900         if (count($list[2]) <= 1) {
4901             return 'space';
4902         }
4903
4904         if ($list[1] === ',') {
4905             return 'comma';
4906         }
4907
4908         return 'space';
4909     }
4910
4911     protected static $libNth = ['list', 'n'];
4912     protected function libNth($args)
4913     {
4914         $list = $this->coerceList($args[0]);
4915         $n = $this->assertNumber($args[1]);
4916
4917         if ($n > 0) {
4918             $n--;
4919         } elseif ($n < 0) {
4920             $n += count($list[2]);
4921         }
4922
4923         return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
4924     }
4925
4926     protected static $libSetNth = ['list', 'n', 'value'];
4927     protected function libSetNth($args)
4928     {
4929         $list = $this->coerceList($args[0]);
4930         $n = $this->assertNumber($args[1]);
4931
4932         if ($n > 0) {
4933             $n--;
4934         } elseif ($n < 0) {
4935             $n += count($list[2]);
4936         }
4937
4938         if (! isset($list[2][$n])) {
4939             $this->throwError('Invalid argument for "n"');
4940
4941             return;
4942         }
4943
4944         $list[2][$n] = $args[2];
4945
4946         return $list;
4947     }
4948
4949     protected static $libMapGet = ['map', 'key'];
4950     protected function libMapGet($args)
4951     {
4952         $map = $this->assertMap($args[0]);
4953         $key = $this->compileStringContent($this->coerceString($args[1]));
4954
4955         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4956             if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4957                 return $map[2][$i];
4958             }
4959         }
4960
4961         return static::$null;
4962     }
4963
4964     protected static $libMapKeys = ['map'];
4965     protected function libMapKeys($args)
4966     {
4967         $map = $this->assertMap($args[0]);
4968         $keys = $map[1];
4969
4970         return [HTML_Scss_Type::T_LIST, ',', $keys];
4971     }
4972
4973     protected static $libMapValues = ['map'];
4974     protected function libMapValues($args)
4975     {
4976         $map = $this->assertMap($args[0]);
4977         $values = $map[2];
4978
4979         return [HTML_Scss_Type::T_LIST, ',', $values];
4980     }
4981
4982     protected static $libMapRemove = ['map', 'key'];
4983     protected function libMapRemove($args)
4984     {
4985         $map = $this->assertMap($args[0]);
4986         $key = $this->compileStringContent($this->coerceString($args[1]));
4987
4988         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
4989             if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
4990                 array_splice($map[1], $i, 1);
4991                 array_splice($map[2], $i, 1);
4992             }
4993         }
4994
4995         return $map;
4996     }
4997
4998     protected static $libMapHasKey = ['map', 'key'];
4999     protected function libMapHasKey($args)
5000     {
5001         $map = $this->assertMap($args[0]);
5002         $key = $this->compileStringContent($this->coerceString($args[1]));
5003
5004         for ($i = count($map[1]) - 1; $i >= 0; $i--) {
5005             if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
5006                 return true;
5007             }
5008         }
5009
5010         return false;
5011     }
5012
5013     protected static $libMapMerge = ['map-1', 'map-2'];
5014     protected function libMapMerge($args)
5015     {
5016         $map1 = $this->assertMap($args[0]);
5017         $map2 = $this->assertMap($args[1]);
5018
5019         foreach ($map2[1] as $i2 => $key2) {
5020             $key = $this->compileStringContent($this->coerceString($key2));
5021
5022             foreach ($map1[1] as $i1 => $key1) {
5023                 if ($key === $this->compileStringContent($this->coerceString($key1))) {
5024                     $map1[2][$i1] = $map2[2][$i2];
5025                     continue 2;
5026                 }
5027             }
5028
5029             $map1[1][] = $map2[1][$i2];
5030             $map1[2][] = $map2[2][$i2];
5031         }
5032
5033         return $map1;
5034     }
5035
5036     protected static $libKeywords = ['args'];
5037     protected function libKeywords($args)
5038     {
5039         $this->assertList($args[0]);
5040
5041         $keys = [];
5042         $values = [];
5043
5044         foreach ($args[0][2] as $name => $arg) {
5045             $keys[] = [HTML_Scss_Type::T_KEYWORD, $name];
5046             $values[] = $arg;
5047         }
5048
5049         return [HTML_Scss_Type::T_MAP, $keys, $values];
5050     }
5051
5052     protected function listSeparatorForJoin($list1, $sep)
5053     {
5054         if (! isset($sep)) {
5055             return $list1[1];
5056         }
5057
5058         switch ($this->compileValue($sep)) {
5059             case 'comma':
5060                 return ',';
5061
5062             case 'space':
5063                 return '';
5064
5065             default:
5066                 return $list1[1];
5067         }
5068     }
5069
5070     protected static $libJoin = ['list1', 'list2', 'separator'];
5071     protected function libJoin($args)
5072     {
5073         list($list1, $list2, $sep) = $args;
5074
5075         $list1 = $this->coerceList($list1, ' ');
5076         $list2 = $this->coerceList($list2, ' ');
5077         $sep = $this->listSeparatorForJoin($list1, $sep);
5078
5079         return [HTML_Scss_Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
5080     }
5081
5082     protected static $libAppend = ['list', 'val', 'separator'];
5083     protected function libAppend($args)
5084     {
5085         list($list1, $value, $sep) = $args;
5086
5087         $list1 = $this->coerceList($list1, ' ');
5088         $sep = $this->listSeparatorForJoin($list1, $sep);
5089
5090         return [HTML_Scss_Type::T_LIST, $sep, array_merge($list1[2], [$value])];
5091     }
5092
5093     protected function libZip($args)
5094     {
5095         foreach ($args as $arg) {
5096             $this->assertList($arg);
5097         }
5098
5099         $lists = [];
5100         $firstList = array_shift($args);
5101
5102         foreach ($firstList[2] as $key => $item) {
5103             $list = [HTML_Scss_Type::T_LIST, '', [$item]];
5104
5105             foreach ($args as $arg) {
5106                 if (isset($arg[2][$key])) {
5107                     $list[2][] = $arg[2][$key];
5108                 } else {
5109                     break 2;
5110                 }
5111             }
5112
5113             $lists[] = $list;
5114         }
5115
5116         return [HTML_Scss_Type::T_LIST, ',', $lists];
5117     }
5118
5119     protected static $libTypeOf = ['value'];
5120     protected function libTypeOf($args)
5121     {
5122         $value = $args[0];
5123
5124         switch ($value[0]) {
5125             case HTML_Scss_Type::T_KEYWORD:
5126                 if ($value === static::$true || $value === static::$false) {
5127                     return 'bool';
5128                 }
5129
5130                 if ($this->coerceColor($value)) {
5131                     return 'color';
5132                 }
5133
5134                 // fall-thru
5135             case HTML_Scss_Type::T_FUNCTION:
5136                 return 'string';
5137
5138             case HTML_Scss_Type::T_LIST:
5139                 if (isset($value[3]) && $value[3]) {
5140                     return 'arglist';
5141                 }
5142
5143                 // fall-thru
5144             default:
5145                 return $value[0];
5146         }
5147     }
5148
5149     protected static $libUnit = ['number'];
5150     protected function libUnit($args)
5151     {
5152         $num = $args[0];
5153
5154         if ($num[0] === HTML_Scss_Type::T_NUMBER) {
5155             return [HTML_Scss_Type::T_STRING, '"', [$num->unitStr()]];
5156         }
5157
5158         return '';
5159     }
5160
5161     protected static $libUnitless = ['number'];
5162     protected function libUnitless($args)
5163     {
5164         $value = $args[0];
5165
5166         return $value[0] === HTML_Scss_Type::T_NUMBER && $value->unitless();
5167     }
5168
5169     protected static $libComparable = ['number-1', 'number-2'];
5170     protected function libComparable($args)
5171     {
5172         list($number1, $number2) = $args;
5173
5174         if (! isset($number1[0]) || $number1[0] !== HTML_Scss_Type::T_NUMBER ||
5175             ! isset($number2[0]) || $number2[0] !== HTML_Scss_Type::T_NUMBER
5176         ) {
5177             $this->throwError('Invalid argument(s) for "comparable"');
5178
5179             return;
5180         }
5181
5182         $number1 = $number1->normalize();
5183         $number2 = $number2->normalize();
5184
5185         return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
5186     }
5187
5188     protected static $libStrIndex = ['string', 'substring'];
5189     protected function libStrIndex($args)
5190     {
5191         $string = $this->coerceString($args[0]);
5192         $stringContent = $this->compileStringContent($string);
5193
5194         $substring = $this->coerceString($args[1]);
5195         $substringContent = $this->compileStringContent($substring);
5196
5197         $result = strpos($stringContent, $substringContent);
5198
5199         return $result === false ? static::$null : new HTML_Scss_Node_Number($result + 1, '');
5200     }
5201
5202     protected static $libStrInsert = ['string', 'insert', 'index'];
5203     protected function libStrInsert($args)
5204     {
5205         $string = $this->coerceString($args[0]);
5206         $stringContent = $this->compileStringContent($string);
5207
5208         $insert = $this->coerceString($args[1]);
5209         $insertContent = $this->compileStringContent($insert);
5210
5211         list(, $index) = $args[2];
5212
5213         $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
5214
5215         return $string;
5216     }
5217
5218     protected static $libStrLength = ['string'];
5219     protected function libStrLength($args)
5220     {
5221         $string = $this->coerceString($args[0]);
5222         $stringContent = $this->compileStringContent($string);
5223
5224         return new HTML_Scss_Node_Number(strlen($stringContent), '');
5225     }
5226
5227     protected static $libStrSlice = ['string', 'start-at', 'end-at'];
5228     protected function libStrSlice($args)
5229     {
5230         if (isset($args[2]) && $args[2][1] == 0) {
5231             return static::$nullString;
5232         }
5233
5234         $string = $this->coerceString($args[0]);
5235         $stringContent = $this->compileStringContent($string);
5236
5237         $start = (int) $args[1][1];
5238
5239         if ($start > 0) {
5240             $start--;
5241         }
5242
5243         $end    = (int) $args[2][1];
5244         $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
5245
5246         $string[2] = $length
5247             ? [substr($stringContent, $start, $length)]
5248             : [substr($stringContent, $start)];
5249
5250         return $string;
5251     }
5252
5253     protected static $libToLowerCase = ['string'];
5254     protected function libToLowerCase($args)
5255     {
5256         $string = $this->coerceString($args[0]);
5257         $stringContent = $this->compileStringContent($string);
5258
5259         $string[2] = [function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)];
5260
5261         return $string;
5262     }
5263
5264     protected static $libToUpperCase = ['string'];
5265     protected function libToUpperCase($args)
5266     {
5267         $string = $this->coerceString($args[0]);
5268         $stringContent = $this->compileStringContent($string);
5269
5270         $string[2] = [function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)];
5271
5272         return $string;
5273     }
5274
5275     protected static $libFeatureExists = ['feature'];
5276     protected function libFeatureExists($args)
5277     {
5278         $string = $this->coerceString($args[0]);
5279         $name = $this->compileStringContent($string);
5280
5281         return $this->toBool(
5282             array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
5283         );
5284     }
5285
5286     protected static $libFunctionExists = ['name'];
5287     protected function libFunctionExists($args)
5288     {
5289         $string = $this->coerceString($args[0]);
5290         $name = $this->compileStringContent($string);
5291
5292         // user defined functions
5293         if ($this->has(static::$namespaces['function'] . $name)) {
5294             return true;
5295         }
5296
5297         $name = $this->normalizeName($name);
5298
5299         if (isset($this->userFunctions[$name])) {
5300             return true;
5301         }
5302
5303         // built-in functions
5304         $f = $this->getBuiltinFunction($name);
5305
5306         return $this->toBool(is_callable($f));
5307     }
5308
5309     protected static $libGlobalVariableExists = ['name'];
5310     protected function libGlobalVariableExists($args)
5311     {
5312         $string = $this->coerceString($args[0]);
5313         $name = $this->compileStringContent($string);
5314
5315         return $this->has($name, $this->rootEnv);
5316     }
5317
5318     protected static $libMixinExists = ['name'];
5319     protected function libMixinExists($args)
5320     {
5321         $string = $this->coerceString($args[0]);
5322         $name = $this->compileStringContent($string);
5323
5324         return $this->has(static::$namespaces['mixin'] . $name);
5325     }
5326
5327     protected static $libVariableExists = ['name'];
5328     protected function libVariableExists($args)
5329     {
5330         $string = $this->coerceString($args[0]);
5331         $name = $this->compileStringContent($string);
5332
5333         return $this->has($name);
5334     }
5335
5336     /**
5337      * Workaround IE7's content counter bug.
5338      *
5339      * @param array $args
5340      *
5341      * @return array
5342      */
5343     protected function libCounter($args)
5344     {
5345         $list = array_map([$this, 'compileValue'], $args);
5346
5347         return [HTML_Scss_Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
5348     }
5349
5350     protected static $libRandom = ['limit'];
5351     protected function libRandom($args)
5352     {
5353         if (isset($args[0])) {
5354             $n = $this->assertNumber($args[0]);
5355
5356             if ($n < 1) {
5357                 $this->throwError("limit must be greater than or equal to 1");
5358
5359                 return;
5360             }
5361
5362             return new HTML_Scss_Node_Number(mt_rand(1, $n), '');
5363         }
5364
5365         return new HTML_Scss_Node_Number(mt_rand(1, mt_getrandmax()), '');
5366     }
5367
5368     protected function libUniqueId()
5369     {
5370         static $id;
5371
5372         if (! isset($id)) {
5373             $id = mt_rand(0, pow(36, 8));
5374         }
5375
5376         $id += mt_rand(0, 10) + 1;
5377
5378         return [HTML_Scss_Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
5379     }
5380
5381     protected static $libInspect = ['value'];
5382     protected function libInspect($args)
5383     {
5384         if ($args[0] === static::$null) {
5385             return [HTML_Scss_Type::T_KEYWORD, 'null'];
5386         }
5387
5388         return $args[0];
5389     }
5390 }