75c5d675282cc05e59b1dfdad5e0f85f41b77c68
[moodle.git] / lib / scssphp / Compiler.php
1 <?php
2 /**
3  * SCSSPHP
4  *
5  * @copyright 2012-2019 Leaf Corcoran
6  *
7  * @license http://opensource.org/licenses/MIT MIT
8  *
9  * @link http://scssphp.github.io/scssphp
10  */
12 namespace ScssPhp\ScssPhp;
14 use ScssPhp\ScssPhp\Base\Range;
15 use ScssPhp\ScssPhp\Block;
16 use ScssPhp\ScssPhp\Cache;
17 use ScssPhp\ScssPhp\Colors;
18 use ScssPhp\ScssPhp\Compiler\Environment;
19 use ScssPhp\ScssPhp\Exception\CompilerException;
20 use ScssPhp\ScssPhp\Formatter\OutputBlock;
21 use ScssPhp\ScssPhp\Node;
22 use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
23 use ScssPhp\ScssPhp\Type;
24 use ScssPhp\ScssPhp\Parser;
25 use ScssPhp\ScssPhp\Util;
27 /**
28  * The scss compiler and parser.
29  *
30  * Converting SCSS to CSS is a three stage process. The incoming file is parsed
31  * by `Parser` into a syntax tree, then it is compiled into another tree
32  * representing the CSS structure by `Compiler`. The CSS tree is fed into a
33  * formatter, like `Formatter` which then outputs CSS as a string.
34  *
35  * During the first compile, all values are *reduced*, which means that their
36  * types are brought to the lowest form before being dump as strings. This
37  * handles math equations, variable dereferences, and the like.
38  *
39  * The `compile` function of `Compiler` is the entry point.
40  *
41  * In summary:
42  *
43  * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
44  * then transforms the resulting tree to a CSS tree. This class also holds the
45  * evaluation context, such as all available mixins and variables at any given
46  * time.
47  *
48  * The `Parser` class is only concerned with parsing its input.
49  *
50  * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
51  * handling things like indentation.
52  */
54 /**
55  * SCSS compiler
56  *
57  * @author Leaf Corcoran <leafot@gmail.com>
58  */
59 class Compiler
60 {
61     const LINE_COMMENTS = 1;
62     const DEBUG_INFO    = 2;
64     const WITH_RULE     = 1;
65     const WITH_MEDIA    = 2;
66     const WITH_SUPPORTS = 4;
67     const WITH_ALL      = 7;
69     const SOURCE_MAP_NONE   = 0;
70     const SOURCE_MAP_INLINE = 1;
71     const SOURCE_MAP_FILE   = 2;
73     /**
74      * @var array
75      */
76     static protected $operatorNames = [
77         '+'   => 'add',
78         '-'   => 'sub',
79         '*'   => 'mul',
80         '/'   => 'div',
81         '%'   => 'mod',
83         '=='  => 'eq',
84         '!='  => 'neq',
85         '<'   => 'lt',
86         '>'   => 'gt',
88         '<='  => 'lte',
89         '>='  => 'gte',
90         '<=>' => 'cmp',
91     ];
93     /**
94      * @var array
95      */
96     static protected $namespaces = [
97         'special'  => '%',
98         'mixin'    => '@',
99         'function' => '^',
100     ];
102     static public $true         = [Type::T_KEYWORD, 'true'];
103     static public $false        = [Type::T_KEYWORD, 'false'];
104     static public $null         = [Type::T_NULL];
105     static public $nullString   = [Type::T_STRING, '', []];
106     static public $defaultValue = [Type::T_KEYWORD, ''];
107     static public $selfSelector = [Type::T_SELF];
108     static public $emptyList    = [Type::T_LIST, '', []];
109     static public $emptyMap     = [Type::T_MAP, [], []];
110     static public $emptyString  = [Type::T_STRING, '"', []];
111     static public $with         = [Type::T_KEYWORD, 'with'];
112     static public $without      = [Type::T_KEYWORD, 'without'];
114     protected $importPaths = [''];
115     protected $importCache = [];
116     protected $importedFiles = [];
117     protected $userFunctions = [];
118     protected $registeredVars = [];
119     protected $registeredFeatures = [
120         'extend-selector-pseudoclass' => false,
121         'at-error'                    => true,
122         'units-level-3'               => false,
123         'global-variable-shadowing'   => false,
124     ];
126     protected $encoding = null;
127     protected $lineNumberStyle = null;
129     protected $sourceMap = self::SOURCE_MAP_NONE;
130     protected $sourceMapOptions = [];
132     /**
133      * @var string|\ScssPhp\ScssPhp\Formatter
134      */
135     protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested';
137     protected $rootEnv;
138     protected $rootBlock;
140     /**
141      * @var \ScssPhp\ScssPhp\Compiler\Environment
142      */
143     protected $env;
144     protected $scope;
145     protected $storeEnv;
146     protected $charsetSeen;
147     protected $sourceNames;
149     protected $cache;
151     protected $indentLevel;
152     protected $extends;
153     protected $extendsMap;
154     protected $parsedFiles;
155     protected $parser;
156     protected $sourceIndex;
157     protected $sourceLine;
158     protected $sourceColumn;
159     protected $stderr;
160     protected $shouldEvaluate;
161     protected $ignoreErrors;
163     protected $callStack = [];
165     /**
166      * Constructor
167      */
168     public function __construct($cacheOptions = null)
169     {
170         $this->parsedFiles = [];
171         $this->sourceNames = [];
173         if ($cacheOptions) {
174             $this->cache = new Cache($cacheOptions);
175         }
176     }
178     public function getCompileOptions()
179     {
180         $options = [
181             'importPaths'        => $this->importPaths,
182             'registeredVars'     => $this->registeredVars,
183             'registeredFeatures' => $this->registeredFeatures,
184             'encoding'           => $this->encoding,
185             'sourceMap'          => serialize($this->sourceMap),
186             'sourceMapOptions'   => $this->sourceMapOptions,
187             'formatter'          => $this->formatter,
188         ];
190         return $options;
191     }
193     /**
194      * Compile scss
195      *
196      * @api
197      *
198      * @param string $code
199      * @param string $path
200      *
201      * @return string
202      */
203     public function compile($code, $path = null)
204     {
205         if ($this->cache) {
206             $cacheKey       = ($path ? $path : "(stdin)") . ":" . md5($code);
207             $compileOptions = $this->getCompileOptions();
208             $cache          = $this->cache->getCache("compile", $cacheKey, $compileOptions);
210             if (is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
211                 // check if any dependency file changed before accepting the cache
212                 foreach ($cache['dependencies'] as $file => $mtime) {
213                     if (! file_exists($file) || filemtime($file) !== $mtime) {
214                         unset($cache);
215                         break;
216                     }
217                 }
219                 if (isset($cache)) {
220                     return $cache['out'];
221                 }
222             }
223         }
226         $this->indentLevel    = -1;
227         $this->extends        = [];
228         $this->extendsMap     = [];
229         $this->sourceIndex    = null;
230         $this->sourceLine     = null;
231         $this->sourceColumn   = null;
232         $this->env            = null;
233         $this->scope          = null;
234         $this->storeEnv       = null;
235         $this->charsetSeen    = null;
236         $this->shouldEvaluate = null;
237         $this->stderr         = fopen('php://stderr', 'w');
239         $this->parser = $this->parserFactory($path);
240         $tree         = $this->parser->parse($code);
241         $this->parser = null;
243         $this->formatter = new $this->formatter();
244         $this->rootBlock = null;
245         $this->rootEnv   = $this->pushEnv($tree);
247         $this->injectVariables($this->registeredVars);
248         $this->compileRoot($tree);
249         $this->popEnv();
251         $sourceMapGenerator = null;
253         if ($this->sourceMap) {
254             if (is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
255                 $sourceMapGenerator = $this->sourceMap;
256                 $this->sourceMap = self::SOURCE_MAP_FILE;
257             } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
258                 $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
259             }
260         }
262         $out = $this->formatter->format($this->scope, $sourceMapGenerator);
264         if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
265             $sourceMap    = $sourceMapGenerator->generateJson();
266             $sourceMapUrl = null;
268             switch ($this->sourceMap) {
269                 case self::SOURCE_MAP_INLINE:
270                     $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
271                     break;
273                 case self::SOURCE_MAP_FILE:
274                     $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
275                     break;
276             }
278             $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
279         }
281         if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
282             $v = [
283                 'dependencies' => $this->getParsedFiles(),
284                 'out' => &$out,
285             ];
287             $this->cache->setCache("compile", $cacheKey, $v, $compileOptions);
288         }
290         return $out;
291     }
293     /**
294      * Instantiate parser
295      *
296      * @param string $path
297      *
298      * @return \ScssPhp\ScssPhp\Parser
299      */
300     protected function parserFactory($path)
301     {
302         $parser = new Parser($path, count($this->sourceNames), $this->encoding, $this->cache);
304         $this->sourceNames[] = $path;
305         $this->addParsedFile($path);
307         return $parser;
308     }
310     /**
311      * Is self extend?
312      *
313      * @param array $target
314      * @param array $origin
315      *
316      * @return boolean
317      */
318     protected function isSelfExtend($target, $origin)
319     {
320         foreach ($origin as $sel) {
321             if (in_array($target, $sel)) {
322                 return true;
323             }
324         }
326         return false;
327     }
329     /**
330      * Push extends
331      *
332      * @param array     $target
333      * @param array     $origin
334      * @param \stdClass $block
335      */
336     protected function pushExtends($target, $origin, $block)
337     {
338         if ($this->isSelfExtend($target, $origin)) {
339             return;
340         }
342         $i = count($this->extends);
343         $this->extends[] = [$target, $origin, $block];
345         foreach ($target as $part) {
346             if (isset($this->extendsMap[$part])) {
347                 $this->extendsMap[$part][] = $i;
348             } else {
349                 $this->extendsMap[$part] = [$i];
350             }
351         }
352     }
354     /**
355      * Make output block
356      *
357      * @param string $type
358      * @param array  $selectors
359      *
360      * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
361      */
362     protected function makeOutputBlock($type, $selectors = null)
363     {
364         $out = new OutputBlock;
365         $out->type         = $type;
366         $out->lines        = [];
367         $out->children     = [];
368         $out->parent       = $this->scope;
369         $out->selectors    = $selectors;
370         $out->depth        = $this->env->depth;
372         if ($this->env->block instanceof Block) {
373             $out->sourceName   = $this->env->block->sourceName;
374             $out->sourceLine   = $this->env->block->sourceLine;
375             $out->sourceColumn = $this->env->block->sourceColumn;
376         } else {
377             $out->sourceName   = null;
378             $out->sourceLine   = null;
379             $out->sourceColumn = null;
380         }
382         return $out;
383     }
385     /**
386      * Compile root
387      *
388      * @param \ScssPhp\ScssPhp\Block $rootBlock
389      */
390     protected function compileRoot(Block $rootBlock)
391     {
392         $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
394         $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
395         $this->flattenSelectors($this->scope);
396         $this->missingSelectors();
397     }
399     /**
400      * Report missing selectors
401      */
402     protected function missingSelectors()
403     {
404         foreach ($this->extends as $extend) {
405             if (isset($extend[3])) {
406                 continue;
407             }
409             list($target, $origin, $block) = $extend;
411             // ignore if !optional
412             if ($block[2]) {
413                 continue;
414             }
416             $target = implode(' ', $target);
417             $origin = $this->collapseSelectors($origin);
419             $this->sourceLine = $block[Parser::SOURCE_LINE];
420             $this->throwError("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
421         }
422     }
424     /**
425      * Flatten selectors
426      *
427      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
428      * @param string                                 $parentKey
429      */
430     protected function flattenSelectors(OutputBlock $block, $parentKey = null)
431     {
432         if ($block->selectors) {
433             $selectors = [];
435             foreach ($block->selectors as $s) {
436                 $selectors[] = $s;
438                 if (! is_array($s)) {
439                     continue;
440                 }
442                 // check extends
443                 if (! empty($this->extendsMap)) {
444                     $this->matchExtends($s, $selectors);
446                     // remove duplicates
447                     array_walk($selectors, function (&$value) {
448                         $value = serialize($value);
449                     });
451                     $selectors = array_unique($selectors);
453                     array_walk($selectors, function (&$value) {
454                         $value = unserialize($value);
455                     });
456                 }
457             }
459             $block->selectors = [];
460             $placeholderSelector = false;
462             foreach ($selectors as $selector) {
463                 if ($this->hasSelectorPlaceholder($selector)) {
464                     $placeholderSelector = true;
465                     continue;
466                 }
468                 $block->selectors[] = $this->compileSelector($selector);
469             }
471             if ($placeholderSelector && 0 === count($block->selectors) && null !== $parentKey) {
472                 unset($block->parent->children[$parentKey]);
474                 return;
475             }
476         }
478         foreach ($block->children as $key => $child) {
479             $this->flattenSelectors($child, $key);
480         }
481     }
483     /**
484      * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
485      *
486      * @param array $parts
487      *
488      * @return array
489      */
490     protected function glueFunctionSelectors($parts)
491     {
492         $new = [];
494         foreach ($parts as $part) {
495             if (is_array($part)) {
496                 $part = $this->glueFunctionSelectors($part);
497                 $new[] = $part;
498             } else {
499                 // a selector part finishing with a ) is the last part of a :not( or :nth-child(
500                 // and need to be joined to this
501                 if (count($new) && is_string($new[count($new) - 1]) &&
502                     strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
503                 ) {
504                     $new[count($new) - 1] .= $part;
505                 } else {
506                     $new[] = $part;
507                 }
508             }
509         }
511         return $new;
512     }
514     /**
515      * Match extends
516      *
517      * @param array   $selector
518      * @param array   $out
519      * @param integer $from
520      * @param boolean $initial
521      */
522     protected function matchExtends($selector, &$out, $from = 0, $initial = true)
523     {
524         static $partsPile = [];
526         $selector = $this->glueFunctionSelectors($selector);
528         if (count($selector) == 1 && in_array(reset($selector), $partsPile)) {
529             return;
530         }
532         foreach ($selector as $i => $part) {
533             if ($i < $from) {
534                 continue;
535             }
537             // check that we are not building an infinite loop of extensions
538             // if the new part is just including a previous part don't try to extend anymore
539             if (count($part) > 1) {
540                 foreach ($partsPile as $previousPart) {
541                     if (! count(array_diff($previousPart, $part))) {
542                         continue 2;
543                     }
544                 }
545             }
547             if ($this->matchExtendsSingle($part, $origin)) {
548                 $partsPile[] = $part;
549                 $after       = array_slice($selector, $i + 1);
550                 $before      = array_slice($selector, 0, $i);
552                 list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
554                 foreach ($origin as $new) {
555                     $k = 0;
557                     // remove shared parts
558                     if (count($new) > 1) {
559                         while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
560                             $k++;
561                         }
562                     }
564                     $replacement = [];
565                     $tempReplacement = $k > 0 ? array_slice($new, $k) : $new;
567                     for ($l = count($tempReplacement) - 1; $l >= 0; $l--) {
568                         $slice = [];
570                         foreach ($tempReplacement[$l] as $chunk) {
571                             if (! in_array($chunk, $slice)) {
572                                 $slice[] = $chunk;
573                             }
574                         }
576                         array_unshift($replacement, $slice);
578                         if (! $this->isImmediateRelationshipCombinator(end($slice))) {
579                             break;
580                         }
581                     }
583                     $afterBefore = $l != 0 ? array_slice($tempReplacement, 0, $l) : [];
585                     // Merge shared direct relationships.
586                     $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
588                     $result = array_merge(
589                         $before,
590                         $mergedBefore,
591                         $replacement,
592                         $after
593                     );
595                     if ($result === $selector) {
596                         continue;
597                     }
599                     $out[] = $result;
601                     // recursively check for more matches
602                     $startRecurseFrom = count($before) + min(count($nonBreakableBefore), count($mergedBefore));
603                     $this->matchExtends($result, $out, $startRecurseFrom, false);
605                     // selector sequence merging
606                     if (! empty($before) && count($new) > 1) {
607                         $preSharedParts = $k > 0 ? array_slice($before, 0, $k) : [];
608                         $postSharedParts = $k > 0 ? array_slice($before, $k) : $before;
610                         list($betweenSharedParts, $nonBreakable2) = $this->extractRelationshipFromFragment($afterBefore);
612                         $result2 = array_merge(
613                             $preSharedParts,
614                             $betweenSharedParts,
615                             $postSharedParts,
616                             $nonBreakable2,
617                             $nonBreakableBefore,
618                             $replacement,
619                             $after
620                         );
622                         $out[] = $result2;
623                     }
624                 }
626                 array_pop($partsPile);
627             }
628         }
629     }
631     /**
632      * Match extends single
633      *
634      * @param array $rawSingle
635      * @param array $outOrigin
636      *
637      * @return boolean
638      */
639     protected function matchExtendsSingle($rawSingle, &$outOrigin)
640     {
641         $counts = [];
642         $single = [];
644         // simple usual cases, no need to do the whole trick
645         if (in_array($rawSingle, [['>'],['+'],['~']])) {
646             return false;
647         }
649         foreach ($rawSingle as $part) {
650             // matches Number
651             if (! is_string($part)) {
652                 return false;
653             }
655             if (! preg_match('/^[\[.:#%]/', $part) && count($single)) {
656                 $single[count($single) - 1] .= $part;
657             } else {
658                 $single[] = $part;
659             }
660         }
662         $extendingDecoratedTag = false;
664         if (count($single) > 1) {
665             $matches = null;
666             $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
667         }
669         foreach ($single as $part) {
670             if (isset($this->extendsMap[$part])) {
671                 foreach ($this->extendsMap[$part] as $idx) {
672                     $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
673                 }
674             }
675         }
677         $outOrigin = [];
678         $found = false;
680         foreach ($counts as $idx => $count) {
681             list($target, $origin, /* $block */) = $this->extends[$idx];
683             $origin = $this->glueFunctionSelectors($origin);
685             // check count
686             if ($count !== count($target)) {
687                 continue;
688             }
690             $this->extends[$idx][3] = true;
692             $rem = array_diff($single, $target);
694             foreach ($origin as $j => $new) {
695                 // prevent infinite loop when target extends itself
696                 if ($this->isSelfExtend($single, $origin)) {
697                     return false;
698                 }
700                 $replacement = end($new);
702                 // Extending a decorated tag with another tag is not possible.
703                 if ($extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
704                     preg_match('/^[a-z0-9]+$/i', $replacement[0])
705                 ) {
706                     unset($origin[$j]);
707                     continue;
708                 }
710                 $combined = $this->combineSelectorSingle($replacement, $rem);
712                 if (count(array_diff($combined, $origin[$j][count($origin[$j]) - 1]))) {
713                     $origin[$j][count($origin[$j]) - 1] = $combined;
714                 }
715             }
717             $outOrigin = array_merge($outOrigin, $origin);
719             $found = true;
720         }
722         return $found;
723     }
725     /**
726      * Extract a relationship from the fragment.
727      *
728      * When extracting the last portion of a selector we will be left with a
729      * fragment which may end with a direction relationship combinator. This
730      * method will extract the relationship fragment and return it along side
731      * the rest.
732      *
733      * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
734      *
735      * @return array The selector without the relationship fragment if any, the relationship fragment.
736      */
737     protected function extractRelationshipFromFragment(array $fragment)
738     {
739         $parents = [];
740         $children = [];
741         $j = $i = count($fragment);
743         for (;;) {
744             $children = $j != $i ? array_slice($fragment, $j, $i - $j) : [];
745             $parents = array_slice($fragment, 0, $j);
746             $slice = end($parents);
748             if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
749                 break;
750             }
752             $j -= 2;
753         }
755         return [$parents, $children];
756     }
758     /**
759      * Combine selector single
760      *
761      * @param array $base
762      * @param array $other
763      *
764      * @return array
765      */
766     protected function combineSelectorSingle($base, $other)
767     {
768         $tag = [];
769         $out = [];
770         $wasTag = true;
772         foreach ([$base, $other] as $single) {
773             foreach ($single as $part) {
774                 if (preg_match('/^[\[.:#]/', $part)) {
775                     $out[] = $part;
776                     $wasTag = false;
777                 } elseif (preg_match('/^[^_-]/', $part)) {
778                     $tag[] = $part;
779                     $wasTag = true;
780                 } elseif ($wasTag) {
781                     $tag[count($tag) - 1] .= $part;
782                 } else {
783                     $out[count($out) - 1] .= $part;
784                 }
785             }
786         }
788         if (count($tag)) {
789             array_unshift($out, $tag[0]);
790         }
792         return $out;
793     }
795     /**
796      * Compile media
797      *
798      * @param \ScssPhp\ScssPhp\Block $media
799      */
800     protected function compileMedia(Block $media)
801     {
802         $this->pushEnv($media);
804         $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
806         if (! empty($mediaQueries) && $mediaQueries) {
807             $previousScope = $this->scope;
808             $parentScope = $this->mediaParent($this->scope);
810             foreach ($mediaQueries as $mediaQuery) {
811                 $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
813                 $parentScope->children[] = $this->scope;
814                 $parentScope = $this->scope;
815             }
817             // top level properties in a media cause it to be wrapped
818             $needsWrap = false;
820             foreach ($media->children as $child) {
821                 $type = $child[0];
823                 if ($type !== Type::T_BLOCK &&
824                     $type !== Type::T_MEDIA &&
825                     $type !== Type::T_DIRECTIVE &&
826                     $type !== Type::T_IMPORT
827                 ) {
828                     $needsWrap = true;
829                     break;
830                 }
831             }
833             if ($needsWrap) {
834                 $wrapped = new Block;
835                 $wrapped->sourceName = $media->sourceName;
836                 $wrapped->sourceIndex = $media->sourceIndex;
837                 $wrapped->sourceLine = $media->sourceLine;
838                 $wrapped->sourceColumn = $media->sourceColumn;
839                 $wrapped->selectors = [];
840                 $wrapped->comments = [];
841                 $wrapped->parent = $media;
842                 $wrapped->children = $media->children;
844                 $media->children = [[Type::T_BLOCK, $wrapped]];
845                 if (isset($this->lineNumberStyle)) {
846                     $annotation = $this->makeOutputBlock(Type::T_COMMENT);
847                     $annotation->depth = 0;
849                     $file = $this->sourceNames[$media->sourceIndex];
850                     $line = $media->sourceLine;
852                     switch ($this->lineNumberStyle) {
853                         case static::LINE_COMMENTS:
854                             $annotation->lines[] = '/* line ' . $line
855                                                  . ($file ? ', ' . $file : '')
856                                                  . ' */';
857                             break;
859                         case static::DEBUG_INFO:
860                             $annotation->lines[] = '@media -sass-debug-info{'
861                                                  . ($file ? 'filename{font-family:"' . $file . '"}' : '')
862                                                  . 'line{font-family:' . $line . '}}';
863                             break;
864                     }
866                     $this->scope->children[] = $annotation;
867                 }
868             }
870             $this->compileChildrenNoReturn($media->children, $this->scope);
872             $this->scope = $previousScope;
873         }
875         $this->popEnv();
876     }
878     /**
879      * Media parent
880      *
881      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
882      *
883      * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
884      */
885     protected function mediaParent(OutputBlock $scope)
886     {
887         while (! empty($scope->parent)) {
888             if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
889                 break;
890             }
892             $scope = $scope->parent;
893         }
895         return $scope;
896     }
898     /**
899      * Compile directive
900      *
901      * @param \ScssPhp\ScssPhp\Block $block
902      */
903     protected function compileDirective(Block $block)
904     {
905         $s = '@' . $block->name;
907         if (! empty($block->value)) {
908             $s .= ' ' . $this->compileValue($block->value);
909         }
911         if ($block->name === 'keyframes' || substr($block->name, -10) === '-keyframes') {
912             $this->compileKeyframeBlock($block, [$s]);
913         } else {
914             $this->compileNestedBlock($block, [$s]);
915         }
916     }
918     /**
919      * Compile at-root
920      *
921      * @param \ScssPhp\ScssPhp\Block $block
922      */
923     protected function compileAtRoot(Block $block)
924     {
925         $env     = $this->pushEnv($block);
926         $envs    = $this->compactEnv($env);
927         list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
929         // wrap inline selector
930         if ($block->selector) {
931             $wrapped = new Block;
932             $wrapped->sourceName   = $block->sourceName;
933             $wrapped->sourceIndex  = $block->sourceIndex;
934             $wrapped->sourceLine   = $block->sourceLine;
935             $wrapped->sourceColumn = $block->sourceColumn;
936             $wrapped->selectors    = $block->selector;
937             $wrapped->comments     = [];
938             $wrapped->parent       = $block;
939             $wrapped->children     = $block->children;
940             $wrapped->selfParent   = $block->selfParent;
942             $block->children = [[Type::T_BLOCK, $wrapped]];
943             $block->selector = null;
944         }
946         $selfParent = $block->selfParent;
948         if (! $block->selfParent->selectors && isset($block->parent) && $block->parent &&
949             isset($block->parent->selectors) && $block->parent->selectors
950         ) {
951             $selfParent = $block->parent;
952         }
954         $this->env = $this->filterWithWithout($envs, $with, $without);
956         $saveScope   = $this->scope;
957         $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
959         // propagate selfParent to the children where they still can be useful
960         $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
962         $this->scope = $this->completeScope($this->scope, $saveScope);
963         $this->scope = $saveScope;
964         $this->env   = $this->extractEnv($envs);
966         $this->popEnv();
967     }
969     /**
970      * Filter at-root scope depending of with/without option
971      *
972      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
973      * @param array                                  $with
974      * @param array                                  $without
975      *
976      * @return mixed
977      */
978     protected function filterScopeWithWithout($scope, $with, $without)
979     {
980         $filteredScopes = [];
982         if ($scope->type === TYPE::T_ROOT) {
983             return $scope;
984         }
986         // start from the root
987         while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
988             $scope = $scope->parent;
989         }
991         for (;;) {
992             if (! $scope) {
993                 break;
994             }
996             if ($this->isWith($scope, $with, $without)) {
997                 $s = clone $scope;
998                 $s->children = [];
999                 $s->lines = [];
1000                 $s->parent = null;
1002                 if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1003                     $s->selectors = [];
1004                 }
1006                 $filteredScopes[] = $s;
1007             }
1009             if ($scope->children) {
1010                 $scope = end($scope->children);
1011             } else {
1012                 $scope = null;
1013             }
1014         }
1016         if (! count($filteredScopes)) {
1017             return $this->rootBlock;
1018         }
1020         $newScope = array_shift($filteredScopes);
1021         $newScope->parent = $this->rootBlock;
1023         $this->rootBlock->children[] = $newScope;
1025         $p = &$newScope;
1027         while (count($filteredScopes)) {
1028             $s = array_shift($filteredScopes);
1029             $s->parent = $p;
1030             $p->children[] = &$s;
1031             $p = $s;
1032         }
1034         return $newScope;
1035     }
1037     /**
1038      * found missing selector from a at-root compilation in the previous scope
1039      * (if at-root is just enclosing a property, the selector is in the parent tree)
1040      *
1041      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1042      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1043      *
1044      * @return mixed
1045      */
1046     protected function completeScope($scope, $previousScope)
1047     {
1048         if (! $scope->type && (! $scope->selectors || ! count($scope->selectors)) && count($scope->lines)) {
1049             $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1050         }
1052         if ($scope->children) {
1053             foreach ($scope->children as $k => $c) {
1054                 $scope->children[$k] = $this->completeScope($c, $previousScope);
1055             }
1056         }
1058         return $scope;
1059     }
1061     /**
1062      * Find a selector by the depth node in the scope
1063      *
1064      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1065      * @param integer                                $depth
1066      *
1067      * @return array
1068      */
1069     protected function findScopeSelectors($scope, $depth)
1070     {
1071         if ($scope->depth === $depth && $scope->selectors) {
1072             return $scope->selectors;
1073         }
1075         if ($scope->children) {
1076             foreach (array_reverse($scope->children) as $c) {
1077                 if ($s = $this->findScopeSelectors($c, $depth)) {
1078                     return $s;
1079                 }
1080             }
1081         }
1083         return [];
1084     }
1086     /**
1087      * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1088      *
1089      * @param array $withCondition
1090      *
1091      * @return array
1092      */
1093     protected function compileWith($withCondition)
1094     {
1095         // just compile what we have in 2 lists
1096         $with = [];
1097         $without = ['rule' => true];
1099         if ($withCondition) {
1100             if ($this->libMapHasKey([$withCondition, static::$with])) {
1101                 $without = []; // cancel the default
1102                 $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1104                 foreach ($list[2] as $item) {
1105                     $keyword = $this->compileStringContent($this->coerceString($item));
1107                     $with[$keyword] = true;
1108                 }
1109             }
1111             if ($this->libMapHasKey([$withCondition, static::$without])) {
1112                 $without = []; // cancel the default
1113                 $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1115                 foreach ($list[2] as $item) {
1116                     $keyword = $this->compileStringContent($this->coerceString($item));
1118                     $without[$keyword] = true;
1119                 }
1120             }
1121         }
1123         return [$with, $without];
1124     }
1126     /**
1127      * Filter env stack
1128      *
1129      * @param array   $envs
1130      * @param array $with
1131      * @param array $without
1132      *
1133      * @return \ScssPhp\ScssPhp\Compiler\Environment
1134      */
1135     protected function filterWithWithout($envs, $with, $without)
1136     {
1137         $filtered = [];
1139         foreach ($envs as $e) {
1140             if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1141                 $ec = clone $e;
1142                 $ec->block = null;
1143                 $ec->selectors = [];
1144                 $filtered[] = $ec;
1145             } else {
1146                 $filtered[] = $e;
1147             }
1148         }
1150         return $this->extractEnv($filtered);
1151     }
1153     /**
1154      * Filter WITH rules
1155      *
1156      * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1157      * @param array                                                         $with
1158      * @param array                                                         $without
1159      *
1160      * @return boolean
1161      */
1162     protected function isWith($block, $with, $without)
1163     {
1164         if (isset($block->type)) {
1165             if ($block->type === Type::T_MEDIA) {
1166                 return $this->testWithWithout('media', $with, $without);
1167             }
1169             if ($block->type === Type::T_DIRECTIVE) {
1170                 if (isset($block->name)) {
1171                     return $this->testWithWithout($block->name, $with, $without);
1172                 }
1173                 elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1174                     return $this->testWithWithout($m[1], $with, $without);
1175                 }
1176                 else {
1177                     return $this->testWithWithout('???', $with, $without);
1178                 }
1179             }
1180         }
1181         elseif (isset($block->selectors)) {
1182             return $this->testWithWithout('rule', $with, $without);
1183         }
1185         return true;
1186     }
1188     /**
1189      * Test a single type of block against with/without lists
1190      *
1191      * @param string $what
1192      * @param array  $with
1193      * @param array  $without
1194      * @return bool
1195      *   true if the block should be kept, false to reject
1196      */
1197     protected function testWithWithout($what, $with, $without) {
1199         // if without, reject only if in the list (or 'all' is in the list)
1200         if (count($without)) {
1201             return (isset($without[$what]) || isset($without['all'])) ? false : true;
1202         }
1204         // otherwise reject all what is not in the with list
1205         return (isset($with[$what]) || isset($with['all'])) ? true : false;
1206     }
1209     /**
1210      * Compile keyframe block
1211      *
1212      * @param \ScssPhp\ScssPhp\Block $block
1213      * @param array                  $selectors
1214      */
1215     protected function compileKeyframeBlock(Block $block, $selectors)
1216     {
1217         $env = $this->pushEnv($block);
1219         $envs = $this->compactEnv($env);
1221         $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
1222             return ! isset($e->block->selectors);
1223         }));
1225         $this->scope = $this->makeOutputBlock($block->type, $selectors);
1226         $this->scope->depth = 1;
1227         $this->scope->parent->children[] = $this->scope;
1229         $this->compileChildrenNoReturn($block->children, $this->scope);
1231         $this->scope = $this->scope->parent;
1232         $this->env   = $this->extractEnv($envs);
1234         $this->popEnv();
1235     }
1237     /**
1238      * Compile nested properties lines
1239      *
1240      * @param \ScssPhp\ScssPhp\Block $block
1241      * @param OutputBlock            $out
1242      */
1243     protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1244     {
1245         $prefix = $this->compileValue($block->prefix) . '-';
1247         $nested = $this->makeOutputBlock($block->type);
1248         $nested->parent = $out;
1250         if ($block->hasValue) {
1251             $nested->depth = $out->depth + 1;
1252         }
1254         $out->children[] = $nested;
1256         foreach ($block->children as $child) {
1257             switch ($child[0]) {
1258                 case Type::T_ASSIGN:
1259                     array_unshift($child[1][2], $prefix);
1260                     break;
1262                 case Type::T_NESTED_PROPERTY:
1263                     array_unshift($child[1]->prefix[2], $prefix);
1264                     break;
1265             }
1266             $this->compileChild($child, $nested);
1267         }
1268     }
1270     /**
1271      * Compile nested block
1272      *
1273      * @param \ScssPhp\ScssPhp\Block $block
1274      * @param array                  $selectors
1275      */
1276     protected function compileNestedBlock(Block $block, $selectors)
1277     {
1278         $this->pushEnv($block);
1280         $this->scope = $this->makeOutputBlock($block->type, $selectors);
1281         $this->scope->parent->children[] = $this->scope;
1283         // wrap assign children in a block
1284         // except for @font-face
1285         if ($block->type !== Type::T_DIRECTIVE || $block->name !== "font-face") {
1286             // need wrapping?
1287             $needWrapping = false;
1289             foreach ($block->children as $child) {
1290                 if ($child[0] === Type::T_ASSIGN) {
1291                     $needWrapping = true;
1292                     break;
1293                 }
1294             }
1296             if ($needWrapping) {
1297                 $wrapped = new Block;
1298                 $wrapped->sourceName = $block->sourceName;
1299                 $wrapped->sourceIndex = $block->sourceIndex;
1300                 $wrapped->sourceLine = $block->sourceLine;
1301                 $wrapped->sourceColumn = $block->sourceColumn;
1302                 $wrapped->selectors = [];
1303                 $wrapped->comments = [];
1304                 $wrapped->parent = $block;
1305                 $wrapped->children = $block->children;
1306                 $wrapped->selfParent = $block->selfParent;
1308                 $block->children = [[Type::T_BLOCK, $wrapped]];
1309             }
1310         }
1312         $this->compileChildrenNoReturn($block->children, $this->scope);
1314         $this->scope = $this->scope->parent;
1316         $this->popEnv();
1317     }
1319     /**
1320      * Recursively compiles a block.
1321      *
1322      * A block is analogous to a CSS block in most cases. A single SCSS document
1323      * is encapsulated in a block when parsed, but it does not have parent tags
1324      * so all of its children appear on the root level when compiled.
1325      *
1326      * Blocks are made up of selectors and children.
1327      *
1328      * The children of a block are just all the blocks that are defined within.
1329      *
1330      * Compiling the block involves pushing a fresh environment on the stack,
1331      * and iterating through the props, compiling each one.
1332      *
1333      * @see Compiler::compileChild()
1334      *
1335      * @param \ScssPhp\ScssPhp\Block $block
1336      */
1337     protected function compileBlock(Block $block)
1338     {
1339         $env = $this->pushEnv($block);
1340         $env->selectors = $this->evalSelectors($block->selectors);
1342         $out = $this->makeOutputBlock(null);
1344         if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
1345             $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1346             $annotation->depth = 0;
1348             $file = $this->sourceNames[$block->sourceIndex];
1349             $line = $block->sourceLine;
1351             switch ($this->lineNumberStyle) {
1352                 case static::LINE_COMMENTS:
1353                     $annotation->lines[] = '/* line ' . $line
1354                                          . ($file ? ', ' . $file : '')
1355                                          . ' */';
1356                     break;
1358                 case static::DEBUG_INFO:
1359                     $annotation->lines[] = '@media -sass-debug-info{'
1360                                          . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1361                                          . 'line{font-family:' . $line . '}}';
1362                     break;
1363             }
1365             $this->scope->children[] = $annotation;
1366         }
1368         $this->scope->children[] = $out;
1370         if (count($block->children)) {
1371             $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1373             // propagate selfParent to the children where they still can be useful
1374             $selfParentSelectors = null;
1376             if (isset($block->selfParent->selectors)) {
1377                 $selfParentSelectors = $block->selfParent->selectors;
1378                 $block->selfParent->selectors = $out->selectors;
1379             }
1381             $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1383             // and revert for the following childs of the same block
1384             if ($selfParentSelectors) {
1385                 $block->selfParent->selectors = $selfParentSelectors;
1386             }
1387         }
1389         $this->formatter->stripSemicolon($out->lines);
1391         $this->popEnv();
1392     }
1394     /**
1395      * Compile root level comment
1396      *
1397      * @param array $block
1398      */
1399     protected function compileComment($block)
1400     {
1401         $out = $this->makeOutputBlock(Type::T_COMMENT);
1402         $out->lines[] = is_string($block[1]) ? $block[1] : $this->compileValue($block[1]);
1404         $this->scope->children[] = $out;
1405     }
1407     /**
1408      * Evaluate selectors
1409      *
1410      * @param array $selectors
1411      *
1412      * @return array
1413      */
1414     protected function evalSelectors($selectors)
1415     {
1416         $this->shouldEvaluate = false;
1418         $selectors = array_map([$this, 'evalSelector'], $selectors);
1420         // after evaluating interpolates, we might need a second pass
1421         if ($this->shouldEvaluate) {
1422             $selectors = $this->revertSelfSelector($selectors);
1423             $buffer = $this->collapseSelectors($selectors);
1424             $parser = $this->parserFactory(__METHOD__);
1426             if ($parser->parseSelector($buffer, $newSelectors)) {
1427                 $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1428             }
1429         }
1431         return $selectors;
1432     }
1434     /**
1435      * Evaluate selector
1436      *
1437      * @param array $selector
1438      *
1439      * @return array
1440      */
1441     protected function evalSelector($selector)
1442     {
1443         return array_map([$this, 'evalSelectorPart'], $selector);
1444     }
1446     /**
1447      * Evaluate selector part; replaces all the interpolates, stripping quotes
1448      *
1449      * @param array $part
1450      *
1451      * @return array
1452      */
1453     protected function evalSelectorPart($part)
1454     {
1455         foreach ($part as &$p) {
1456             if (is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
1457                 $p = $this->compileValue($p);
1459                 // force re-evaluation
1460                 if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
1461                     $this->shouldEvaluate = true;
1462                 }
1463             } elseif (is_string($p) && strlen($p) >= 2 &&
1464                 ($first = $p[0]) && ($first === '"' || $first === "'") &&
1465                 substr($p, -1) === $first
1466             ) {
1467                 $p = substr($p, 1, -1);
1468             }
1469         }
1471         return $this->flattenSelectorSingle($part);
1472     }
1474     /**
1475      * Collapse selectors
1476      *
1477      * @param array   $selectors
1478      * @param boolean $selectorFormat
1479      *   if false return a collapsed string
1480      *   if true return an array description of a structured selector
1481      *
1482      * @return string
1483      */
1484     protected function collapseSelectors($selectors, $selectorFormat = false)
1485     {
1486         $parts = [];
1488         foreach ($selectors as $selector) {
1489             $output = [];
1490             $glueNext = false;
1492             foreach ($selector as $node) {
1493                 $compound = '';
1495                 array_walk_recursive(
1496                     $node,
1497                     function ($value, $key) use (&$compound) {
1498                         $compound .= $value;
1499                     }
1500                 );
1502                 if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1503                     if (count($output)) {
1504                         $output[count($output) - 1] .= ' ' . $compound;
1505                     } else {
1506                         $output[] = $compound;
1507                     }
1508                     $glueNext = true;
1509                 } elseif ($glueNext) {
1510                     $output[count($output) - 1] .= ' ' . $compound;
1511                     $glueNext = false;
1512                 } else {
1513                     $output[] = $compound;
1514                 }
1515             }
1517             if ($selectorFormat) {
1518                 foreach ($output as &$o) {
1519                     $o = [Type::T_STRING, '', [$o]];
1520                 }
1521                 $output = [Type::T_LIST, ' ', $output];
1522             } else {
1523                 $output = implode(' ', $output);
1524             }
1526             $parts[] = $output;
1527         }
1529         if ($selectorFormat) {
1530             $parts = [Type::T_LIST, ',', $parts];
1531         } else {
1532             $parts = implode(', ', $parts);
1533         }
1535         return $parts;
1536     }
1538     /**
1539      * Parse down the selector and revert [self] to "&" before a reparsing
1540      *
1541      * @param array $selectors
1542      *
1543      * @return array
1544      */
1545     protected function revertSelfSelector($selectors)
1546     {
1547         foreach ($selectors as &$part) {
1548             if (is_array($part)) {
1549                 if ($part === [Type::T_SELF]) {
1550                     $part = '&';
1551                 } else {
1552                     $part = $this->revertSelfSelector($part);
1553                 }
1554             }
1555         }
1557         return $selectors;
1558     }
1560     /**
1561      * Flatten selector single; joins together .classes and #ids
1562      *
1563      * @param array $single
1564      *
1565      * @return array
1566      */
1567     protected function flattenSelectorSingle($single)
1568     {
1569         $joined = [];
1571         foreach ($single as $part) {
1572             if (empty($joined) ||
1573                 ! is_string($part) ||
1574                 preg_match('/[\[.:#%]/', $part)
1575             ) {
1576                 $joined[] = $part;
1577                 continue;
1578             }
1580             if (is_array(end($joined))) {
1581                 $joined[] = $part;
1582             } else {
1583                 $joined[count($joined) - 1] .= $part;
1584             }
1585         }
1587         return $joined;
1588     }
1590     /**
1591      * Compile selector to string; self(&) should have been replaced by now
1592      *
1593      * @param string|array $selector
1594      *
1595      * @return string
1596      */
1597     protected function compileSelector($selector)
1598     {
1599         if (! is_array($selector)) {
1600             return $selector; // media and the like
1601         }
1603         return implode(
1604             ' ',
1605             array_map(
1606                 [$this, 'compileSelectorPart'],
1607                 $selector
1608             )
1609         );
1610     }
1612     /**
1613      * Compile selector part
1614      *
1615      * @param array $piece
1616      *
1617      * @return string
1618      */
1619     protected function compileSelectorPart($piece)
1620     {
1621         foreach ($piece as &$p) {
1622             if (! is_array($p)) {
1623                 continue;
1624             }
1626             switch ($p[0]) {
1627                 case Type::T_SELF:
1628                     $p = '&';
1629                     break;
1631                 default:
1632                     $p = $this->compileValue($p);
1633                     break;
1634             }
1635         }
1637         return implode($piece);
1638     }
1640     /**
1641      * Has selector placeholder?
1642      *
1643      * @param array $selector
1644      *
1645      * @return boolean
1646      */
1647     protected function hasSelectorPlaceholder($selector)
1648     {
1649         if (! is_array($selector)) {
1650             return false;
1651         }
1653         foreach ($selector as $parts) {
1654             foreach ($parts as $part) {
1655                 if (strlen($part) && '%' === $part[0]) {
1656                     return true;
1657                 }
1658             }
1659         }
1661         return false;
1662     }
1664     protected function pushCallStack($name = '')
1665     {
1666         $this->callStack[] = [
1667           'n' => $name,
1668           Parser::SOURCE_INDEX => $this->sourceIndex,
1669           Parser::SOURCE_LINE => $this->sourceLine,
1670           Parser::SOURCE_COLUMN => $this->sourceColumn
1671         ];
1673         // infinite calling loop
1674         if (count($this->callStack) > 25000) {
1675             // not displayed but you can var_dump it to deep debug
1676             $msg = $this->callStackMessage(true, 100);
1677             $msg = "Infinite calling loop";
1678             $this->throwError($msg);
1679         }
1680     }
1682     protected function popCallStack()
1683     {
1684         array_pop($this->callStack);
1685     }
1687     /**
1688      * Compile children and return result
1689      *
1690      * @param array                                  $stms
1691      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1692      * @param string                                 $traceName
1693      *
1694      * @return array|null
1695      */
1696     protected function compileChildren($stms, OutputBlock $out, $traceName = '')
1697     {
1698         $this->pushCallStack($traceName);
1700         foreach ($stms as $stm) {
1701             $ret = $this->compileChild($stm, $out);
1703             if (isset($ret)) {
1704                 return $ret;
1705             }
1706         }
1708         $this->popCallStack();
1710         return null;
1711     }
1713     /**
1714      * Compile children and throw exception if unexpected @return
1715      *
1716      * @param array                                  $stms
1717      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1718      * @param \ScssPhp\ScssPhp\Block                 $selfParent
1719      * @param string                                 $traceName
1720      *
1721      * @throws \Exception
1722      */
1723     protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
1724     {
1725         $this->pushCallStack($traceName);
1727         foreach ($stms as $stm) {
1728             if ($selfParent && isset($stm[1]) && is_object($stm[1]) && $stm[1] instanceof Block) {
1729                 $stm[1]->selfParent = $selfParent;
1730                 $ret = $this->compileChild($stm, $out);
1731                 $stm[1]->selfParent = null;
1732             } elseif ($selfParent && $stm[0] === TYPE::T_INCLUDE) {
1733                 $stm['selfParent'] = $selfParent;
1734                 $ret = $this->compileChild($stm, $out);
1735                 unset($stm['selfParent']);
1736             } else {
1737                 $ret = $this->compileChild($stm, $out);
1738             }
1740             if (isset($ret)) {
1741                 $this->throwError('@return may only be used within a function');
1743                 return;
1744             }
1745         }
1747         $this->popCallStack();
1748     }
1751     /**
1752      * evaluate media query : compile internal value keeping the structure inchanged
1753      *
1754      * @param array $queryList
1755      *
1756      * @return array
1757      */
1758     protected function evaluateMediaQuery($queryList)
1759     {
1760         static $parser = null;
1761         $outQueryList = [];
1762         foreach ($queryList as $kql => $query) {
1763             $shouldReparse = false;
1764             foreach ($query as $kq => $q) {
1765                 for ($i = 1; $i < count($q); $i++) {
1766                     $value = $this->compileValue($q[$i]);
1768                     // the parser had no mean to know if media type or expression if it was an interpolation
1769                     // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
1770                     if ($q[0] == Type::T_MEDIA_TYPE &&
1771                         (strpos($value, '(') !== false ||
1772                         strpos($value, ')') !== false ||
1773                         strpos($value, ':') !== false ||
1774                         strpos($value, ',') !== false)
1775                     ) {
1776                         $shouldReparse = true;
1777                     }
1779                     $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
1780                 }
1781             }
1782             if ($shouldReparse) {
1783                 if (is_null($parser)) {
1784                     $parser = $this->parserFactory(__METHOD__);
1785                 }
1786                 $queryString = $this->compileMediaQuery([$queryList[$kql]]);
1787                 $queryString = reset($queryString);
1788                 if (strpos($queryString, '@media ') === 0) {
1789                     $queryString = substr($queryString, 7);
1790                     $queries = [];
1791                     if ($parser->parseMediaQueryList($queryString, $queries)) {
1792                         $queries = $this->evaluateMediaQuery($queries[2]);
1793                         while (count($queries)) {
1794                             $outQueryList[] = array_shift($queries);
1795                         }
1796                         continue;
1797                     }
1798                 }
1799             }
1800             $outQueryList[] = $queryList[$kql];
1801         }
1803         return $outQueryList;
1804     }
1806     /**
1807      * Compile media query
1808      *
1809      * @param array $queryList
1810      *
1811      * @return array
1812      */
1813     protected function compileMediaQuery($queryList)
1814     {
1815         $start = '@media ';
1816         $default = trim($start);
1817         $out = [];
1818         $current = "";
1820         foreach ($queryList as $query) {
1821             $type = null;
1822             $parts = [];
1824             $mediaTypeOnly = true;
1826             foreach ($query as $q) {
1827                 if ($q[0] !== Type::T_MEDIA_TYPE) {
1828                     $mediaTypeOnly = false;
1829                     break;
1830                 }
1831             }
1833             foreach ($query as $q) {
1834                 switch ($q[0]) {
1835                     case Type::T_MEDIA_TYPE:
1836                         $newType = array_map([$this, 'compileValue'], array_slice($q, 1));
1837                         // combining not and anything else than media type is too risky and should be avoided
1838                         if (! $mediaTypeOnly) {
1839                             if (in_array(Type::T_NOT, $newType) || ($type && in_array(Type::T_NOT, $type) )) {
1840                                 if ($type) {
1841                                     array_unshift($parts, implode(' ', array_filter($type)));
1842                                 }
1844                                 if (! empty($parts)) {
1845                                     if (strlen($current)) {
1846                                         $current .= $this->formatter->tagSeparator;
1847                                     }
1849                                     $current .= implode(' and ', $parts);
1850                                 }
1852                                 if ($current) {
1853                                     $out[] = $start . $current;
1854                                 }
1856                                 $current = "";
1857                                 $type = null;
1858                                 $parts = [];
1859                             }
1860                         }
1862                         if ($newType === ['all'] && $default) {
1863                             $default = $start . 'all';
1864                         }
1866                         // all can be safely ignored and mixed with whatever else
1867                         if ($newType !== ['all']) {
1868                             if ($type) {
1869                                 $type = $this->mergeMediaTypes($type, $newType);
1871                                 if (empty($type)) {
1872                                     // merge failed : ignore this query that is not valid, skip to the next one
1873                                     $parts = [];
1874                                     $default = ''; // if everything fail, no @media at all
1875                                     continue 3;
1876                                 }
1877                             } else {
1878                                 $type = $newType;
1879                             }
1880                         }
1881                         break;
1883                     case Type::T_MEDIA_EXPRESSION:
1884                         if (isset($q[2])) {
1885                             $parts[] = '('
1886                                 . $this->compileValue($q[1])
1887                                 . $this->formatter->assignSeparator
1888                                 . $this->compileValue($q[2])
1889                                 . ')';
1890                         } else {
1891                             $parts[] = '('
1892                                 . $this->compileValue($q[1])
1893                                 . ')';
1894                         }
1895                         break;
1897                     case Type::T_MEDIA_VALUE:
1898                         $parts[] = $this->compileValue($q[1]);
1899                         break;
1900                 }
1901             }
1903             if ($type) {
1904                 array_unshift($parts, implode(' ', array_filter($type)));
1905             }
1907             if (! empty($parts)) {
1908                 if (strlen($current)) {
1909                     $current .= $this->formatter->tagSeparator;
1910                 }
1912                 $current .= implode(' and ', $parts);
1913             }
1914         }
1916         if ($current) {
1917             $out[] = $start . $current;
1918         }
1920         // no @media type except all, and no conflict?
1921         if (! $out && $default) {
1922             $out[] = $default;
1923         }
1925         return $out;
1926     }
1928     /**
1929      * Merge direct relationships between selectors
1930      *
1931      * @param array $selectors1
1932      * @param array $selectors2
1933      *
1934      * @return array
1935      */
1936     protected function mergeDirectRelationships($selectors1, $selectors2)
1937     {
1938         if (empty($selectors1) || empty($selectors2)) {
1939             return array_merge($selectors1, $selectors2);
1940         }
1942         $part1 = end($selectors1);
1943         $part2 = end($selectors2);
1945         if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
1946             return array_merge($selectors1, $selectors2);
1947         }
1949         $merged = [];
1951         do {
1952             $part1 = array_pop($selectors1);
1953             $part2 = array_pop($selectors2);
1955             if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
1956                 if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
1957                     array_unshift($merged, [$part1[0] . $part2[0]]);
1958                     $merged = array_merge($selectors1, $selectors2, $merged);
1959                 } else {
1960                     $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
1961                 }
1963                 break;
1964             }
1966             array_unshift($merged, $part1);
1967         } while (! empty($selectors1) && ! empty($selectors2));
1969         return $merged;
1970     }
1972     /**
1973      * Merge media types
1974      *
1975      * @param array $type1
1976      * @param array $type2
1977      *
1978      * @return array|null
1979      */
1980     protected function mergeMediaTypes($type1, $type2)
1981     {
1982         if (empty($type1)) {
1983             return $type2;
1984         }
1986         if (empty($type2)) {
1987             return $type1;
1988         }
1990         $m1 = '';
1991         $t1 = '';
1993         if (count($type1) > 1) {
1994             $m1= strtolower($type1[0]);
1995             $t1= strtolower($type1[1]);
1996         } else {
1997             $t1 = strtolower($type1[0]);
1998         }
2000         $m2 = '';
2001         $t2 = '';
2003         if (count($type2) > 1) {
2004             $m2 = strtolower($type2[0]);
2005             $t2 = strtolower($type2[1]);
2006         } else {
2007             $t2 = strtolower($type2[0]);
2008         }
2010         if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
2011             if ($t1 === $t2) {
2012                 return null;
2013             }
2015             return [
2016                 $m1 === Type::T_NOT ? $m2 : $m1,
2017                 $m1 === Type::T_NOT ? $t2 : $t1,
2018             ];
2019         }
2021         if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
2022             // CSS has no way of representing "neither screen nor print"
2023             if ($t1 !== $t2) {
2024                 return null;
2025             }
2027             return [Type::T_NOT, $t1];
2028         }
2030         if ($t1 !== $t2) {
2031             return null;
2032         }
2034         // t1 == t2, neither m1 nor m2 are "not"
2035         return [empty($m1)? $m2 : $m1, $t1];
2036     }
2038     /**
2039      * Compile import; returns true if the value was something that could be imported
2040      *
2041      * @param array                                  $rawPath
2042      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2043      * @param boolean                                $once
2044      *
2045      * @return boolean
2046      */
2047     protected function compileImport($rawPath, OutputBlock $out, $once = false)
2048     {
2049         if ($rawPath[0] === Type::T_STRING) {
2050             $path = $this->compileStringContent($rawPath);
2052             if ($path = $this->findImport($path)) {
2053                 if (! $once || ! in_array($path, $this->importedFiles)) {
2054                     $this->importFile($path, $out);
2055                     $this->importedFiles[] = $path;
2056                 }
2058                 return true;
2059             }
2061             return false;
2062         }
2064         if ($rawPath[0] === Type::T_LIST) {
2065             // handle a list of strings
2066             if (count($rawPath[2]) === 0) {
2067                 return false;
2068             }
2070             foreach ($rawPath[2] as $path) {
2071                 if ($path[0] !== Type::T_STRING) {
2072                     return false;
2073                 }
2074             }
2076             foreach ($rawPath[2] as $path) {
2077                 $this->compileImport($path, $out);
2078             }
2080             return true;
2081         }
2083         return false;
2084     }
2087     /**
2088      * Append a root directive like @import or @charset as near as the possible from the source code
2089      * (keeping before comments, @import and @charset coming before in the source code)
2090      *
2091      * @param string                                        $line
2092      * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2093      * @param array                                         $allowed
2094      */
2095     protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2096     {
2097         $root = $out;
2099         while ($root->parent) {
2100             $root = $root->parent;
2101         }
2103         $i = 0;
2105         while ($i < count($root->children)) {
2106             if (! isset($root->children[$i]->type) || ! in_array($root->children[$i]->type, $allowed)) {
2107                 break;
2108             }
2110             $i++;
2111         }
2113         // remove incompatible children from the bottom of the list
2114         $saveChildren = [];
2116         while ($i < count($root->children)) {
2117             $saveChildren[] = array_pop($root->children);
2118         }
2120         // insert the directive as a comment
2121         $child = $this->makeOutputBlock(Type::T_COMMENT);
2122         $child->lines[] = $line;
2123         $child->sourceName = $this->sourceNames[$this->sourceIndex];
2124         $child->sourceLine = $this->sourceLine;
2125         $child->sourceColumn = $this->sourceColumn;
2127         $root->children[] = $child;
2129         // repush children
2130         while (count($saveChildren)) {
2131             $root->children[] = array_pop($saveChildren);
2132         }
2133     }
2135     /**
2136      * Append lines to the courrent output block:
2137      * directly to the block or through a child if necessary
2138      *
2139      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2140      * @param string                                 $type
2141      * @param string                                 $line
2142      */
2143     protected function appendOutputLine(OutputBlock $out, $type, $line)
2144     {
2145         $outWrite = &$out;
2147         if ($type === Type::T_COMMENT) {
2148             $parent = $out->parent;
2150             if (end($parent->children) !== $out) {
2151                 $outWrite = &$parent->children[count($parent->children)-1];
2152             }
2153         }
2155         // check if it's a flat output or not
2156         if (count($out->children)) {
2157             $lastChild = &$out->children[count($out->children) -1];
2159             if ($lastChild->depth === $out->depth && is_null($lastChild->selectors) && ! count($lastChild->children)) {
2160                 $outWrite = $lastChild;
2161             } else {
2162                 $nextLines = $this->makeOutputBlock($type);
2163                 $nextLines->parent = $out;
2164                 $nextLines->depth = $out->depth;
2166                 $out->children[] = $nextLines;
2167                 $outWrite = &$nextLines;
2168             }
2169         }
2171         $outWrite->lines[] = $line;
2172     }
2174     /**
2175      * Compile child; returns a value to halt execution
2176      *
2177      * @param array                                  $child
2178      * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2179      *
2180      * @return array
2181      */
2182     protected function compileChild($child, OutputBlock $out)
2183     {
2184         if (isset($child[Parser::SOURCE_LINE])) {
2185             $this->sourceIndex = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2186             $this->sourceLine = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2187             $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2188         } elseif (is_array($child) && isset($child[1]->sourceLine)) {
2189             $this->sourceIndex = $child[1]->sourceIndex;
2190             $this->sourceLine = $child[1]->sourceLine;
2191             $this->sourceColumn = $child[1]->sourceColumn;
2192         } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2193             $this->sourceLine = $out->sourceLine;
2194             $this->sourceIndex = array_search($out->sourceName, $this->sourceNames);
2196             if ($this->sourceIndex === false) {
2197                 $this->sourceIndex = null;
2198             }
2199         }
2201         switch ($child[0]) {
2202             case Type::T_SCSSPHP_IMPORT_ONCE:
2203                 $rawPath = $this->reduce($child[1]);
2205                 if (! $this->compileImport($rawPath, $out, true)) {
2206                     $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2207                 }
2208                 break;
2210             case Type::T_IMPORT:
2211                 $rawPath = $this->reduce($child[1]);
2213                 if (! $this->compileImport($rawPath, $out)) {
2214                     $this->appendRootDirective('@import ' . $this->compileValue($rawPath) . ';', $out);
2215                 }
2216                 break;
2218             case Type::T_DIRECTIVE:
2219                 $this->compileDirective($child[1]);
2220                 break;
2222             case Type::T_AT_ROOT:
2223                 $this->compileAtRoot($child[1]);
2224                 break;
2226             case Type::T_MEDIA:
2227                 $this->compileMedia($child[1]);
2228                 break;
2230             case Type::T_BLOCK:
2231                 $this->compileBlock($child[1]);
2232                 break;
2234             case Type::T_CHARSET:
2235                 if (! $this->charsetSeen) {
2236                     $this->charsetSeen = true;
2237                     $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
2238                 }
2239                 break;
2241             case Type::T_ASSIGN:
2242                 list(, $name, $value) = $child;
2244                 if ($name[0] === Type::T_VARIABLE) {
2245                     $flags = isset($child[3]) ? $child[3] : [];
2246                     $isDefault = in_array('!default', $flags);
2247                     $isGlobal = in_array('!global', $flags);
2249                     if ($isGlobal) {
2250                         $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2251                         break;
2252                     }
2254                     $shouldSet = $isDefault &&
2255                         (($result = $this->get($name[1], false)) === null ||
2256                         $result === static::$null);
2258                     if (! $isDefault || $shouldSet) {
2259                         $this->set($name[1], $this->reduce($value), true, null, $value);
2260                     }
2261                     break;
2262                 }
2264                 $compiledName = $this->compileValue($name);
2266                 // handle shorthand syntax: size / line-height
2267                 if ($compiledName === 'font' || $compiledName === 'grid-row' || $compiledName === 'grid-column') {
2268                     if ($value[0] === Type::T_VARIABLE) {
2269                         // if the font value comes from variable, the content is already reduced
2270                         // (i.e., formulas were already calculated), so we need the original unreduced value
2271                         $value = $this->get($value[1], true, null, true);
2272                     }
2274                     $fontValue=&$value;
2276                     if ($value[0] === Type::T_LIST && $value[1]==',') {
2277                         // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2278                         // we need to handle the first list element
2279                         $fontValue=&$value[2][0];
2280                     }
2282                     if ($fontValue[0] === Type::T_EXPRESSION && $fontValue[1] === '/') {
2283                         $fontValue = $this->expToString($fontValue);
2284                     } elseif ($fontValue[0] === Type::T_LIST) {
2285                         foreach ($fontValue[2] as &$item) {
2286                             if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2287                                 $item = $this->expToString($item);
2288                             }
2289                         }
2290                     }
2291                 }
2293                 // if the value reduces to null from something else then
2294                 // the property should be discarded
2295                 if ($value[0] !== Type::T_NULL) {
2296                     $value = $this->reduce($value);
2298                     if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2299                         break;
2300                     }
2301                 }
2303                 $compiledValue = $this->compileValue($value);
2305                 $line = $this->formatter->property(
2306                     $compiledName,
2307                     $compiledValue
2308                 );
2309                 $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2310                 break;
2312             case Type::T_COMMENT:
2313                 if ($out->type === Type::T_ROOT) {
2314                     $this->compileComment($child);
2315                     break;
2316                 }
2318                 $this->appendOutputLine($out, Type::T_COMMENT, $child[1]);
2319                 break;
2321             case Type::T_MIXIN:
2322             case Type::T_FUNCTION:
2323                 list(, $block) = $child;
2325                 $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
2326                 break;
2328             case Type::T_EXTEND:
2329                 foreach ($child[1] as $sel) {
2330                     $results = $this->evalSelectors([$sel]);
2332                     foreach ($results as $result) {
2333                         // only use the first one
2334                         $result = current($result);
2336                         $this->pushExtends($result, $out->selectors, $child);
2337                     }
2338                 }
2339                 break;
2341             case Type::T_IF:
2342                 list(, $if) = $child;
2344                 if ($this->isTruthy($this->reduce($if->cond, true))) {
2345                     return $this->compileChildren($if->children, $out);
2346                 }
2348                 foreach ($if->cases as $case) {
2349                     if ($case->type === Type::T_ELSE ||
2350                         $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
2351                     ) {
2352                         return $this->compileChildren($case->children, $out);
2353                     }
2354                 }
2355                 break;
2357             case Type::T_EACH:
2358                 list(, $each) = $child;
2360                 $list = $this->coerceList($this->reduce($each->list));
2362                 $this->pushEnv();
2364                 foreach ($list[2] as $item) {
2365                     if (count($each->vars) === 1) {
2366                         $this->set($each->vars[0], $item, true);
2367                     } else {
2368                         list(,, $values) = $this->coerceList($item);
2370                         foreach ($each->vars as $i => $var) {
2371                             $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
2372                         }
2373                     }
2375                     $ret = $this->compileChildren($each->children, $out);
2377                     if ($ret) {
2378                         if ($ret[0] !== Type::T_CONTROL) {
2379                             $this->popEnv();
2381                             return $ret;
2382                         }
2384                         if ($ret[1]) {
2385                             break;
2386                         }
2387                     }
2388                 }
2390                 $this->popEnv();
2391                 break;
2393             case Type::T_WHILE:
2394                 list(, $while) = $child;
2396                 while ($this->isTruthy($this->reduce($while->cond, true))) {
2397                     $ret = $this->compileChildren($while->children, $out);
2399                     if ($ret) {
2400                         if ($ret[0] !== Type::T_CONTROL) {
2401                             return $ret;
2402                         }
2404                         if ($ret[1]) {
2405                             break;
2406                         }
2407                     }
2408                 }
2409                 break;
2411             case Type::T_FOR:
2412                 list(, $for) = $child;
2414                 $start = $this->reduce($for->start, true);
2415                 $end   = $this->reduce($for->end, true);
2417                 if (! ($start[2] == $end[2] || $end->unitless())) {
2418                     $this->throwError('Incompatible units: "%s" and "%s".', $start->unitStr(), $end->unitStr());
2420                     break;
2421                 }
2423                 $unit  = $start[2];
2424                 $start = $start[1];
2425                 $end   = $end[1];
2427                 $d = $start < $end ? 1 : -1;
2429                 for (;;) {
2430                     if ((! $for->until && $start - $d == $end) ||
2431                         ($for->until && $start == $end)
2432                     ) {
2433                         break;
2434                     }
2436                     $this->set($for->var, new Node\Number($start, $unit));
2437                     $start += $d;
2439                     $ret = $this->compileChildren($for->children, $out);
2441                     if ($ret) {
2442                         if ($ret[0] !== Type::T_CONTROL) {
2443                             return $ret;
2444                         }
2446                         if ($ret[1]) {
2447                             break;
2448                         }
2449                     }
2450                 }
2451                 break;
2453             case Type::T_BREAK:
2454                 return [Type::T_CONTROL, true];
2456             case Type::T_CONTINUE:
2457                 return [Type::T_CONTROL, false];
2459             case Type::T_RETURN:
2460                 return $this->reduce($child[1], true);
2462             case Type::T_NESTED_PROPERTY:
2463                 $this->compileNestedPropertiesBlock($child[1], $out);
2464                 break;
2466             case Type::T_INCLUDE:
2467                 // including a mixin
2468                 list(, $name, $argValues, $content) = $child;
2470                 $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
2472                 if (! $mixin) {
2473                     $this->throwError("Undefined mixin $name");
2474                     break;
2475                 }
2477                 $callingScope = $this->getStoreEnv();
2479                 // push scope, apply args
2480                 $this->pushEnv();
2481                 $this->env->depth--;
2483                 $storeEnv = $this->storeEnv;
2484                 $this->storeEnv = $this->env;
2486                 // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
2487                 // and assign this fake parent to childs
2488                 $selfParent = null;
2490                 if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
2491                     $selfParent = $child['selfParent'];
2492                 } else {
2493                     $parentSelectors = $this->multiplySelectors($this->env);
2495                     if ($parentSelectors) {
2496                         $parent = new Block();
2497                         $parent->selectors = $parentSelectors;
2499                         foreach ($mixin->children as $k => $child) {
2500                             if (isset($child[1]) && is_object($child[1]) && $child[1] instanceof Block) {
2501                                 $mixin->children[$k][1]->parent = $parent;
2502                             }
2503                         }
2504                     }
2505                 }
2507                 // clone the stored content to not have its scope spoiled by a further call to the same mixin
2508                 // i.e., recursive @include of the same mixin
2509                 if (isset($content)) {
2510                     $copyContent = clone $content;
2511                     $copyContent->scope = $callingScope;
2513                     $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
2514                 } else {
2515                     $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
2516                 }
2518                 if (isset($mixin->args)) {
2519                     $this->applyArguments($mixin->args, $argValues);
2520                 }
2522                 $this->env->marker = 'mixin';
2524                 $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . " " . $name);
2526                 $this->storeEnv = $storeEnv;
2528                 $this->popEnv();
2529                 break;
2531             case Type::T_MIXIN_CONTENT:
2532                 $env = isset($this->storeEnv) ? $this->storeEnv : $this->env;
2533                 $content = $this->get(static::$namespaces['special'] . 'content', false, $env);
2535                 if (! $content) {
2536                     $content = new \stdClass();
2537                     $content->scope = new \stdClass();
2538                     $content->children = $env->parent->block->children;
2539                     break;
2540                 }
2542                 $storeEnv = $this->storeEnv;
2543                 $this->storeEnv = $content->scope;
2544                 $this->compileChildrenNoReturn($content->children, $out);
2546                 $this->storeEnv = $storeEnv;
2547                 break;
2549             case Type::T_DEBUG:
2550                 list(, $value) = $child;
2552                 $fname = $this->sourceNames[$this->sourceIndex];
2553                 $line = $this->sourceLine;
2554                 $value = $this->compileValue($this->reduce($value, true));
2555                 fwrite($this->stderr, "File $fname on line $line DEBUG: $value\n");
2556                 break;
2558             case Type::T_WARN:
2559                 list(, $value) = $child;
2561                 $fname = $this->sourceNames[$this->sourceIndex];
2562                 $line = $this->sourceLine;
2563                 $value = $this->compileValue($this->reduce($value, true));
2564                 fwrite($this->stderr, "File $fname on line $line WARN: $value\n");
2565                 break;
2567             case Type::T_ERROR:
2568                 list(, $value) = $child;
2570                 $fname = $this->sourceNames[$this->sourceIndex];
2571                 $line = $this->sourceLine;
2572                 $value = $this->compileValue($this->reduce($value, true));
2573                 $this->throwError("File $fname on line $line ERROR: $value\n");
2574                 break;
2576             case Type::T_CONTROL:
2577                 $this->throwError('@break/@continue not permitted in this scope');
2578                 break;
2580             default:
2581                 $this->throwError("unknown child type: $child[0]");
2582         }
2583     }
2585     /**
2586      * Reduce expression to string
2587      *
2588      * @param array $exp
2589      *
2590      * @return array
2591      */
2592     protected function expToString($exp)
2593     {
2594         list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
2596         $content = [$this->reduce($left)];
2598         if ($whiteLeft) {
2599             $content[] = ' ';
2600         }
2602         $content[] = $op;
2604         if ($whiteRight) {
2605             $content[] = ' ';
2606         }
2608         $content[] = $this->reduce($right);
2610         return [Type::T_STRING, '', $content];
2611     }
2613     /**
2614      * Is truthy?
2615      *
2616      * @param array $value
2617      *
2618      * @return boolean
2619      */
2620     protected function isTruthy($value)
2621     {
2622         return $value !== static::$false && $value !== static::$null;
2623     }
2625     /**
2626      * Is the value a direct relationship combinator?
2627      *
2628      * @param string $value
2629      *
2630      * @return boolean
2631      */
2632     protected function isImmediateRelationshipCombinator($value)
2633     {
2634         return $value === '>' || $value === '+' || $value === '~';
2635     }
2637     /**
2638      * Should $value cause its operand to eval
2639      *
2640      * @param array $value
2641      *
2642      * @return boolean
2643      */
2644     protected function shouldEval($value)
2645     {
2646         switch ($value[0]) {
2647             case Type::T_EXPRESSION:
2648                 if ($value[1] === '/') {
2649                     return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
2650                 }
2652                 // fall-thru
2653             case Type::T_VARIABLE:
2654             case Type::T_FUNCTION_CALL:
2655                 return true;
2656         }
2658         return false;
2659     }
2661     /**
2662      * Reduce value
2663      *
2664      * @param array   $value
2665      * @param boolean $inExp
2666      *
2667      * @return null|array|\ScssPhp\ScssPhp\Node\Number
2668      */
2669     protected function reduce($value, $inExp = false)
2670     {
2672         if (is_null($value)) {
2673             return null;
2674         }
2676         switch ($value[0]) {
2677             case Type::T_EXPRESSION:
2678                 list(, $op, $left, $right, $inParens) = $value;
2680                 $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
2681                 $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
2683                 $left = $this->reduce($left, true);
2685                 if ($op !== 'and' && $op !== 'or') {
2686                     $right = $this->reduce($right, true);
2687                 }
2689                 // special case: looks like css shorthand
2690                 if ($opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
2691                     (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
2692                     ($right[0] === Type::T_NUMBER && ! $right->unitless()))
2693                 ) {
2694                     return $this->expToString($value);
2695                 }
2697                 $left = $this->coerceForExpression($left);
2698                 $right = $this->coerceForExpression($right);
2700                 $ltype = $left[0];
2701                 $rtype = $right[0];
2703                 $ucOpName = ucfirst($opName);
2704                 $ucLType  = ucfirst($ltype);
2705                 $ucRType  = ucfirst($rtype);
2707                 // this tries:
2708                 // 1. op[op name][left type][right type]
2709                 // 2. op[left type][right type] (passing the op as first arg
2710                 // 3. op[op name]
2711                 $fn = "op${ucOpName}${ucLType}${ucRType}";
2713                 if (is_callable([$this, $fn]) ||
2714                     (($fn = "op${ucLType}${ucRType}") &&
2715                         is_callable([$this, $fn]) &&
2716                         $passOp = true) ||
2717                     (($fn = "op${ucOpName}") &&
2718                         is_callable([$this, $fn]) &&
2719                         $genOp = true)
2720                 ) {
2721                     $coerceUnit = false;
2723                     if (! isset($genOp) &&
2724                         $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
2725                     ) {
2726                         $coerceUnit = true;
2728                         switch ($opName) {
2729                             case 'mul':
2730                                 $targetUnit = $left[2];
2732                                 foreach ($right[2] as $unit => $exp) {
2733                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
2734                                 }
2735                                 break;
2737                             case 'div':
2738                                 $targetUnit = $left[2];
2740                                 foreach ($right[2] as $unit => $exp) {
2741                                     $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
2742                                 }
2743                                 break;
2745                             case 'mod':
2746                                 $targetUnit = $left[2];
2747                                 break;
2749                             default:
2750                                 $targetUnit = $left->unitless() ? $right[2] : $left[2];
2751                         }
2753                         if (! $left->unitless() && ! $right->unitless()) {
2754                             $left = $left->normalize();
2755                             $right = $right->normalize();
2756                         }
2757                     }
2759                     $shouldEval = $inParens || $inExp;
2761                     if (isset($passOp)) {
2762                         $out = $this->$fn($op, $left, $right, $shouldEval);
2763                     } else {
2764                         $out = $this->$fn($left, $right, $shouldEval);
2765                     }
2767                     if (isset($out)) {
2768                         if ($coerceUnit && $out[0] === Type::T_NUMBER) {
2769                             $out = $out->coerce($targetUnit);
2770                         }
2772                         return $out;
2773                     }
2774                 }
2776                 return $this->expToString($value);
2778             case Type::T_UNARY:
2779                 list(, $op, $exp, $inParens) = $value;
2781                 $inExp = $inExp || $this->shouldEval($exp);
2782                 $exp = $this->reduce($exp);
2784                 if ($exp[0] === Type::T_NUMBER) {
2785                     switch ($op) {
2786                         case '+':
2787                             return new Node\Number($exp[1], $exp[2]);
2789                         case '-':
2790                             return new Node\Number(-$exp[1], $exp[2]);
2791                     }
2792                 }
2794                 if ($op === 'not') {
2795                     if ($inExp || $inParens) {
2796                         if ($exp === static::$false || $exp === static::$null) {
2797                             return static::$true;
2798                         }
2800                         return static::$false;
2801                     }
2803                     $op = $op . ' ';
2804                 }
2806                 return [Type::T_STRING, '', [$op, $exp]];
2808             case Type::T_VARIABLE:
2809                 return $this->reduce($this->get($value[1]));
2811             case Type::T_LIST:
2812                 foreach ($value[2] as &$item) {
2813                     $item = $this->reduce($item);
2814                 }
2816                 return $value;
2818             case Type::T_MAP:
2819                 foreach ($value[1] as &$item) {
2820                     $item = $this->reduce($item);
2821                 }
2823                 foreach ($value[2] as &$item) {
2824                     $item = $this->reduce($item);
2825                 }
2827                 return $value;
2829             case Type::T_STRING:
2830                 foreach ($value[2] as &$item) {
2831                     if (is_array($item) || $item instanceof \ArrayAccess) {
2832                         $item = $this->reduce($item);
2833                     }
2834                 }
2836                 return $value;
2838             case Type::T_INTERPOLATE:
2839                 $value[1] = $this->reduce($value[1]);
2840                 if ($inExp) {
2841                     return $value[1];
2842                 }
2844                 return $value;
2846             case Type::T_FUNCTION_CALL:
2847                 return $this->fncall($value[1], $value[2]);
2849             case Type::T_SELF:
2850                 $selfSelector = $this->multiplySelectors($this->env);
2851                 $selfSelector = $this->collapseSelectors($selfSelector, true);
2852                 return $selfSelector;
2854             default:
2855                 return $value;
2856         }
2857     }
2859     /**
2860      * Function caller
2861      *
2862      * @param string $name
2863      * @param array  $argValues
2864      *
2865      * @return array|null
2866      */
2867     protected function fncall($name, $argValues)
2868     {
2869         // SCSS @function
2870         if ($this->callScssFunction($name, $argValues, $returnValue)) {
2871             return $returnValue;
2872         }
2874         // native PHP functions
2875         if ($this->callNativeFunction($name, $argValues, $returnValue)) {
2876             return $returnValue;
2877         }
2879         // for CSS functions, simply flatten the arguments into a list
2880         $listArgs = [];
2882         foreach ((array) $argValues as $arg) {
2883             if (empty($arg[0])) {
2884                 $listArgs[] = $this->reduce($arg[1]);
2885             }
2886         }
2888         return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', $listArgs]];
2889     }
2891     /**
2892      * Normalize name
2893      *
2894      * @param string $name
2895      *
2896      * @return string
2897      */
2898     protected function normalizeName($name)
2899     {
2900         return str_replace('-', '_', $name);
2901     }
2903     /**
2904      * Normalize value
2905      *
2906      * @param array $value
2907      *
2908      * @return array
2909      */
2910     public function normalizeValue($value)
2911     {
2912         $value = $this->coerceForExpression($this->reduce($value));
2914         switch ($value[0]) {
2915             case Type::T_LIST:
2916                 $value = $this->extractInterpolation($value);
2918                 if ($value[0] !== Type::T_LIST) {
2919                     return [Type::T_KEYWORD, $this->compileValue($value)];
2920                 }
2922                 foreach ($value[2] as $key => $item) {
2923                     $value[2][$key] = $this->normalizeValue($item);
2924                 }
2926                 return $value;
2928             case Type::T_STRING:
2929                 return [$value[0], '"', [$this->compileStringContent($value)]];
2931             case Type::T_NUMBER:
2932                 return $value->normalize();
2934             case Type::T_INTERPOLATE:
2935                 return [Type::T_KEYWORD, $this->compileValue($value)];
2937             default:
2938                 return $value;
2939         }
2940     }
2942     /**
2943      * Add numbers
2944      *
2945      * @param array $left
2946      * @param array $right
2947      *
2948      * @return \ScssPhp\ScssPhp\Node\Number
2949      */
2950     protected function opAddNumberNumber($left, $right)
2951     {
2952         return new Node\Number($left[1] + $right[1], $left[2]);
2953     }
2955     /**
2956      * Multiply numbers
2957      *
2958      * @param array $left
2959      * @param array $right
2960      *
2961      * @return \ScssPhp\ScssPhp\Node\Number
2962      */
2963     protected function opMulNumberNumber($left, $right)
2964     {
2965         return new Node\Number($left[1] * $right[1], $left[2]);
2966     }
2968     /**
2969      * Subtract numbers
2970      *
2971      * @param array $left
2972      * @param array $right
2973      *
2974      * @return \ScssPhp\ScssPhp\Node\Number
2975      */
2976     protected function opSubNumberNumber($left, $right)
2977     {
2978         return new Node\Number($left[1] - $right[1], $left[2]);
2979     }
2981     /**
2982      * Divide numbers
2983      *
2984      * @param array $left
2985      * @param array $right
2986      *
2987      * @return array|\ScssPhp\ScssPhp\Node\Number
2988      */
2989     protected function opDivNumberNumber($left, $right)
2990     {
2991         if ($right[1] == 0) {
2992             return [Type::T_STRING, '', [$left[1] . $left[2] . '/' . $right[1] . $right[2]]];
2993         }
2995         return new Node\Number($left[1] / $right[1], $left[2]);
2996     }
2998     /**
2999      * Mod numbers
3000      *
3001      * @param array $left
3002      * @param array $right
3003      *
3004      * @return \ScssPhp\ScssPhp\Node\Number
3005      */
3006     protected function opModNumberNumber($left, $right)
3007     {
3008         return new Node\Number($left[1] % $right[1], $left[2]);
3009     }
3011     /**
3012      * Add strings
3013      *
3014      * @param array $left
3015      * @param array $right
3016      *
3017      * @return array|null
3018      */
3019     protected function opAdd($left, $right)
3020     {
3021         if ($strLeft = $this->coerceString($left)) {
3022             if ($right[0] === Type::T_STRING) {
3023                 $right[1] = '';
3024             }
3026             $strLeft[2][] = $right;
3028             return $strLeft;
3029         }
3031         if ($strRight = $this->coerceString($right)) {
3032             if ($left[0] === Type::T_STRING) {
3033                 $left[1] = '';
3034             }
3036             array_unshift($strRight[2], $left);
3038             return $strRight;
3039         }
3041         return null;
3042     }
3044     /**
3045      * Boolean and
3046      *
3047      * @param array   $left
3048      * @param array   $right
3049      * @param boolean $shouldEval
3050      *
3051      * @return array|null
3052      */
3053     protected function opAnd($left, $right, $shouldEval)
3054     {
3055         $truthy = ($left === static::$null || $right === static::$null) ||
3056                   ($left === static::$false || $left === static::$true) &&
3057                   ($right === static::$false || $right === static::$true);
3059         if (! $shouldEval) {
3060             if (! $truthy) {
3061                 return null;
3062             }
3063         }
3065         if ($left !== static::$false && $left !== static::$null) {
3066             return $this->reduce($right, true);
3067         }
3069         return $left;
3070     }
3072     /**
3073      * Boolean or
3074      *
3075      * @param array   $left
3076      * @param array   $right
3077      * @param boolean $shouldEval
3078      *
3079      * @return array|null
3080      */
3081     protected function opOr($left, $right, $shouldEval)
3082     {
3083         $truthy = ($left === static::$null || $right === static::$null) ||
3084                   ($left === static::$false || $left === static::$true) &&
3085                   ($right === static::$false || $right === static::$true);
3087         if (! $shouldEval) {
3088             if (! $truthy) {
3089                 return null;
3090             }
3091         }
3093         if ($left !== static::$false && $left !== static::$null) {
3094             return $left;
3095         }
3097         return $this->reduce($right, true);
3098     }
3100     /**
3101      * Compare colors
3102      *
3103      * @param string $op
3104      * @param array  $left
3105      * @param array  $right
3106      *
3107      * @return array
3108      */
3109     protected function opColorColor($op, $left, $right)
3110     {
3111         $out = [Type::T_COLOR];
3113         foreach ([1, 2, 3] as $i) {
3114             $lval = isset($left[$i]) ? $left[$i] : 0;
3115             $rval = isset($right[$i]) ? $right[$i] : 0;
3117             switch ($op) {
3118                 case '+':
3119                     $out[] = $lval + $rval;
3120                     break;
3122                 case '-':
3123                     $out[] = $lval - $rval;
3124                     break;
3126                 case '*':
3127                     $out[] = $lval * $rval;
3128                     break;
3130                 case '%':
3131                     $out[] = $lval % $rval;
3132                     break;
3134                 case '/':
3135                     if ($rval == 0) {
3136                         $this->throwError("color: Can't divide by zero");
3137                         break 2;
3138                     }
3140                     $out[] = (int) ($lval / $rval);
3141                     break;
3143                 case '==':
3144                     return $this->opEq($left, $right);
3146                 case '!=':
3147                     return $this->opNeq($left, $right);
3149                 default:
3150                     $this->throwError("color: unknown op $op");
3151                     break 2;
3152             }
3153         }
3155         if (isset($left[4])) {
3156             $out[4] = $left[4];
3157         } elseif (isset($right[4])) {
3158             $out[4] = $right[4];
3159         }
3161         return $this->fixColor($out);
3162     }
3164     /**
3165      * Compare color and number
3166      *
3167      * @param string $op
3168      * @param array  $left
3169      * @param array  $right
3170      *
3171      * @return array
3172      */
3173     protected function opColorNumber($op, $left, $right)
3174     {
3175         $value = $right[1];
3177         return $this->opColorColor(
3178             $op,
3179             $left,
3180             [Type::T_COLOR, $value, $value, $value]
3181         );
3182     }
3184     /**
3185      * Compare number and color
3186      *
3187      * @param string $op
3188      * @param array  $left
3189      * @param array  $right
3190      *
3191      * @return array
3192      */
3193     protected function opNumberColor($op, $left, $right)
3194     {
3195         $value = $left[1];
3197         return $this->opColorColor(
3198             $op,
3199             [Type::T_COLOR, $value, $value, $value],
3200             $right
3201         );
3202     }
3204     /**
3205      * Compare number1 == number2
3206      *
3207      * @param array $left
3208      * @param array $right
3209      *
3210      * @return array
3211      */
3212     protected function opEq($left, $right)
3213     {
3214         if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3215             $lStr[1] = '';
3216             $rStr[1] = '';
3218             $left = $this->compileValue($lStr);
3219             $right = $this->compileValue($rStr);
3220         }
3222         return $this->toBool($left === $right);
3223     }
3225     /**
3226      * Compare number1 != number2
3227      *
3228      * @param array $left
3229      * @param array $right
3230      *
3231      * @return array
3232      */
3233     protected function opNeq($left, $right)
3234     {
3235         if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3236             $lStr[1] = '';
3237             $rStr[1] = '';
3239             $left = $this->compileValue($lStr);
3240             $right = $this->compileValue($rStr);
3241         }
3243         return $this->toBool($left !== $right);
3244     }
3246     /**
3247      * Compare number1 >= number2
3248      *
3249      * @param array $left
3250      * @param array $right
3251      *
3252      * @return array
3253      */
3254     protected function opGteNumberNumber($left, $right)
3255     {
3256         return $this->toBool($left[1] >= $right[1]);
3257     }
3259     /**
3260      * Compare number1 > number2
3261      *
3262      * @param array $left
3263      * @param array $right
3264      *
3265      * @return array
3266      */
3267     protected function opGtNumberNumber($left, $right)
3268     {
3269         return $this->toBool($left[1] > $right[1]);
3270     }
3272     /**
3273      * Compare number1 <= number2
3274      *
3275      * @param array $left
3276      * @param array $right
3277      *
3278      * @return array
3279      */
3280     protected function opLteNumberNumber($left, $right)
3281     {
3282         return $this->toBool($left[1] <= $right[1]);
3283     }
3285     /**
3286      * Compare number1 < number2
3287      *
3288      * @param array $left
3289      * @param array $right
3290      *
3291      * @return array
3292      */
3293     protected function opLtNumberNumber($left, $right)
3294     {
3295         return $this->toBool($left[1] < $right[1]);
3296     }
3298     /**
3299      * Three-way comparison, aka spaceship operator
3300      *
3301      * @param array $left
3302      * @param array $right
3303      *
3304      * @return \ScssPhp\ScssPhp\Node\Number
3305      */
3306     protected function opCmpNumberNumber($left, $right)
3307     {
3308         $n = $left[1] - $right[1];
3310         return new Node\Number($n ? $n / abs($n) : 0, '');
3311     }
3313     /**
3314      * Cast to boolean
3315      *
3316      * @api
3317      *
3318      * @param mixed $thing
3319      *
3320      * @return array
3321      */
3322     public function toBool($thing)
3323     {
3324         return $thing ? static::$true : static::$false;
3325     }
3327     /**
3328      * Compiles a primitive value into a CSS property value.
3329      *
3330      * Values in scssphp are typed by being wrapped in arrays, their format is
3331      * typically:
3332      *
3333      *     array(type, contents [, additional_contents]*)
3334      *
3335      * The input is expected to be reduced. This function will not work on
3336      * things like expressions and variables.
3337      *
3338      * @api
3339      *
3340      * @param array $value
3341      *
3342      * @return string
3343      */
3344     public function compileValue($value)
3345     {
3346         $value = $this->reduce($value);
3348         switch ($value[0]) {
3349             case Type::T_KEYWORD:
3350                 return $value[1];
3352             case Type::T_COLOR:
3353                 // [1] - red component (either number for a %)
3354                 // [2] - green component
3355                 // [3] - blue component
3356                 // [4] - optional alpha component
3357                 list(, $r, $g, $b) = $value;
3359                 $r = round($r);
3360                 $g = round($g);
3361                 $b = round($b);
3363                 if (count($value) === 5 && $value[4] !== 1) { // rgba
3364                     $a = new Node\Number($value[4], '');
3366                     return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
3367                 }
3369                 $h = sprintf('#%02x%02x%02x', $r, $g, $b);
3371                 // Converting hex color to short notation (e.g. #003399 to #039)
3372                 if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
3373                     $h = '#' . $h[1] . $h[3] . $h[5];
3374                 }
3376                 return $h;
3378             case Type::T_NUMBER:
3379                 return $value->output($this);
3381             case Type::T_STRING:
3382                 return $value[1] . $this->compileStringContent($value) . $value[1];
3384             case Type::T_FUNCTION:
3385                 $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
3387                 return "$value[1]($args)";
3389             case Type::T_LIST:
3390                 $value = $this->extractInterpolation($value);
3392                 if ($value[0] !== Type::T_LIST) {
3393                     return $this->compileValue($value);
3394                 }
3396                 list(, $delim, $items) = $value;
3398                 if ($delim !== ' ') {
3399                     $delim .= ' ';
3400                 }
3402                 $filtered = [];
3404                 foreach ($items as $item) {
3405                     if ($item[0] === Type::T_NULL) {
3406                         continue;
3407                     }
3409                     $filtered[] = $this->compileValue($item);
3410                 }
3412                 return implode("$delim", $filtered);
3414             case Type::T_MAP:
3415                 $keys = $value[1];
3416                 $values = $value[2];
3417                 $filtered = [];
3419                 for ($i = 0, $s = count($keys); $i < $s; $i++) {
3420                     $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
3421                 }
3423                 array_walk($filtered, function (&$value, $key) {
3424                     $value = $key . ': ' . $value;
3425                 });
3427                 return '(' . implode(', ', $filtered) . ')';
3429             case Type::T_INTERPOLATED:
3430                 // node created by extractInterpolation
3431                 list(, $interpolate, $left, $right) = $value;
3432                 list(,, $whiteLeft, $whiteRight) = $interpolate;
3434                 $left = count($left[2]) > 0 ?
3435                     $this->compileValue($left) . $whiteLeft : '';
3437                 $right = count($right[2]) > 0 ?
3438                     $whiteRight . $this->compileValue($right) : '';
3440                 return $left . $this->compileValue($interpolate) . $right;
3442             case Type::T_INTERPOLATE:
3443                 // strip quotes if it's a string
3444                 $reduced = $this->reduce($value[1]);
3446                 switch ($reduced[0]) {
3447                     case Type::T_LIST:
3448                         $reduced = $this->extractInterpolation($reduced);
3450                         if ($reduced[0] !== Type::T_LIST) {
3451                             break;
3452                         }
3454                         list(, $delim, $items) = $reduced;
3456                         if ($delim !== ' ') {
3457                             $delim .= ' ';
3458                         }
3460                         $filtered = [];
3462                         foreach ($items as $item) {
3463                             if ($item[0] === Type::T_NULL) {
3464                                 continue;
3465                             }
3467                             $temp = $this->compileValue([Type::T_KEYWORD, $item]);
3468                             if ($temp[0] === Type::T_STRING) {
3469                                 $filtered[] = $this->compileStringContent($temp);
3470                             } elseif ($temp[0] === Type::T_KEYWORD) {
3471                                 $filtered[] = $temp[1];
3472                             } else {
3473                                 $filtered[] = $this->compileValue($temp);
3474                             }
3475                         }
3477                         $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
3478                         break;
3480                     case Type::T_STRING:
3481                         $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
3482                         break;
3484                     case Type::T_NULL:
3485                         $reduced = [Type::T_KEYWORD, ''];
3486                 }
3488                 return $this->compileValue($reduced);
3490             case Type::T_NULL:
3491                 return 'null';
3493             default:
3494                 $this->throwError("unknown value type: ".json_encode($value));
3495         }
3496     }
3498     /**
3499      * Flatten list
3500      *
3501      * @param array $list
3502      *
3503      * @return string
3504      */
3505     protected function flattenList($list)
3506     {
3507         return $this->compileValue($list);
3508     }
3510     /**
3511      * Compile string content
3512      *
3513      * @param array $string
3514      *
3515      * @return string
3516      */
3517     protected function compileStringContent($string)
3518     {
3519         $parts = [];
3521         foreach ($string[2] as $part) {
3522             if (is_array($part) || $part instanceof \ArrayAccess) {
3523                 $parts[] = $this->compileValue($part);
3524             } else {
3525                 $parts[] = $part;
3526             }
3527         }
3529         return implode($parts);
3530     }
3532     /**
3533      * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
3534      *
3535      * @param array $list
3536      *
3537      * @return array
3538      */
3539     protected function extractInterpolation($list)
3540     {
3541         $items = $list[2];
3543         foreach ($items as $i => $item) {
3544             if ($item[0] === Type::T_INTERPOLATE) {
3545                 $before = [Type::T_LIST, $list[1], array_slice($items, 0, $i)];
3546                 $after  = [Type::T_LIST, $list[1], array_slice($items, $i + 1)];
3548                 return [Type::T_INTERPOLATED, $item, $before, $after];
3549             }
3550         }
3552         return $list;
3553     }
3555     /**
3556      * Find the final set of selectors
3557      *
3558      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3559      * @param \ScssPhp\ScssPhp\Block                $selfParent
3560      *
3561      * @return array
3562      */
3563     protected function multiplySelectors(Environment $env, $selfParent = null)
3564     {
3565         $envs            = $this->compactEnv($env);
3566         $selectors       = [];
3567         $parentSelectors = [[]];
3569         $selfParentSelectors = null;
3571         if (! is_null($selfParent) && $selfParent->selectors) {
3572             $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
3573         }
3575         while ($env = array_pop($envs)) {
3576             if (empty($env->selectors)) {
3577                 continue;
3578             }
3580             $selectors = $env->selectors;
3582             do {
3583                 $stillHasSelf = false;
3584                 $prevSelectors = $selectors;
3585                 $selectors = [];
3587                 foreach ($prevSelectors as $selector) {
3588                     foreach ($parentSelectors as $parent) {
3589                         if ($selfParentSelectors) {
3590                             foreach ($selfParentSelectors as $selfParent) {
3591                                 // if no '&' in the selector, each call will give same result, only add once
3592                                 $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
3593                                 $selectors[serialize($s)] = $s;
3594                             }
3595                         } else {
3596                             $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
3597                             $selectors[serialize($s)] = $s;
3598                         }
3599                     }
3600                 }
3601             } while ($stillHasSelf);
3603             $parentSelectors = $selectors;
3604         }
3606         $selectors = array_values($selectors);
3608         return $selectors;
3609     }
3611     /**
3612      * Join selectors; looks for & to replace, or append parent before child
3613      *
3614      * @param array   $parent
3615      * @param array   $child
3616      * @param boolean &$stillHasSelf
3617      * @param array   $selfParentSelectors
3619      * @return array
3620      */
3621     protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
3622     {
3623         $setSelf = false;
3624         $out = [];
3626         foreach ($child as $part) {
3627             $newPart = [];
3629             foreach ($part as $p) {
3630                 // only replace & once and should be recalled to be able to make combinations
3631                 if ($p === static::$selfSelector && $setSelf) {
3632                     $stillHasSelf = true;
3633                 }
3635                 if ($p === static::$selfSelector && ! $setSelf) {
3636                     $setSelf = true;
3638                     if (is_null($selfParentSelectors)) {
3639                         $selfParentSelectors = $parent;
3640                     }
3642                     foreach ($selfParentSelectors as $i => $parentPart) {
3643                         if ($i > 0) {
3644                             $out[] = $newPart;
3645                             $newPart = [];
3646                         }
3648                         foreach ($parentPart as $pp) {
3649                             if (is_array($pp)) {
3650                                 $flatten = [];
3651                                 array_walk_recursive($pp, function ($a) use (&$flatten) {
3652                                     $flatten[] = $a;
3653                                 });
3654                                 $pp = implode($flatten);
3655                             }
3657                             $newPart[] = $pp;
3658                         }
3659                     }
3660                 } else {
3661                     $newPart[] = $p;
3662                 }
3663             }
3665             $out[] = $newPart;
3666         }
3668         return $setSelf ? $out : array_merge($parent, $child);
3669     }
3671     /**
3672      * Multiply media
3673      *
3674      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3675      * @param array                                 $childQueries
3676      *
3677      * @return array
3678      */
3679     protected function multiplyMedia(Environment $env = null, $childQueries = null)
3680     {
3681         if (! isset($env) ||
3682             ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
3683         ) {
3684             return $childQueries;
3685         }
3687         // plain old block, skip
3688         if (empty($env->block->type)) {
3689             return $this->multiplyMedia($env->parent, $childQueries);
3690         }
3692         $parentQueries = isset($env->block->queryList)
3693             ? $env->block->queryList
3694             : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
3696         $store = [$this->env, $this->storeEnv];
3697         $this->env = $env;
3698         $this->storeEnv = null;
3699         $parentQueries = $this->evaluateMediaQuery($parentQueries);
3700         list($this->env, $this->storeEnv) = $store;
3702         if ($childQueries === null) {
3703             $childQueries = $parentQueries;
3704         } else {
3705             $originalQueries = $childQueries;
3706             $childQueries = [];
3708             foreach ($parentQueries as $parentQuery) {
3709                 foreach ($originalQueries as $childQuery) {
3710                     $childQueries[] = array_merge(
3711                         $parentQuery,
3712                         [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
3713                         $childQuery
3714                     );
3715                 }
3716             }
3717         }
3719         return $this->multiplyMedia($env->parent, $childQueries);
3720     }
3722     /**
3723      * Convert env linked list to stack
3724      *
3725      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3726      *
3727      * @return array
3728      */
3729     protected function compactEnv(Environment $env)
3730     {
3731         for ($envs = []; $env; $env = $env->parent) {
3732             $envs[] = $env;
3733         }
3735         return $envs;
3736     }
3738     /**
3739      * Convert env stack to singly linked list
3740      *
3741      * @param array $envs
3742      *
3743      * @return \ScssPhp\ScssPhp\Compiler\Environment
3744      */
3745     protected function extractEnv($envs)
3746     {
3747         for ($env = null; $e = array_pop($envs);) {
3748             $e->parent = $env;
3749             $env = $e;
3750         }
3752         return $env;
3753     }
3755     /**
3756      * Push environment
3757      *
3758      * @param \ScssPhp\ScssPhp\Block $block
3759      *
3760      * @return \ScssPhp\ScssPhp\Compiler\Environment
3761      */
3762     protected function pushEnv(Block $block = null)
3763     {
3764         $env = new Environment;
3765         $env->parent = $this->env;
3766         $env->store  = [];
3767         $env->block  = $block;
3768         $env->depth  = isset($this->env->depth) ? $this->env->depth + 1 : 0;
3770         $this->env = $env;
3772         return $env;
3773     }
3775     /**
3776      * Pop environment
3777      */
3778     protected function popEnv()
3779     {
3780         $this->env = $this->env->parent;
3781     }
3783     /**
3784      * Get store environment
3785      *
3786      * @return \ScssPhp\ScssPhp\Compiler\Environment
3787      */
3788     protected function getStoreEnv()
3789     {
3790         return isset($this->storeEnv) ? $this->storeEnv : $this->env;
3791     }
3793     /**
3794      * Set variable
3795      *
3796      * @param string                                $name
3797      * @param mixed                                 $value
3798      * @param boolean                               $shadow
3799      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3800      * @param mixed                                 $valueUnreduced
3801      */
3802     protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
3803     {
3804         $name = $this->normalizeName($name);
3806         if (! isset($env)) {
3807             $env = $this->getStoreEnv();
3808         }
3810         if ($shadow) {
3811             $this->setRaw($name, $value, $env, $valueUnreduced);
3812         } else {
3813             $this->setExisting($name, $value, $env, $valueUnreduced);
3814         }
3815     }
3817     /**
3818      * Set existing variable
3819      *
3820      * @param string                                $name
3821      * @param mixed                                 $value
3822      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3823      * @param mixed                                 $valueUnreduced
3824      */
3825     protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
3826     {
3827         $storeEnv = $env;
3829         $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
3831         for (;;) {
3832             if (array_key_exists($name, $env->store)) {
3833                 break;
3834             }
3836             if (! $hasNamespace && isset($env->marker)) {
3837                 $env = $storeEnv;
3838                 break;
3839             }
3841             if (! isset($env->parent)) {
3842                 $env = $storeEnv;
3843                 break;
3844             }
3846             $env = $env->parent;
3847         }
3849         $env->store[$name] = $value;
3851         if ($valueUnreduced) {
3852             $env->storeUnreduced[$name] = $valueUnreduced;
3853         }
3854     }
3856     /**
3857      * Set raw variable
3858      *
3859      * @param string                                $name
3860      * @param mixed                                 $value
3861      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3862      * @param mixed                                 $valueUnreduced
3863      */
3864     protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
3865     {
3866         $env->store[$name] = $value;
3868         if ($valueUnreduced) {
3869             $env->storeUnreduced[$name] = $valueUnreduced;
3870         }
3871     }
3873     /**
3874      * Get variable
3875      *
3876      * @api
3877      *
3878      * @param string                                $name
3879      * @param boolean                               $shouldThrow
3880      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3881      * @param boolean                               $unreduced
3882      *
3883      * @return mixed|null
3884      */
3885     public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
3886     {
3887         $normalizedName = $this->normalizeName($name);
3888         $specialContentKey = static::$namespaces['special'] . 'content';
3890         if (! isset($env)) {
3891             $env = $this->getStoreEnv();
3892         }
3894         $nextIsRoot = false;
3895         $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
3897         $maxDepth = 10000;
3899         for (;;) {
3900             if ($maxDepth-- <= 0) {
3901                 break;
3902             }
3904             if (array_key_exists($normalizedName, $env->store)) {
3905                 if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
3906                     return $env->storeUnreduced[$normalizedName];
3907                 }
3909                 return $env->store[$normalizedName];
3910             }
3912             if (! $hasNamespace && isset($env->marker)) {
3913                 if (! $nextIsRoot && ! empty($env->store[$specialContentKey])) {
3914                     $env = $env->store[$specialContentKey]->scope;
3915                     continue;
3916                 }
3918                 $env = $this->rootEnv;
3919                 continue;
3920             }
3922             if (! isset($env->parent)) {
3923                 break;
3924             }
3926             $env = $env->parent;
3927         }
3929         if ($shouldThrow) {
3930             $this->throwError("Undefined variable \$$name" . ($maxDepth<=0 ? " (infinite recursion)" : ""));
3931         }
3933         // found nothing
3934         return null;
3935     }
3937     /**
3938      * Has variable?
3939      *
3940      * @param string                                $name
3941      * @param \ScssPhp\ScssPhp\Compiler\Environment $env
3942      *
3943      * @return boolean
3944      */
3945     protected function has($name, Environment $env = null)
3946     {
3947         return $this->get($name, false, $env) !== null;
3948     }
3950     /**
3951      * Inject variables
3952      *
3953      * @param array $args
3954      */
3955     protected function injectVariables(array $args)
3956     {
3957         if (empty($args)) {
3958             return;
3959         }
3961         $parser = $this->parserFactory(__METHOD__);
3963         foreach ($args as $name => $strValue) {
3964             if ($name[0] === '$') {
3965                 $name = substr($name, 1);
3966             }
3968             if (! $parser->parseValue($strValue, $value)) {
3969                 $value = $this->coerceValue($strValue);
3970             }
3972             $this->set($name, $value);
3973         }
3974     }
3976     /**
3977      * Set variables
3978      *
3979      * @api
3980      *
3981      * @param array $variables
3982      */
3983     public function setVariables(array $variables)
3984     {
3985         $this->registeredVars = array_merge($this->registeredVars, $variables);
3986     }
3988     /**
3989      * Unset variable
3990      *
3991      * @api
3992      *
3993      * @param string $name
3994      */
3995     public function unsetVariable($name)
3996     {
3997         unset($this->registeredVars[$name]);
3998     }
4000     /**
4001      * Returns list of variables
4002      *
4003      * @api
4004      *
4005      * @return array
4006      */
4007     public function getVariables()
4008     {
4009         return $this->registeredVars;
4010     }
4012     /**
4013      * Adds to list of parsed files
4014      *
4015      * @api
4016      *
4017      * @param string $path
4018      */
4019     public function addParsedFile($path)
4020     {
4021         if (isset($path) && file_exists($path)) {
4022             $this->parsedFiles[realpath($path)] = filemtime($path);
4023         }
4024     }
4026     /**
4027      * Returns list of parsed files
4028      *
4029      * @api
4030      *
4031      * @return array
4032      */
4033     public function getParsedFiles()
4034     {
4035         return $this->parsedFiles;
4036     }
4038     /**
4039      * Add import path
4040      *
4041      * @api
4042      *
4043      * @param string|callable $path
4044      */
4045     public function addImportPath($path)
4046     {
4047         if (! in_array($path, $this->importPaths)) {
4048             $this->importPaths[] = $path;
4049         }
4050     }
4052     /**
4053      * Set import paths
4054      *
4055      * @api
4056      *
4057      * @param string|array $path
4058      */
4059     public function setImportPaths($path)
4060     {
4061         $this->importPaths = (array) $path;
4062     }
4064     /**
4065      * Set number precision
4066      *
4067      * @api
4068      *
4069      * @param integer $numberPrecision
4070      */
4071     public function setNumberPrecision($numberPrecision)
4072     {
4073         Node\Number::$precision = $numberPrecision;
4074     }
4076     /**
4077      * Set formatter
4078      *
4079      * @api
4080      *
4081      * @param string $formatterName
4082      */
4083     public function setFormatter($formatterName)
4084     {
4085         $this->formatter = $formatterName;
4086     }
4088     /**
4089      * Set line number style
4090      *
4091      * @api