Passed
Push — master ( 36baab...67c376 )
by Chris
04:26
created

Compiler::matchExtends()   F

Complexity

Conditions 23
Paths 679

Size

Total Lines 101
Code Lines 55

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 23
eloc 55
c 1
b 0
f 0
nc 679
nop 4
dl 0
loc 101
rs 0.4458

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

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