Passed
Push — add/6 ( 1ceb7c...3ff08f )
by
unknown
09:22 queued 04:37
created

Compiler::compileMediaQuery()   F

Complexity

Conditions 28
Paths 6748

Size

Total Lines 113
Code Lines 67

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 28
eloc 67
nc 6748
nop 1
dl 0
loc 113
rs 0
c 0
b 0
f 0

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?
0 ignored issues
show
introduced by
The condition is_object($this->sourceMap) is always false.
Loading history...
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;
0 ignored issues
show
introduced by
$this->env->block is always a sub-type of Leafo\ScssPhp\Block.
Loading history...
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
            }
0 ignored issues
show
Bug Best Practice introduced by
The expression $block->selectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
907
            foreach ($list[2] as $item) {
908
                $keyword = $this->compileStringContent($this->coerceString($item));
909
0 ignored issues
show
Bug introduced by
The property value does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
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
    }
0 ignored issues
show
Bug introduced by
The property selector does not exist on Leafo\ScssPhp\Block. Did you mean selectors?
Loading history...
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
0 ignored issues
show
Bug Best Practice introduced by
The expression $block->selfParent->selectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
950
            $filtered[] = $e;
0 ignored issues
show
Bug Best Practice introduced by
The expression $block->parent->selectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
     *
0 ignored issues
show
Bug Best Practice introduced by
The expression $scope->children of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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);
0 ignored issues
show
Bug Best Practice introduced by
The expression $scope->selectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1049
1050
        $out = $this->makeOutputBlock(null);
1051
1052
        if (isset($this->lineNumberStyle) && count($env->selectors) && count($block->children)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $scope->children of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
            }
0 ignored issues
show
Bug Best Practice introduced by
The expression $scope->selectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
1072
1073
            $this->scope->children[] = $annotation;
1074
        }
1075
0 ignored issues
show
Bug Best Practice introduced by
The expression $scope->children of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
            ' ',
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
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
        }
0 ignored issues
show
Bug introduced by
The property selectors does not seem to exist on Leafo\ScssPhp\Compiler\Environment.
Loading history...
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:
0 ignored issues
show
introduced by
The condition is_array($selector) is always true.
Loading history...
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;
0 ignored issues
show
Performance Best Practice introduced by
It seems like you are calling the size function count() as part of the test condition. You might want to compute the size beforehand, and not on each iteration.

If the size of the collection does not change during the iteration, it is generally a good practice to compute it beforehand, and not on each iteration:

for ($i=0; $i<count($array); $i++) { // calls count() on each iteration
}

// Better
for ($i=0, $c=count($array); $i<$c; $i++) { // calls count() just once
}
Loading history...
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
                    }
0 ignored issues
show
Bug introduced by
$type of type void is incompatible with the type array expected by parameter $haystack of in_array(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1794
                    }/** @scrutinizer ignore-type */ 
Loading history...
1795
1796
                    $this->set($for->var, new Node\Number($start, $unit));
0 ignored issues
show
Bug introduced by
$type of type void is incompatible with the type array expected by parameter $input of array_filter(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1796
                    $this->set($for->var, new Node\Number($start, $unit));/** @scrutinizer ignore-type */ 
Loading history...
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
0 ignored issues
show
Bug introduced by
$type of type void is incompatible with the type array expected by parameter $type1 of Leafo\ScssPhp\Compiler::mergeMediaTypes(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

1824
/** @scrutinizer ignore-type */ 
Loading history...
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
0 ignored issues
show
Bug introduced by
It seems like $rawPath can also be of type Leafo\ScssPhp\Node\Number; however, parameter $rawPath of Leafo\ScssPhp\Compiler::compileImport() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2072
                        $left[0] === Type::/** @scrutinizer ignore-type */ T_NUMBER && $right[0] === Type::T_NUMBER
Loading history...
2073
                    ) {
0 ignored issues
show
Bug introduced by
It seems like $rawPath can also be of type Leafo\ScssPhp\Node\Number; however, parameter $value of Leafo\ScssPhp\Compiler::compileValue() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2073
                    ) {/** @scrutinizer ignore-type */ 
Loading history...
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);
0 ignored issues
show
Bug Best Practice introduced by
The expression $parentSelectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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;
0 ignored issues
show
Bug introduced by
The property marker does not seem to exist on Leafo\ScssPhp\Compiler\Environment.
Loading history...
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
     *
0 ignored issues
show
Bug introduced by
It seems like $left can also be of type Leafo\ScssPhp\Node\Number; however, parameter $value of Leafo\ScssPhp\Compiler::shouldEval() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2561
     */** @scrutinizer ignore-type */ 
Loading history...
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
    /**
0 ignored issues
show
Bug introduced by
It seems like $left can also be of type Leafo\ScssPhp\Node\Number; however, parameter $value of Leafo\ScssPhp\Compiler::coerceForExpression() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2577
    /**/** @scrutinizer ignore-type */ 
Loading history...
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:
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type ArrayAccess; however, parameter $value of Leafo\ScssPhp\Compiler::reduce() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2712
            case Type::T_STRING:/** @scrutinizer ignore-type */ 
Loading history...
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
                        }
0 ignored issues
show
Bug introduced by
It seems like $this->reduce($value) can also be of type Leafo\ScssPhp\Node\Number; however, parameter $value of Leafo\ScssPhp\Compiler::coerceForExpression() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

2792
                        }/** @scrutinizer ignore-type */ 
Loading history...
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
    {
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type Leafo\ScssPhp\Node\Number; however, parameter $var of count() does only seem to accept Countable|array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3243
    {/** @scrutinizer ignore-type */ 
Loading history...
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
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type Leafo\ScssPhp\Node\Number; however, parameter $string of Leafo\ScssPhp\Compiler::compileStringContent() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3262
     * @api/** @scrutinizer ignore-type */ 
Loading history...
3263
     *
3264
     * @return array
3265
     */
0 ignored issues
show
Bug introduced by
It seems like $value[2] can also be of type integer; however, parameter $value of Leafo\ScssPhp\Compiler::compileValue() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3265
     *//** @scrutinizer ignore-type */ 
Loading history...
3266
    public function getVariables()
3267
    {
3268
        return $this->registeredVars;
3269
    }
3270
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type Leafo\ScssPhp\Node\Number; however, parameter $list of Leafo\ScssPhp\Compiler::extractInterpolation() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3270
/** @scrutinizer ignore-type */ 
Loading history...
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
     *
0 ignored issues
show
Bug introduced by
$keys of type double|integer is incompatible with the type Countable|array expected by parameter $var of count(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3299
     */** @scrutinizer ignore-type */ 
Loading history...
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
0 ignored issues
show
Bug introduced by
$value[1] of type double|integer is incompatible with the type array expected by parameter $value of Leafo\ScssPhp\Compiler::reduce(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3324
     * Set number precision/** @scrutinizer ignore-type */ 
Loading history...
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
     */
0 ignored issues
show
Bug introduced by
It seems like $part can also be of type ArrayAccess; however, parameter $value of Leafo\ScssPhp\Compiler::compileValue() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

3403
     *//** @scrutinizer ignore-type */ 
Loading history...
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
0 ignored issues
show
Bug Best Practice introduced by
The expression $selfParent->selectors of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
    }
0 ignored issues
show
Bug introduced by
The expression $selfParentSelectors of type null is not traversable.
Loading history...
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
     */
0 ignored issues
show
Bug introduced by
The property value does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
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
0 ignored issues
show
Documentation Bug introduced by
Are you sure the doc-type for parameter $limit is correct as it would always require null to be passed?
Loading history...
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;
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->callStack of type array is implicitly converted to a boolean; are you sure this is intended? If so, consider using ! empty($expr) instead to make it clear that you intend to check for an array without elements.

This check marks implicit conversions of arrays to boolean values in a comparison. While in PHP an empty array is considered to be equal (but not identical) to false, this is not always apparent.

Consider making the comparison explicit by using empty(..) or ! empty(...) instead.

Loading history...
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
        }
0 ignored issues
show
Bug introduced by
The property marker does not seem to exist on Leafo\ScssPhp\Compiler\Environment.
Loading history...
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
0 ignored issues
show
Bug Best Practice introduced by
The expression $hasVariable of type false|integer is loosely compared to true; this is ambiguous if the integer can be 0. You might want to explicitly use !== false instead.

In PHP, under loose comparison (like ==, or !=, or switch conditions), values of different types might be equal.

For integer values, zero is a special case, in particular the following results might be unexpected:

0   == false // true
0   == null  // true
123 == false // false
123 == null  // false

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
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];
0 ignored issues
show
Bug introduced by
The expression $val[1] of type double|integer is not traversable.
Loading history...
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
0 ignored issues
show
Bug introduced by
It seems like $this->reduce(array_shift($args), true) can also be of type Leafo\ScssPhp\Node\Number; however, parameter $value of Leafo\ScssPhp\Compiler::coerceString() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4929
/** @scrutinizer ignore-type */ 
Loading history...
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;
0 ignored issues
show
Bug introduced by
It seems like $this->reduce($cond, true) can also be of type Leafo\ScssPhp\Node\Number; however, parameter $value of Leafo\ScssPhp\Compiler::isTruthy() does only seem to accept array, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

4970
        return false;/** @scrutinizer ignore-type */ 
Loading history...
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
0 ignored issues
show
Bug introduced by
$hsl[3] of type double is incompatible with the type integer expected by parameter $lightness of Leafo\ScssPhp\Compiler::toRGB(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5058
/** @scrutinizer ignore-type */ 
Loading history...
Bug introduced by
$hsl[1] of type double is incompatible with the type integer expected by parameter $hue of Leafo\ScssPhp\Compiler::toRGB(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5058
/** @scrutinizer ignore-type */ 
Loading history...
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
    }
0 ignored issues
show
introduced by
The condition $w * $a === -1 is always false.
Loading history...
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)
0 ignored issues
show
Bug introduced by
$hsl[1] of type double is incompatible with the type integer expected by parameter $hue of Leafo\ScssPhp\Compiler::toRGB(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5274
    protected function libVa/** @scrutinizer ignore-type */ riableExists($args)
Loading history...
Bug introduced by
$hsl[3] of type double is incompatible with the type integer expected by parameter $lightness of Leafo\ScssPhp\Compiler::toRGB(). ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

5274
    protected function libVariableExists($args/** @scrutinizer ignore-type */ )
Loading history...
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