Passed
Push — master ( d4a329...711fef )
by Jeroen De
03:36
created

Compiler::normalizeValue()   B

Complexity

Conditions 8
Paths 9

Size

Total Lines 33
Code Lines 19

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 8
eloc 19
c 1
b 0
f 0
nc 9
nop 1
dl 0
loc 33
rs 8.4444
1
<?php
2
3
/**
4
 * SCSSPHP
5
 *
6
 * @copyright 2012-2020 Leaf Corcoran
7
 *
8
 * @license http://opensource.org/licenses/MIT MIT
9
 *
10
 * @link http://scssphp.github.io/scssphp
11
 */
12
13
namespace ScssPhp\ScssPhp;
14
15
use ScssPhp\ScssPhp\Base\Range;
16
use ScssPhp\ScssPhp\Block;
17
use ScssPhp\ScssPhp\Cache;
18
use ScssPhp\ScssPhp\Colors;
19
use ScssPhp\ScssPhp\Compiler\Environment;
20
use ScssPhp\ScssPhp\Exception\CompilerException;
21
use ScssPhp\ScssPhp\Formatter\OutputBlock;
22
use ScssPhp\ScssPhp\Node;
23
use ScssPhp\ScssPhp\SourceMap\SourceMapGenerator;
24
use ScssPhp\ScssPhp\Type;
25
use ScssPhp\ScssPhp\Parser;
26
use ScssPhp\ScssPhp\Util;
27
28
/**
29
 * The scss compiler and parser.
30
 *
31
 * Converting SCSS to CSS is a three stage process. The incoming file is parsed
32
 * by `Parser` into a syntax tree, then it is compiled into another tree
33
 * representing the CSS structure by `Compiler`. The CSS tree is fed into a
34
 * formatter, like `Formatter` which then outputs CSS as a string.
35
 *
36
 * During the first compile, all values are *reduced*, which means that their
37
 * types are brought to the lowest form before being dump as strings. This
38
 * handles math equations, variable dereferences, and the like.
39
 *
40
 * The `compile` function of `Compiler` is the entry point.
41
 *
42
 * In summary:
43
 *
44
 * The `Compiler` class creates an instance of the parser, feeds it SCSS code,
45
 * then transforms the resulting tree to a CSS tree. This class also holds the
46
 * evaluation context, such as all available mixins and variables at any given
47
 * time.
48
 *
49
 * The `Parser` class is only concerned with parsing its input.
50
 *
51
 * The `Formatter` takes a CSS tree, and dumps it to a formatted string,
52
 * handling things like indentation.
53
 */
54
55
/**
56
 * SCSS compiler
57
 *
58
 * @author Leaf Corcoran <[email protected]>
59
 */
60
class Compiler
61
{
62
    const LINE_COMMENTS = 1;
63
    const DEBUG_INFO    = 2;
64
65
    const WITH_RULE     = 1;
66
    const WITH_MEDIA    = 2;
67
    const WITH_SUPPORTS = 4;
68
    const WITH_ALL      = 7;
69
70
    const SOURCE_MAP_NONE   = 0;
71
    const SOURCE_MAP_INLINE = 1;
72
    const SOURCE_MAP_FILE   = 2;
73
74
    /**
75
     * @var array
76
     */
77
    protected static $operatorNames = [
78
        '+'   => 'add',
79
        '-'   => 'sub',
80
        '*'   => 'mul',
81
        '/'   => 'div',
82
        '%'   => 'mod',
83
84
        '=='  => 'eq',
85
        '!='  => 'neq',
86
        '<'   => 'lt',
87
        '>'   => 'gt',
88
89
        '<='  => 'lte',
90
        '>='  => 'gte',
91
        '<=>' => 'cmp',
92
    ];
93
94
    /**
95
     * @var array
96
     */
97
    protected static $namespaces = [
98
        'special'  => '%',
99
        'mixin'    => '@',
100
        'function' => '^',
101
    ];
102
103
    public static $true         = [Type::T_KEYWORD, 'true'];
104
    public static $false        = [Type::T_KEYWORD, 'false'];
105
    public static $NaN          = [Type::T_KEYWORD, 'NaN'];
106
    public static $Infinity     = [Type::T_KEYWORD, 'Infinity'];
107
    public static $null         = [Type::T_NULL];
108
    public static $nullString   = [Type::T_STRING, '', []];
109
    public static $defaultValue = [Type::T_KEYWORD, ''];
110
    public static $selfSelector = [Type::T_SELF];
111
    public static $emptyList    = [Type::T_LIST, '', []];
112
    public static $emptyMap     = [Type::T_MAP, [], []];
113
    public static $emptyString  = [Type::T_STRING, '"', []];
114
    public static $with         = [Type::T_KEYWORD, 'with'];
115
    public static $without      = [Type::T_KEYWORD, 'without'];
116
117
    protected $importPaths = [''];
118
    protected $importCache = [];
119
    protected $importedFiles = [];
120
    protected $userFunctions = [];
121
    protected $registeredVars = [];
122
    protected $registeredFeatures = [
123
        'extend-selector-pseudoclass' => false,
124
        'at-error'                    => true,
125
        'units-level-3'               => false,
126
        'global-variable-shadowing'   => false,
127
    ];
128
129
    protected $encoding = null;
130
    protected $lineNumberStyle = null;
131
132
    protected $sourceMap = self::SOURCE_MAP_NONE;
133
    protected $sourceMapOptions = [];
134
135
    /**
136
     * @var string|\ScssPhp\ScssPhp\Formatter
137
     */
138
    protected $formatter = 'ScssPhp\ScssPhp\Formatter\Nested';
139
140
    protected $rootEnv;
141
    protected $rootBlock;
142
143
    /**
144
     * @var \ScssPhp\ScssPhp\Compiler\Environment
145
     */
146
    protected $env;
147
    protected $scope;
148
    protected $storeEnv;
149
    protected $charsetSeen;
150
    protected $sourceNames;
151
152
    protected $cache;
153
154
    protected $indentLevel;
155
    protected $extends;
156
    protected $extendsMap;
157
    protected $parsedFiles;
158
    protected $parser;
159
    protected $sourceIndex;
160
    protected $sourceLine;
161
    protected $sourceColumn;
162
    protected $stderr;
163
    protected $shouldEvaluate;
164
    protected $ignoreErrors;
165
    protected $ignoreCallStackMessage = false;
166
167
    protected $callStack = [];
168
169
    /**
170
     * Constructor
171
     *
172
     * @param array|null $cacheOptions
173
     */
174
    public function __construct($cacheOptions = null)
175
    {
176
        $this->parsedFiles = [];
177
        $this->sourceNames = [];
178
179
        if ($cacheOptions) {
180
            $this->cache = new Cache($cacheOptions);
181
        }
182
183
        $this->stderr = fopen('php://stderr', 'w');
184
    }
185
186
    /**
187
     * Get compiler options
188
     *
189
     * @return array
190
     */
191
    public function getCompileOptions()
192
    {
193
        $options = [
194
            'importPaths'        => $this->importPaths,
195
            'registeredVars'     => $this->registeredVars,
196
            'registeredFeatures' => $this->registeredFeatures,
197
            'encoding'           => $this->encoding,
198
            'sourceMap'          => serialize($this->sourceMap),
199
            'sourceMapOptions'   => $this->sourceMapOptions,
200
            'formatter'          => $this->formatter,
201
        ];
202
203
        return $options;
204
    }
205
206
    /**
207
     * Set an alternative error output stream, for testing purpose only
208
     *
209
     * @param resource $handle
210
     */
211
    public function setErrorOuput($handle)
212
    {
213
        $this->stderr = $handle;
214
    }
215
216
    /**
217
     * Compile scss
218
     *
219
     * @api
220
     *
221
     * @param string $code
222
     * @param string $path
223
     *
224
     * @return string
225
     */
226
    public function compile($code, $path = null)
227
    {
228
        if ($this->cache) {
229
            $cacheKey       = ($path ? $path : '(stdin)') . ':' . md5($code);
230
            $compileOptions = $this->getCompileOptions();
231
            $cache          = $this->cache->getCache('compile', $cacheKey, $compileOptions);
232
233
            if (\is_array($cache) && isset($cache['dependencies']) && isset($cache['out'])) {
234
                // check if any dependency file changed before accepting the cache
235
                foreach ($cache['dependencies'] as $file => $mtime) {
236
                    if (! is_file($file) || filemtime($file) !== $mtime) {
237
                        unset($cache);
238
                        break;
239
                    }
240
                }
241
242
                if (isset($cache)) {
243
                    return $cache['out'];
244
                }
245
            }
246
        }
247
248
249
        $this->indentLevel    = -1;
250
        $this->extends        = [];
251
        $this->extendsMap     = [];
252
        $this->sourceIndex    = null;
253
        $this->sourceLine     = null;
254
        $this->sourceColumn   = null;
255
        $this->env            = null;
256
        $this->scope          = null;
257
        $this->storeEnv       = null;
258
        $this->charsetSeen    = null;
259
        $this->shouldEvaluate = null;
260
        $this->ignoreCallStackMessage = false;
261
262
        $this->parser = $this->parserFactory($path);
263
        $tree         = $this->parser->parse($code);
264
        $this->parser = null;
265
266
        $this->formatter = new $this->formatter();
267
        $this->rootBlock = null;
268
        $this->rootEnv   = $this->pushEnv($tree);
269
270
        $this->injectVariables($this->registeredVars);
271
        $this->compileRoot($tree);
272
        $this->popEnv();
273
274
        $sourceMapGenerator = null;
275
276
        if ($this->sourceMap) {
277
            if (\is_object($this->sourceMap) && $this->sourceMap instanceof SourceMapGenerator) {
0 ignored issues
show
introduced by
The condition is_object($this->sourceMap) is always false.
Loading history...
278
                $sourceMapGenerator = $this->sourceMap;
279
                $this->sourceMap = self::SOURCE_MAP_FILE;
280
            } elseif ($this->sourceMap !== self::SOURCE_MAP_NONE) {
281
                $sourceMapGenerator = new SourceMapGenerator($this->sourceMapOptions);
282
            }
283
        }
284
285
        $out = $this->formatter->format($this->scope, $sourceMapGenerator);
286
287
        if (! empty($out) && $this->sourceMap && $this->sourceMap !== self::SOURCE_MAP_NONE) {
288
            $sourceMap    = $sourceMapGenerator->generateJson();
289
            $sourceMapUrl = null;
290
291
            switch ($this->sourceMap) {
292
                case self::SOURCE_MAP_INLINE:
293
                    $sourceMapUrl = sprintf('data:application/json,%s', Util::encodeURIComponent($sourceMap));
294
                    break;
295
296
                case self::SOURCE_MAP_FILE:
297
                    $sourceMapUrl = $sourceMapGenerator->saveMap($sourceMap);
298
                    break;
299
            }
300
301
            $out .= sprintf('/*# sourceMappingURL=%s */', $sourceMapUrl);
302
        }
303
304
        if ($this->cache && isset($cacheKey) && isset($compileOptions)) {
305
            $v = [
306
                'dependencies' => $this->getParsedFiles(),
307
                'out' => &$out,
308
            ];
309
310
            $this->cache->setCache('compile', $cacheKey, $v, $compileOptions);
311
        }
312
313
        return $out;
314
    }
315
316
    /**
317
     * Instantiate parser
318
     *
319
     * @param string $path
320
     *
321
     * @return \ScssPhp\ScssPhp\Parser
322
     */
323
    protected function parserFactory($path)
324
    {
325
        // https://sass-lang.com/documentation/at-rules/import
326
        // CSS files imported by Sass don’t allow any special Sass features.
327
        // In order to make sure authors don’t accidentally write Sass in their CSS,
328
        // all Sass features that aren’t also valid CSS will produce errors.
329
        // Otherwise, the CSS will be rendered as-is. It can even be extended!
330
        $cssOnly = false;
331
332
        if (substr($path, '-4') === '.css') {
333
            $cssOnly = true;
334
        }
335
336
        $parser = new Parser($path, \count($this->sourceNames), $this->encoding, $this->cache, $cssOnly);
337
338
        $this->sourceNames[] = $path;
339
        $this->addParsedFile($path);
340
341
        return $parser;
342
    }
343
344
    /**
345
     * Is self extend?
346
     *
347
     * @param array $target
348
     * @param array $origin
349
     *
350
     * @return boolean
351
     */
352
    protected function isSelfExtend($target, $origin)
353
    {
354
        foreach ($origin as $sel) {
355
            if (\in_array($target, $sel)) {
356
                return true;
357
            }
358
        }
359
360
        return false;
361
    }
362
363
    /**
364
     * Push extends
365
     *
366
     * @param array      $target
367
     * @param array      $origin
368
     * @param array|null $block
369
     */
370
    protected function pushExtends($target, $origin, $block)
371
    {
372
        $i = \count($this->extends);
373
        $this->extends[] = [$target, $origin, $block];
374
375
        foreach ($target as $part) {
376
            if (isset($this->extendsMap[$part])) {
377
                $this->extendsMap[$part][] = $i;
378
            } else {
379
                $this->extendsMap[$part] = [$i];
380
            }
381
        }
382
    }
383
384
    /**
385
     * Make output block
386
     *
387
     * @param string $type
388
     * @param array  $selectors
389
     *
390
     * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
391
     */
392
    protected function makeOutputBlock($type, $selectors = null)
393
    {
394
        $out = new OutputBlock();
395
        $out->type      = $type;
396
        $out->lines     = [];
397
        $out->children  = [];
398
        $out->parent    = $this->scope;
399
        $out->selectors = $selectors;
400
        $out->depth     = $this->env->depth;
401
402
        if ($this->env->block instanceof Block) {
0 ignored issues
show
introduced by
$this->env->block is always a sub-type of ScssPhp\ScssPhp\Block.
Loading history...
403
            $out->sourceName   = $this->env->block->sourceName;
404
            $out->sourceLine   = $this->env->block->sourceLine;
405
            $out->sourceColumn = $this->env->block->sourceColumn;
406
        } else {
407
            $out->sourceName   = null;
408
            $out->sourceLine   = null;
409
            $out->sourceColumn = null;
410
        }
411
412
        return $out;
413
    }
414
415
    /**
416
     * Compile root
417
     *
418
     * @param \ScssPhp\ScssPhp\Block $rootBlock
419
     */
420
    protected function compileRoot(Block $rootBlock)
421
    {
422
        $this->rootBlock = $this->scope = $this->makeOutputBlock(Type::T_ROOT);
423
424
        $this->compileChildrenNoReturn($rootBlock->children, $this->scope);
425
        $this->flattenSelectors($this->scope);
426
        $this->missingSelectors();
427
    }
428
429
    /**
430
     * Report missing selectors
431
     */
432
    protected function missingSelectors()
433
    {
434
        foreach ($this->extends as $extend) {
435
            if (isset($extend[3])) {
436
                continue;
437
            }
438
439
            list($target, $origin, $block) = $extend;
440
441
            // ignore if !optional
442
            if ($block[2]) {
443
                continue;
444
            }
445
446
            $target = implode(' ', $target);
447
            $origin = $this->collapseSelectors($origin);
448
449
            $this->sourceLine = $block[Parser::SOURCE_LINE];
450
            throw $this->error("\"$origin\" failed to @extend \"$target\". The selector \"$target\" was not found.");
451
        }
452
    }
453
454
    /**
455
     * Flatten selectors
456
     *
457
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $block
458
     * @param string                                 $parentKey
459
     */
460
    protected function flattenSelectors(OutputBlock $block, $parentKey = null)
461
    {
462
        if ($block->selectors) {
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...
463
            $selectors = [];
464
465
            foreach ($block->selectors as $s) {
466
                $selectors[] = $s;
467
468
                if (! \is_array($s)) {
469
                    continue;
470
                }
471
472
                // check extends
473
                if (! empty($this->extendsMap)) {
474
                    $this->matchExtends($s, $selectors);
475
476
                    // remove duplicates
477
                    array_walk($selectors, function (&$value) {
478
                        $value = serialize($value);
479
                    });
480
481
                    $selectors = array_unique($selectors);
482
483
                    array_walk($selectors, function (&$value) {
484
                        $value = unserialize($value);
485
                    });
486
                }
487
            }
488
489
            $block->selectors = [];
490
            $placeholderSelector = false;
491
492
            foreach ($selectors as $selector) {
493
                if ($this->hasSelectorPlaceholder($selector)) {
494
                    $placeholderSelector = true;
495
                    continue;
496
                }
497
498
                $block->selectors[] = $this->compileSelector($selector);
499
            }
500
501
            if ($placeholderSelector && 0 === \count($block->selectors) && null !== $parentKey) {
502
                unset($block->parent->children[$parentKey]);
503
504
                return;
505
            }
506
        }
507
508
        foreach ($block->children as $key => $child) {
509
            $this->flattenSelectors($child, $key);
510
        }
511
    }
512
513
    /**
514
     * Glue parts of :not( or :nth-child( ... that are in general splitted in selectors parts
515
     *
516
     * @param array $parts
517
     *
518
     * @return array
519
     */
520
    protected function glueFunctionSelectors($parts)
521
    {
522
        $new = [];
523
524
        foreach ($parts as $part) {
525
            if (\is_array($part)) {
526
                $part = $this->glueFunctionSelectors($part);
527
                $new[] = $part;
528
            } else {
529
                // a selector part finishing with a ) is the last part of a :not( or :nth-child(
530
                // and need to be joined to this
531
                if (
532
                    \count($new) && \is_string($new[\count($new) - 1]) &&
533
                    \strlen($part) && substr($part, -1) === ')' && strpos($part, '(') === false
534
                ) {
535
                    while (\count($new) > 1 && substr($new[\count($new) - 1], -1) !== '(') {
536
                        $part = array_pop($new) . $part;
537
                    }
538
                    $new[\count($new) - 1] .= $part;
539
                } else {
540
                    $new[] = $part;
541
                }
542
            }
543
        }
544
545
        return $new;
546
    }
547
548
    /**
549
     * Match extends
550
     *
551
     * @param array   $selector
552
     * @param array   $out
553
     * @param integer $from
554
     * @param boolean $initial
555
     */
556
    protected function matchExtends($selector, &$out, $from = 0, $initial = true)
557
    {
558
        static $partsPile = [];
559
        $selector = $this->glueFunctionSelectors($selector);
560
561
        if (\count($selector) == 1 && \in_array(reset($selector), $partsPile)) {
562
            return;
563
        }
564
565
        $outRecurs = [];
566
567
        foreach ($selector as $i => $part) {
568
            if ($i < $from) {
569
                continue;
570
            }
571
572
            // check that we are not building an infinite loop of extensions
573
            // if the new part is just including a previous part don't try to extend anymore
574
            if (\count($part) > 1) {
575
                foreach ($partsPile as $previousPart) {
576
                    if (! \count(array_diff($previousPart, $part))) {
577
                        continue 2;
578
                    }
579
                }
580
            }
581
582
            $partsPile[] = $part;
583
584
            if ($this->matchExtendsSingle($part, $origin, $initial)) {
585
                $after       = \array_slice($selector, $i + 1);
586
                $before      = \array_slice($selector, 0, $i);
587
                list($before, $nonBreakableBefore) = $this->extractRelationshipFromFragment($before);
588
589
                foreach ($origin as $new) {
590
                    $k = 0;
591
592
                    // remove shared parts
593
                    if (\count($new) > 1) {
594
                        while ($k < $i && isset($new[$k]) && $selector[$k] === $new[$k]) {
595
                            $k++;
596
                        }
597
                    }
598
599
                    if (\count($nonBreakableBefore) && $k === \count($new)) {
600
                        $k--;
601
                    }
602
603
                    $replacement = [];
604
                    $tempReplacement = $k > 0 ? \array_slice($new, $k) : $new;
605
606
                    for ($l = \count($tempReplacement) - 1; $l >= 0; $l--) {
607
                        $slice = [];
608
609
                        foreach ($tempReplacement[$l] as $chunk) {
610
                            if (! \in_array($chunk, $slice)) {
611
                                $slice[] = $chunk;
612
                            }
613
                        }
614
615
                        array_unshift($replacement, $slice);
616
617
                        if (! $this->isImmediateRelationshipCombinator(end($slice))) {
618
                            break;
619
                        }
620
                    }
621
622
                    $afterBefore = $l != 0 ? \array_slice($tempReplacement, 0, $l) : [];
623
624
                    // Merge shared direct relationships.
625
                    $mergedBefore = $this->mergeDirectRelationships($afterBefore, $nonBreakableBefore);
626
627
                    $result = array_merge(
628
                        $before,
629
                        $mergedBefore,
630
                        $replacement,
631
                        $after
632
                    );
633
634
                    if ($result === $selector) {
635
                        continue;
636
                    }
637
638
                    $this->pushOrMergeExtentedSelector($out, $result);
639
640
                    // recursively check for more matches
641
                    $startRecurseFrom = \count($before) + min(\count($nonBreakableBefore), \count($mergedBefore));
642
643
                    if (\count($origin) > 1) {
644
                        $this->matchExtends($result, $out, $startRecurseFrom, false);
645
                    } else {
646
                        $this->matchExtends($result, $outRecurs, $startRecurseFrom, false);
647
                    }
648
649
                    // selector sequence merging
650
                    if (! empty($before) && \count($new) > 1) {
651
                        $preSharedParts = $k > 0 ? \array_slice($before, 0, $k) : [];
652
                        $postSharedParts = $k > 0 ? \array_slice($before, $k) : $before;
653
654
                        list($betweenSharedParts, $nonBreakabl2) = $this->extractRelationshipFromFragment($afterBefore);
655
656
                        $result2 = array_merge(
657
                            $preSharedParts,
658
                            $betweenSharedParts,
659
                            $postSharedParts,
660
                            $nonBreakabl2,
661
                            $nonBreakableBefore,
662
                            $replacement,
663
                            $after
664
                        );
665
666
                        $this->pushOrMergeExtentedSelector($out, $result2);
667
                    }
668
                }
669
            }
670
            array_pop($partsPile);
671
        }
672
673
        while (\count($outRecurs)) {
674
            $result = array_shift($outRecurs);
675
            $this->pushOrMergeExtentedSelector($out, $result);
676
        }
677
    }
678
679
    /**
680
     * Test a part for being a pseudo selector
681
     *
682
     * @param string $part
683
     * @param array  $matches
684
     *
685
     * @return boolean
686
     */
687
    protected function isPseudoSelector($part, &$matches)
688
    {
689
        if (
690
            strpos($part, ':') === 0 &&
691
            preg_match(",^::?([\w-]+)\((.+)\)$,", $part, $matches)
692
        ) {
693
            return true;
694
        }
695
696
        return false;
697
    }
698
699
    /**
700
     * Push extended selector except if
701
     *  - this is a pseudo selector
702
     *  - same as previous
703
     *  - in a white list
704
     * in this case we merge the pseudo selector content
705
     *
706
     * @param array $out
707
     * @param array $extended
708
     */
709
    protected function pushOrMergeExtentedSelector(&$out, $extended)
710
    {
711
        if (\count($out) && \count($extended) === 1 && \count(reset($extended)) === 1) {
712
            $single = reset($extended);
713
            $part = reset($single);
714
715
            if (
716
                $this->isPseudoSelector($part, $matchesExtended) &&
717
                \in_array($matchesExtended[1], [ 'slotted' ])
718
            ) {
719
                $prev = end($out);
720
                $prev = $this->glueFunctionSelectors($prev);
721
722
                if (\count($prev) === 1 && \count(reset($prev)) === 1) {
723
                    $single = reset($prev);
724
                    $part = reset($single);
725
726
                    if (
727
                        $this->isPseudoSelector($part, $matchesPrev) &&
728
                        $matchesPrev[1] === $matchesExtended[1]
729
                    ) {
730
                        $extended = explode($matchesExtended[1] . '(', $matchesExtended[0], 2);
731
                        $extended[1] = $matchesPrev[2] . ', ' . $extended[1];
732
                        $extended = implode($matchesExtended[1] . '(', $extended);
733
                        $extended = [ [ $extended ]];
734
                        array_pop($out);
735
                    }
736
                }
737
            }
738
        }
739
        $out[] = $extended;
740
    }
741
742
    /**
743
     * Match extends single
744
     *
745
     * @param array   $rawSingle
746
     * @param array   $outOrigin
747
     * @param boolean $initial
748
     *
749
     * @return boolean
750
     */
751
    protected function matchExtendsSingle($rawSingle, &$outOrigin, $initial = true)
752
    {
753
        $counts = [];
754
        $single = [];
755
756
        // simple usual cases, no need to do the whole trick
757
        if (\in_array($rawSingle, [['>'],['+'],['~']])) {
758
            return false;
759
        }
760
761
        foreach ($rawSingle as $part) {
762
            // matches Number
763
            if (! \is_string($part)) {
764
                return false;
765
            }
766
767
            if (! preg_match('/^[\[.:#%]/', $part) && \count($single)) {
768
                $single[\count($single) - 1] .= $part;
769
            } else {
770
                $single[] = $part;
771
            }
772
        }
773
774
        $extendingDecoratedTag = false;
775
776
        if (\count($single) > 1) {
777
            $matches = null;
778
            $extendingDecoratedTag = preg_match('/^[a-z0-9]+$/i', $single[0], $matches) ? $matches[0] : false;
779
        }
780
781
        $outOrigin = [];
782
        $found = false;
783
784
        foreach ($single as $k => $part) {
785
            if (isset($this->extendsMap[$part])) {
786
                foreach ($this->extendsMap[$part] as $idx) {
787
                    $counts[$idx] = isset($counts[$idx]) ? $counts[$idx] + 1 : 1;
788
                }
789
            }
790
791
            if (
792
                $initial &&
793
                $this->isPseudoSelector($part, $matches) &&
794
                ! \in_array($matches[1], [ 'not' ])
795
            ) {
796
                $buffer    = $matches[2];
797
                $parser    = $this->parserFactory(__METHOD__);
798
799
                if ($parser->parseSelector($buffer, $subSelectors)) {
800
                    foreach ($subSelectors as $ksub => $subSelector) {
801
                        $subExtended = [];
802
                        $this->matchExtends($subSelector, $subExtended, 0, false);
803
804
                        if ($subExtended) {
805
                            $subSelectorsExtended = $subSelectors;
806
                            $subSelectorsExtended[$ksub] = $subExtended;
807
808
                            foreach ($subSelectorsExtended as $ksse => $sse) {
809
                                $subSelectorsExtended[$ksse] = $this->collapseSelectors($sse);
810
                            }
811
812
                            $subSelectorsExtended = implode(', ', $subSelectorsExtended);
813
                            $singleExtended = $single;
814
                            $singleExtended[$k] = str_replace('(' . $buffer . ')', "($subSelectorsExtended)", $part);
815
                            $outOrigin[] = [ $singleExtended ];
816
                            $found = true;
817
                        }
818
                    }
819
                }
820
            }
821
        }
822
823
        foreach ($counts as $idx => $count) {
824
            list($target, $origin, /* $block */) = $this->extends[$idx];
825
826
            $origin = $this->glueFunctionSelectors($origin);
827
828
            // check count
829
            if ($count !== \count($target)) {
830
                continue;
831
            }
832
833
            $this->extends[$idx][3] = true;
834
835
            $rem = array_diff($single, $target);
836
837
            foreach ($origin as $j => $new) {
838
                // prevent infinite loop when target extends itself
839
                if ($this->isSelfExtend($single, $origin) && ! $initial) {
840
                    return false;
841
                }
842
843
                $replacement = end($new);
844
845
                // Extending a decorated tag with another tag is not possible.
846
                if (
847
                    $extendingDecoratedTag && $replacement[0] != $extendingDecoratedTag &&
848
                    preg_match('/^[a-z0-9]+$/i', $replacement[0])
849
                ) {
850
                    unset($origin[$j]);
851
                    continue;
852
                }
853
854
                $combined = $this->combineSelectorSingle($replacement, $rem);
855
856
                if (\count(array_diff($combined, $origin[$j][\count($origin[$j]) - 1]))) {
857
                    $origin[$j][\count($origin[$j]) - 1] = $combined;
858
                }
859
            }
860
861
            $outOrigin = array_merge($outOrigin, $origin);
862
863
            $found = true;
864
        }
865
866
        return $found;
867
    }
868
869
    /**
870
     * Extract a relationship from the fragment.
871
     *
872
     * When extracting the last portion of a selector we will be left with a
873
     * fragment which may end with a direction relationship combinator. This
874
     * method will extract the relationship fragment and return it along side
875
     * the rest.
876
     *
877
     * @param array $fragment The selector fragment maybe ending with a direction relationship combinator.
878
     *
879
     * @return array The selector without the relationship fragment if any, the relationship fragment.
880
     */
881
    protected function extractRelationshipFromFragment(array $fragment)
882
    {
883
        $parents = [];
884
        $children = [];
885
886
        $j = $i = \count($fragment);
887
888
        for (;;) {
889
            $children = $j != $i ? \array_slice($fragment, $j, $i - $j) : [];
890
            $parents  = \array_slice($fragment, 0, $j);
891
            $slice    = end($parents);
892
893
            if (empty($slice) || ! $this->isImmediateRelationshipCombinator($slice[0])) {
894
                break;
895
            }
896
897
            $j -= 2;
898
        }
899
900
        return [$parents, $children];
901
    }
902
903
    /**
904
     * Combine selector single
905
     *
906
     * @param array $base
907
     * @param array $other
908
     *
909
     * @return array
910
     */
911
    protected function combineSelectorSingle($base, $other)
912
    {
913
        $tag    = [];
914
        $out    = [];
915
        $wasTag = false;
916
        $pseudo = [];
917
918
        while (\count($other) && strpos(end($other), ':') === 0) {
919
            array_unshift($pseudo, array_pop($other));
920
        }
921
922
        foreach ([array_reverse($base), array_reverse($other)] as $single) {
923
            $rang = count($single);
924
925
            foreach ($single as $part) {
926
                if (preg_match('/^[\[:]/', $part)) {
927
                    $out[] = $part;
928
                    $wasTag = false;
929
                } elseif (preg_match('/^[\.#]/', $part)) {
930
                    array_unshift($out, $part);
931
                    $wasTag = false;
932
                } elseif (preg_match('/^[^_-]/', $part) && $rang === 1) {
933
                    $tag[] = $part;
934
                    $wasTag = true;
935
                } elseif ($wasTag) {
936
                    $tag[\count($tag) - 1] .= $part;
937
                } else {
938
                    array_unshift($out, $part);
939
                }
940
                $rang--;
941
            }
942
        }
943
944
        if (\count($tag)) {
945
            array_unshift($out, $tag[0]);
946
        }
947
948
        while (\count($pseudo)) {
949
            $out[] = array_shift($pseudo);
950
        }
951
952
        return $out;
953
    }
954
955
    /**
956
     * Compile media
957
     *
958
     * @param \ScssPhp\ScssPhp\Block $media
959
     */
960
    protected function compileMedia(Block $media)
961
    {
962
        $this->pushEnv($media);
963
964
        $mediaQueries = $this->compileMediaQuery($this->multiplyMedia($this->env));
965
966
        if (! empty($mediaQueries) && $mediaQueries) {
967
            $previousScope = $this->scope;
968
            $parentScope = $this->mediaParent($this->scope);
969
970
            foreach ($mediaQueries as $mediaQuery) {
971
                $this->scope = $this->makeOutputBlock(Type::T_MEDIA, [$mediaQuery]);
972
973
                $parentScope->children[] = $this->scope;
974
                $parentScope = $this->scope;
975
            }
976
977
            // top level properties in a media cause it to be wrapped
978
            $needsWrap = false;
979
980
            foreach ($media->children as $child) {
981
                $type = $child[0];
982
983
                if (
984
                    $type !== Type::T_BLOCK &&
985
                    $type !== Type::T_MEDIA &&
986
                    $type !== Type::T_DIRECTIVE &&
987
                    $type !== Type::T_IMPORT
988
                ) {
989
                    $needsWrap = true;
990
                    break;
991
                }
992
            }
993
994
            if ($needsWrap) {
995
                $wrapped = new Block();
996
                $wrapped->sourceName   = $media->sourceName;
997
                $wrapped->sourceIndex  = $media->sourceIndex;
998
                $wrapped->sourceLine   = $media->sourceLine;
999
                $wrapped->sourceColumn = $media->sourceColumn;
1000
                $wrapped->selectors    = [];
1001
                $wrapped->comments     = [];
1002
                $wrapped->parent       = $media;
1003
                $wrapped->children     = $media->children;
1004
1005
                $media->children = [[Type::T_BLOCK, $wrapped]];
1006
1007
                if (isset($this->lineNumberStyle)) {
1008
                    $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1009
                    $annotation->depth = 0;
1010
1011
                    $file = $this->sourceNames[$media->sourceIndex];
1012
                    $line = $media->sourceLine;
1013
1014
                    switch ($this->lineNumberStyle) {
1015
                        case static::LINE_COMMENTS:
1016
                            $annotation->lines[] = '/* line ' . $line
1017
                                                 . ($file ? ', ' . $file : '')
1018
                                                 . ' */';
1019
                            break;
1020
1021
                        case static::DEBUG_INFO:
1022
                            $annotation->lines[] = '@media -sass-debug-info{'
1023
                                                 . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1024
                                                 . 'line{font-family:' . $line . '}}';
1025
                            break;
1026
                    }
1027
1028
                    $this->scope->children[] = $annotation;
1029
                }
1030
            }
1031
1032
            $this->compileChildrenNoReturn($media->children, $this->scope);
1033
1034
            $this->scope = $previousScope;
1035
        }
1036
1037
        $this->popEnv();
1038
    }
1039
1040
    /**
1041
     * Media parent
1042
     *
1043
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1044
     *
1045
     * @return \ScssPhp\ScssPhp\Formatter\OutputBlock
1046
     */
1047
    protected function mediaParent(OutputBlock $scope)
1048
    {
1049
        while (! empty($scope->parent)) {
1050
            if (! empty($scope->type) && $scope->type !== Type::T_MEDIA) {
1051
                break;
1052
            }
1053
1054
            $scope = $scope->parent;
1055
        }
1056
1057
        return $scope;
1058
    }
1059
1060
    /**
1061
     * Compile directive
1062
     *
1063
     * @param \ScssPhp\ScssPhp\Block|array $block
1064
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1065
     */
1066
    protected function compileDirective($directive, OutputBlock $out)
1067
    {
1068
        if (\is_array($directive)) {
1069
            $s = '@' . $directive[0];
1070
1071
            if (! empty($directive[1])) {
1072
                $s .= ' ' . $this->compileValue($directive[1]);
0 ignored issues
show
Bug introduced by
Are you sure $this->compileValue($directive[1]) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

1072
                $s .= ' ' . /** @scrutinizer ignore-type */ $this->compileValue($directive[1]);
Loading history...
1073
            }
1074
1075
            $this->appendRootDirective($s . ';', $out);
1076
        } else {
1077
            $s = '@' . $directive->name;
1078
1079
            if (! empty($directive->value)) {
1080
                $s .= ' ' . $this->compileValue($directive->value);
1081
            }
1082
1083
            if ($directive->name === 'keyframes' || substr($directive->name, -10) === '-keyframes') {
1084
                $this->compileKeyframeBlock($directive, [$s]);
1085
            } else {
1086
                $this->compileNestedBlock($directive, [$s]);
1087
            }
1088
        }
1089
    }
1090
1091
    /**
1092
     * Compile at-root
1093
     *
1094
     * @param \ScssPhp\ScssPhp\Block $block
1095
     */
1096
    protected function compileAtRoot(Block $block)
1097
    {
1098
        $env     = $this->pushEnv($block);
1099
        $envs    = $this->compactEnv($env);
1100
        list($with, $without) = $this->compileWith(isset($block->with) ? $block->with : null);
1101
1102
        // wrap inline selector
1103
        if ($block->selector) {
0 ignored issues
show
Bug introduced by
The property selector does not exist on ScssPhp\ScssPhp\Block. Did you mean selectors?
Loading history...
1104
            $wrapped = new Block();
1105
            $wrapped->sourceName   = $block->sourceName;
1106
            $wrapped->sourceIndex  = $block->sourceIndex;
1107
            $wrapped->sourceLine   = $block->sourceLine;
1108
            $wrapped->sourceColumn = $block->sourceColumn;
1109
            $wrapped->selectors    = $block->selector;
1110
            $wrapped->comments     = [];
1111
            $wrapped->parent       = $block;
1112
            $wrapped->children     = $block->children;
1113
            $wrapped->selfParent   = $block->selfParent;
1114
1115
            $block->children = [[Type::T_BLOCK, $wrapped]];
1116
            $block->selector = null;
1117
        }
1118
1119
        $selfParent = $block->selfParent;
1120
1121
        if (
1122
            ! $block->selfParent->selectors &&
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...
1123
            isset($block->parent) && $block->parent &&
1124
            isset($block->parent->selectors) && $block->parent->selectors
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...
1125
        ) {
1126
            $selfParent = $block->parent;
1127
        }
1128
1129
        $this->env = $this->filterWithWithout($envs, $with, $without);
1130
1131
        $saveScope   = $this->scope;
1132
        $this->scope = $this->filterScopeWithWithout($saveScope, $with, $without);
1133
1134
        // propagate selfParent to the children where they still can be useful
1135
        $this->compileChildrenNoReturn($block->children, $this->scope, $selfParent);
1136
1137
        $this->scope = $this->completeScope($this->scope, $saveScope);
1138
        $this->scope = $saveScope;
1139
        $this->env   = $this->extractEnv($envs);
1140
1141
        $this->popEnv();
1142
    }
1143
1144
    /**
1145
     * Filter at-root scope depending of with/without option
1146
     *
1147
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1148
     * @param array                                  $with
1149
     * @param array                                  $without
1150
     *
1151
     * @return mixed
1152
     */
1153
    protected function filterScopeWithWithout($scope, $with, $without)
1154
    {
1155
        $filteredScopes = [];
1156
        $childStash = [];
1157
1158
        if ($scope->type === TYPE::T_ROOT) {
1159
            return $scope;
1160
        }
1161
1162
        // start from the root
1163
        while ($scope->parent && $scope->parent->type !== TYPE::T_ROOT) {
1164
            array_unshift($childStash, $scope);
1165
            $scope = $scope->parent;
1166
        }
1167
1168
        for (;;) {
1169
            if (! $scope) {
1170
                break;
1171
            }
1172
1173
            if ($this->isWith($scope, $with, $without)) {
1174
                $s = clone $scope;
1175
                $s->children = [];
1176
                $s->lines    = [];
1177
                $s->parent   = null;
1178
1179
                if ($s->type !== Type::T_MEDIA && $s->type !== Type::T_DIRECTIVE) {
1180
                    $s->selectors = [];
1181
                }
1182
1183
                $filteredScopes[] = $s;
1184
            }
1185
1186
            if (\count($childStash)) {
1187
                $scope = array_shift($childStash);
1188
            } elseif ($scope->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...
1189
                $scope = end($scope->children);
1190
            } else {
1191
                $scope = null;
1192
            }
1193
        }
1194
1195
        if (! \count($filteredScopes)) {
1196
            return $this->rootBlock;
1197
        }
1198
1199
        $newScope = array_shift($filteredScopes);
1200
        $newScope->parent = $this->rootBlock;
1201
1202
        $this->rootBlock->children[] = $newScope;
1203
1204
        $p = &$newScope;
1205
1206
        while (\count($filteredScopes)) {
1207
            $s = array_shift($filteredScopes);
1208
            $s->parent = $p;
1209
            $p->children[] = $s;
1210
            $newScope = &$p->children[0];
1211
            $p = &$p->children[0];
1212
        }
1213
1214
        return $newScope;
1215
    }
1216
1217
    /**
1218
     * found missing selector from a at-root compilation in the previous scope
1219
     * (if at-root is just enclosing a property, the selector is in the parent tree)
1220
     *
1221
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1222
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $previousScope
1223
     *
1224
     * @return mixed
1225
     */
1226
    protected function completeScope($scope, $previousScope)
1227
    {
1228
        if (! $scope->type && (! $scope->selectors || ! \count($scope->selectors)) && \count($scope->lines)) {
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...
1229
            $scope->selectors = $this->findScopeSelectors($previousScope, $scope->depth);
1230
        }
1231
1232
        if ($scope->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...
1233
            foreach ($scope->children as $k => $c) {
1234
                $scope->children[$k] = $this->completeScope($c, $previousScope);
1235
            }
1236
        }
1237
1238
        return $scope;
1239
    }
1240
1241
    /**
1242
     * Find a selector by the depth node in the scope
1243
     *
1244
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $scope
1245
     * @param integer                                $depth
1246
     *
1247
     * @return array
1248
     */
1249
    protected function findScopeSelectors($scope, $depth)
1250
    {
1251
        if ($scope->depth === $depth && $scope->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...
1252
            return $scope->selectors;
1253
        }
1254
1255
        if ($scope->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...
1256
            foreach (array_reverse($scope->children) as $c) {
1257
                if ($s = $this->findScopeSelectors($c, $depth)) {
1258
                    return $s;
1259
                }
1260
            }
1261
        }
1262
1263
        return [];
1264
    }
1265
1266
    /**
1267
     * Compile @at-root's with: inclusion / without: exclusion into 2 lists uses to filter scope/env later
1268
     *
1269
     * @param array $withCondition
1270
     *
1271
     * @return array
1272
     */
1273
    protected function compileWith($withCondition)
1274
    {
1275
        // just compile what we have in 2 lists
1276
        $with = [];
1277
        $without = ['rule' => true];
1278
1279
        if ($withCondition) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $withCondition 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...
1280
            if ($withCondition[0] === Type::T_INTERPOLATE) {
1281
                $w = $this->compileValue($withCondition);
1282
1283
                $buffer = "($w)";
1284
                $parser = $this->parserFactory(__METHOD__);
1285
1286
                if ($parser->parseValue($buffer, $reParsedWith)) {
1287
                    $withCondition = $reParsedWith;
1288
                }
1289
            }
1290
1291
            if ($this->libMapHasKey([$withCondition, static::$with])) {
1292
                $without = []; // cancel the default
1293
                $list = $this->coerceList($this->libMapGet([$withCondition, static::$with]));
1294
1295
                foreach ($list[2] as $item) {
1296
                    $keyword = $this->compileStringContent($this->coerceString($item));
1297
1298
                    $with[$keyword] = true;
1299
                }
1300
            }
1301
1302
            if ($this->libMapHasKey([$withCondition, static::$without])) {
1303
                $without = []; // cancel the default
1304
                $list = $this->coerceList($this->libMapGet([$withCondition, static::$without]));
1305
1306
                foreach ($list[2] as $item) {
1307
                    $keyword = $this->compileStringContent($this->coerceString($item));
1308
1309
                    $without[$keyword] = true;
1310
                }
1311
            }
1312
        }
1313
1314
        return [$with, $without];
1315
    }
1316
1317
    /**
1318
     * Filter env stack
1319
     *
1320
     * @param array $envs
1321
     * @param array $with
1322
     * @param array $without
1323
     *
1324
     * @return \ScssPhp\ScssPhp\Compiler\Environment
1325
     */
1326
    protected function filterWithWithout($envs, $with, $without)
1327
    {
1328
        $filtered = [];
1329
1330
        foreach ($envs as $e) {
1331
            if ($e->block && ! $this->isWith($e->block, $with, $without)) {
1332
                $ec = clone $e;
1333
                $ec->block     = null;
1334
                $ec->selectors = [];
1335
1336
                $filtered[] = $ec;
1337
            } else {
1338
                $filtered[] = $e;
1339
            }
1340
        }
1341
1342
        return $this->extractEnv($filtered);
1343
    }
1344
1345
    /**
1346
     * Filter WITH rules
1347
     *
1348
     * @param \ScssPhp\ScssPhp\Block|\ScssPhp\ScssPhp\Formatter\OutputBlock $block
1349
     * @param array                                                         $with
1350
     * @param array                                                         $without
1351
     *
1352
     * @return boolean
1353
     */
1354
    protected function isWith($block, $with, $without)
1355
    {
1356
        if (isset($block->type)) {
1357
            if ($block->type === Type::T_MEDIA) {
1358
                return $this->testWithWithout('media', $with, $without);
1359
            }
1360
1361
            if ($block->type === Type::T_DIRECTIVE) {
1362
                if (isset($block->name)) {
1363
                    return $this->testWithWithout($block->name, $with, $without);
1364
                } elseif (isset($block->selectors) && preg_match(',@(\w+),ims', json_encode($block->selectors), $m)) {
1365
                    return $this->testWithWithout($m[1], $with, $without);
1366
                } else {
1367
                    return $this->testWithWithout('???', $with, $without);
1368
                }
1369
            }
1370
        } elseif (isset($block->selectors)) {
1371
            // a selector starting with number is a keyframe rule
1372
            if (\count($block->selectors)) {
1373
                $s = reset($block->selectors);
1374
1375
                while (\is_array($s)) {
1376
                    $s = reset($s);
1377
                }
1378
1379
                if (\is_object($s) && $s instanceof Node\Number) {
1380
                    return $this->testWithWithout('keyframes', $with, $without);
1381
                }
1382
            }
1383
1384
            return $this->testWithWithout('rule', $with, $without);
1385
        }
1386
1387
        return true;
1388
    }
1389
1390
    /**
1391
     * Test a single type of block against with/without lists
1392
     *
1393
     * @param string $what
1394
     * @param array  $with
1395
     * @param array  $without
1396
     *
1397
     * @return boolean
1398
     *   true if the block should be kept, false to reject
1399
     */
1400
    protected function testWithWithout($what, $with, $without)
1401
    {
1402
        // if without, reject only if in the list (or 'all' is in the list)
1403
        if (\count($without)) {
1404
            return (isset($without[$what]) || isset($without['all'])) ? false : true;
1405
        }
1406
1407
        // otherwise reject all what is not in the with list
1408
        return (isset($with[$what]) || isset($with['all'])) ? true : false;
1409
    }
1410
1411
1412
    /**
1413
     * Compile keyframe block
1414
     *
1415
     * @param \ScssPhp\ScssPhp\Block $block
1416
     * @param array                  $selectors
1417
     */
1418
    protected function compileKeyframeBlock(Block $block, $selectors)
1419
    {
1420
        $env = $this->pushEnv($block);
1421
1422
        $envs = $this->compactEnv($env);
1423
1424
        $this->env = $this->extractEnv(array_filter($envs, function (Environment $e) {
1425
            return ! isset($e->block->selectors);
1426
        }));
1427
1428
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
1429
        $this->scope->depth = 1;
1430
        $this->scope->parent->children[] = $this->scope;
1431
1432
        $this->compileChildrenNoReturn($block->children, $this->scope);
1433
1434
        $this->scope = $this->scope->parent;
1435
        $this->env   = $this->extractEnv($envs);
1436
1437
        $this->popEnv();
1438
    }
1439
1440
    /**
1441
     * Compile nested properties lines
1442
     *
1443
     * @param \ScssPhp\ScssPhp\Block                 $block
1444
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1445
     */
1446
    protected function compileNestedPropertiesBlock(Block $block, OutputBlock $out)
1447
    {
1448
        $prefix = $this->compileValue($block->prefix) . '-';
0 ignored issues
show
Bug introduced by
Are you sure $this->compileValue($block->prefix) of type array|string can be used in concatenation? ( Ignorable by Annotation )

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

1448
        $prefix = /** @scrutinizer ignore-type */ $this->compileValue($block->prefix) . '-';
Loading history...
Bug introduced by
The property prefix does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
1449
1450
        $nested = $this->makeOutputBlock($block->type);
1451
        $nested->parent = $out;
1452
1453
        if ($block->hasValue) {
0 ignored issues
show
Bug introduced by
The property hasValue does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
1454
            $nested->depth = $out->depth + 1;
1455
        }
1456
1457
        $out->children[] = $nested;
1458
1459
        foreach ($block->children as $child) {
1460
            switch ($child[0]) {
1461
                case Type::T_ASSIGN:
1462
                    array_unshift($child[1][2], $prefix);
1463
                    break;
1464
1465
                case Type::T_NESTED_PROPERTY:
1466
                    array_unshift($child[1]->prefix[2], $prefix);
1467
                    break;
1468
            }
1469
1470
            $this->compileChild($child, $nested);
1471
        }
1472
    }
1473
1474
    /**
1475
     * Compile nested block
1476
     *
1477
     * @param \ScssPhp\ScssPhp\Block $block
1478
     * @param array                  $selectors
1479
     */
1480
    protected function compileNestedBlock(Block $block, $selectors)
1481
    {
1482
        $this->pushEnv($block);
1483
1484
        $this->scope = $this->makeOutputBlock($block->type, $selectors);
1485
        $this->scope->parent->children[] = $this->scope;
1486
1487
        // wrap assign children in a block
1488
        // except for @font-face
1489
        if ($block->type !== Type::T_DIRECTIVE || $block->name !== 'font-face') {
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
1490
            // need wrapping?
1491
            $needWrapping = false;
1492
1493
            foreach ($block->children as $child) {
1494
                if ($child[0] === Type::T_ASSIGN) {
1495
                    $needWrapping = true;
1496
                    break;
1497
                }
1498
            }
1499
1500
            if ($needWrapping) {
1501
                $wrapped = new Block();
1502
                $wrapped->sourceName   = $block->sourceName;
1503
                $wrapped->sourceIndex  = $block->sourceIndex;
1504
                $wrapped->sourceLine   = $block->sourceLine;
1505
                $wrapped->sourceColumn = $block->sourceColumn;
1506
                $wrapped->selectors    = [];
1507
                $wrapped->comments     = [];
1508
                $wrapped->parent       = $block;
1509
                $wrapped->children     = $block->children;
1510
                $wrapped->selfParent   = $block->selfParent;
1511
1512
                $block->children = [[Type::T_BLOCK, $wrapped]];
1513
            }
1514
        }
1515
1516
        $this->compileChildrenNoReturn($block->children, $this->scope);
1517
1518
        $this->scope = $this->scope->parent;
1519
1520
        $this->popEnv();
1521
    }
1522
1523
    /**
1524
     * Recursively compiles a block.
1525
     *
1526
     * A block is analogous to a CSS block in most cases. A single SCSS document
1527
     * is encapsulated in a block when parsed, but it does not have parent tags
1528
     * so all of its children appear on the root level when compiled.
1529
     *
1530
     * Blocks are made up of selectors and children.
1531
     *
1532
     * The children of a block are just all the blocks that are defined within.
1533
     *
1534
     * Compiling the block involves pushing a fresh environment on the stack,
1535
     * and iterating through the props, compiling each one.
1536
     *
1537
     * @see Compiler::compileChild()
1538
     *
1539
     * @param \ScssPhp\ScssPhp\Block $block
1540
     */
1541
    protected function compileBlock(Block $block)
1542
    {
1543
        $env = $this->pushEnv($block);
1544
        $env->selectors = $this->evalSelectors($block->selectors);
0 ignored issues
show
Bug introduced by
The property selectors does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
1545
1546
        $out = $this->makeOutputBlock(null);
1547
1548
        if (isset($this->lineNumberStyle) && \count($env->selectors) && \count($block->children)) {
1549
            $annotation = $this->makeOutputBlock(Type::T_COMMENT);
1550
            $annotation->depth = 0;
1551
1552
            $file = $this->sourceNames[$block->sourceIndex];
1553
            $line = $block->sourceLine;
1554
1555
            switch ($this->lineNumberStyle) {
1556
                case static::LINE_COMMENTS:
1557
                    $annotation->lines[] = '/* line ' . $line
1558
                                         . ($file ? ', ' . $file : '')
1559
                                         . ' */';
1560
                    break;
1561
1562
                case static::DEBUG_INFO:
1563
                    $annotation->lines[] = '@media -sass-debug-info{'
1564
                                         . ($file ? 'filename{font-family:"' . $file . '"}' : '')
1565
                                         . 'line{font-family:' . $line . '}}';
1566
                    break;
1567
            }
1568
1569
            $this->scope->children[] = $annotation;
1570
        }
1571
1572
        $this->scope->children[] = $out;
1573
1574
        if (\count($block->children)) {
1575
            $out->selectors = $this->multiplySelectors($env, $block->selfParent);
1576
1577
            // propagate selfParent to the children where they still can be useful
1578
            $selfParentSelectors = null;
1579
1580
            if (isset($block->selfParent->selectors)) {
1581
                $selfParentSelectors = $block->selfParent->selectors;
1582
                $block->selfParent->selectors = $out->selectors;
1583
            }
1584
1585
            $this->compileChildrenNoReturn($block->children, $out, $block->selfParent);
1586
1587
            // and revert for the following children of the same block
1588
            if ($selfParentSelectors) {
1589
                $block->selfParent->selectors = $selfParentSelectors;
1590
            }
1591
        }
1592
1593
        $this->popEnv();
1594
    }
1595
1596
1597
    /**
1598
     * Compile the value of a comment that can have interpolation
1599
     *
1600
     * @param array   $value
1601
     * @param boolean $pushEnv
1602
     *
1603
     * @return array|mixed|string
1604
     */
1605
    protected function compileCommentValue($value, $pushEnv = false)
1606
    {
1607
        $c = $value[1];
1608
1609
        if (isset($value[2])) {
1610
            if ($pushEnv) {
1611
                $this->pushEnv();
1612
            }
1613
1614
            $ignoreCallStackMessage = $this->ignoreCallStackMessage;
1615
            $this->ignoreCallStackMessage = true;
1616
1617
            try {
1618
                $c = $this->compileValue($value[2]);
1619
            } catch (\Exception $e) {
1620
                // ignore error in comment compilation which are only interpolation
1621
            }
1622
1623
            $this->ignoreCallStackMessage = $ignoreCallStackMessage;
1624
1625
            if ($pushEnv) {
1626
                $this->popEnv();
1627
            }
1628
        }
1629
1630
        return $c;
1631
    }
1632
1633
    /**
1634
     * Compile root level comment
1635
     *
1636
     * @param array $block
1637
     */
1638
    protected function compileComment($block)
1639
    {
1640
        $out = $this->makeOutputBlock(Type::T_COMMENT);
1641
        $out->lines[] = $this->compileCommentValue($block, true);
1642
1643
        $this->scope->children[] = $out;
1644
    }
1645
1646
    /**
1647
     * Evaluate selectors
1648
     *
1649
     * @param array $selectors
1650
     *
1651
     * @return array
1652
     */
1653
    protected function evalSelectors($selectors)
1654
    {
1655
        $this->shouldEvaluate = false;
1656
1657
        $selectors = array_map([$this, 'evalSelector'], $selectors);
1658
1659
        // after evaluating interpolates, we might need a second pass
1660
        if ($this->shouldEvaluate) {
1661
            $selectors = $this->replaceSelfSelector($selectors, '&');
1662
            $buffer    = $this->collapseSelectors($selectors);
1663
            $parser    = $this->parserFactory(__METHOD__);
1664
1665
            if ($parser->parseSelector($buffer, $newSelectors)) {
1666
                $selectors = array_map([$this, 'evalSelector'], $newSelectors);
1667
            }
1668
        }
1669
1670
        return $selectors;
1671
    }
1672
1673
    /**
1674
     * Evaluate selector
1675
     *
1676
     * @param array $selector
1677
     *
1678
     * @return array
1679
     */
1680
    protected function evalSelector($selector)
1681
    {
1682
        return array_map([$this, 'evalSelectorPart'], $selector);
1683
    }
1684
1685
    /**
1686
     * Evaluate selector part; replaces all the interpolates, stripping quotes
1687
     *
1688
     * @param array $part
1689
     *
1690
     * @return array
1691
     */
1692
    protected function evalSelectorPart($part)
1693
    {
1694
        foreach ($part as &$p) {
1695
            if (\is_array($p) && ($p[0] === Type::T_INTERPOLATE || $p[0] === Type::T_STRING)) {
1696
                $p = $this->compileValue($p);
1697
1698
                // force re-evaluation
1699
                if (strpos($p, '&') !== false || strpos($p, ',') !== false) {
0 ignored issues
show
Bug introduced by
It seems like $p can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, 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

1699
                if (strpos(/** @scrutinizer ignore-type */ $p, '&') !== false || strpos($p, ',') !== false) {
Loading history...
1700
                    $this->shouldEvaluate = true;
1701
                }
1702
            } elseif (
1703
                \is_string($p) && \strlen($p) >= 2 &&
1704
                ($first = $p[0]) && ($first === '"' || $first === "'") &&
1705
                substr($p, -1) === $first
1706
            ) {
1707
                $p = substr($p, 1, -1);
1708
            }
1709
        }
1710
1711
        return $this->flattenSelectorSingle($part);
1712
    }
1713
1714
    /**
1715
     * Collapse selectors
1716
     *
1717
     * @param array   $selectors
1718
     * @param boolean $selectorFormat
1719
     *   if false return a collapsed string
1720
     *   if true return an array description of a structured selector
1721
     *
1722
     * @return string
1723
     */
1724
    protected function collapseSelectors($selectors, $selectorFormat = false)
1725
    {
1726
        $parts = [];
1727
1728
        foreach ($selectors as $selector) {
1729
            $output = [];
1730
            $glueNext = false;
1731
1732
            foreach ($selector as $node) {
1733
                $compound = '';
1734
1735
                array_walk_recursive(
1736
                    $node,
1737
                    function ($value, $key) use (&$compound) {
1738
                        $compound .= $value;
1739
                    }
1740
                );
1741
1742
                if ($selectorFormat && $this->isImmediateRelationshipCombinator($compound)) {
1743
                    if (\count($output)) {
1744
                        $output[\count($output) - 1] .= ' ' . $compound;
1745
                    } else {
1746
                        $output[] = $compound;
1747
                    }
1748
1749
                    $glueNext = true;
1750
                } elseif ($glueNext) {
1751
                    $output[\count($output) - 1] .= ' ' . $compound;
1752
                    $glueNext = false;
1753
                } else {
1754
                    $output[] = $compound;
1755
                }
1756
            }
1757
1758
            if ($selectorFormat) {
1759
                foreach ($output as &$o) {
1760
                    $o = [Type::T_STRING, '', [$o]];
1761
                }
1762
1763
                $output = [Type::T_LIST, ' ', $output];
1764
            } else {
1765
                $output = implode(' ', $output);
1766
            }
1767
1768
            $parts[] = $output;
1769
        }
1770
1771
        if ($selectorFormat) {
1772
            $parts = [Type::T_LIST, ',', $parts];
1773
        } else {
1774
            $parts = implode(', ', $parts);
1775
        }
1776
1777
        return $parts;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $parts also could return the type array<integer,array|string|string[]> which is incompatible with the documented return type string.
Loading history...
1778
    }
1779
1780
    /**
1781
     * Parse down the selector and revert [self] to "&" before a reparsing
1782
     *
1783
     * @param array $selectors
1784
     *
1785
     * @return array
1786
     */
1787
    protected function replaceSelfSelector($selectors, $replace = null)
1788
    {
1789
        foreach ($selectors as &$part) {
1790
            if (\is_array($part)) {
1791
                if ($part === [Type::T_SELF]) {
1792
                    if (\is_null($replace)) {
1793
                        $replace = $this->reduce([Type::T_SELF]);
1794
                        $replace = $this->compileValue($replace);
1795
                    }
1796
                    $part = $replace;
1797
                } else {
1798
                    $part = $this->replaceSelfSelector($part, $replace);
1799
                }
1800
            }
1801
        }
1802
1803
        return $selectors;
1804
    }
1805
1806
    /**
1807
     * Flatten selector single; joins together .classes and #ids
1808
     *
1809
     * @param array $single
1810
     *
1811
     * @return array
1812
     */
1813
    protected function flattenSelectorSingle($single)
1814
    {
1815
        $joined = [];
1816
1817
        foreach ($single as $part) {
1818
            if (
1819
                empty($joined) ||
1820
                ! \is_string($part) ||
1821
                preg_match('/[\[.:#%]/', $part)
1822
            ) {
1823
                $joined[] = $part;
1824
                continue;
1825
            }
1826
1827
            if (\is_array(end($joined))) {
1828
                $joined[] = $part;
1829
            } else {
1830
                $joined[\count($joined) - 1] .= $part;
1831
            }
1832
        }
1833
1834
        return $joined;
1835
    }
1836
1837
    /**
1838
     * Compile selector to string; self(&) should have been replaced by now
1839
     *
1840
     * @param string|array $selector
1841
     *
1842
     * @return string
1843
     */
1844
    protected function compileSelector($selector)
1845
    {
1846
        if (! \is_array($selector)) {
1847
            return $selector; // media and the like
1848
        }
1849
1850
        return implode(
1851
            ' ',
1852
            array_map(
1853
                [$this, 'compileSelectorPart'],
1854
                $selector
1855
            )
1856
        );
1857
    }
1858
1859
    /**
1860
     * Compile selector part
1861
     *
1862
     * @param array $piece
1863
     *
1864
     * @return string
1865
     */
1866
    protected function compileSelectorPart($piece)
1867
    {
1868
        foreach ($piece as &$p) {
1869
            if (! \is_array($p)) {
1870
                continue;
1871
            }
1872
1873
            switch ($p[0]) {
1874
                case Type::T_SELF:
1875
                    $p = '&';
1876
                    break;
1877
1878
                default:
1879
                    $p = $this->compileValue($p);
1880
                    break;
1881
            }
1882
        }
1883
1884
        return implode($piece);
1885
    }
1886
1887
    /**
1888
     * Has selector placeholder?
1889
     *
1890
     * @param array $selector
1891
     *
1892
     * @return boolean
1893
     */
1894
    protected function hasSelectorPlaceholder($selector)
1895
    {
1896
        if (! \is_array($selector)) {
0 ignored issues
show
introduced by
The condition is_array($selector) is always true.
Loading history...
1897
            return false;
1898
        }
1899
1900
        foreach ($selector as $parts) {
1901
            foreach ($parts as $part) {
1902
                if (\strlen($part) && '%' === $part[0]) {
1903
                    return true;
1904
                }
1905
            }
1906
        }
1907
1908
        return false;
1909
    }
1910
1911
    protected function pushCallStack($name = '')
1912
    {
1913
        $this->callStack[] = [
1914
          'n' => $name,
1915
          Parser::SOURCE_INDEX => $this->sourceIndex,
1916
          Parser::SOURCE_LINE => $this->sourceLine,
1917
          Parser::SOURCE_COLUMN => $this->sourceColumn
1918
        ];
1919
1920
        // infinite calling loop
1921
        if (\count($this->callStack) > 25000) {
1922
            // not displayed but you can var_dump it to deep debug
1923
            $msg = $this->callStackMessage(true, 100);
0 ignored issues
show
Unused Code introduced by
The assignment to $msg is dead and can be removed.
Loading history...
1924
            $msg = 'Infinite calling loop';
1925
1926
            throw $this->error($msg);
1927
        }
1928
    }
1929
1930
    protected function popCallStack()
1931
    {
1932
        array_pop($this->callStack);
1933
    }
1934
1935
    /**
1936
     * Compile children and return result
1937
     *
1938
     * @param array                                  $stms
1939
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1940
     * @param string                                 $traceName
1941
     *
1942
     * @return array|null
1943
     */
1944
    protected function compileChildren($stms, OutputBlock $out, $traceName = '')
1945
    {
1946
        $this->pushCallStack($traceName);
1947
1948
        foreach ($stms as $stm) {
1949
            $ret = $this->compileChild($stm, $out);
1950
1951
            if (isset($ret)) {
1952
                $this->popCallStack();
1953
1954
                return $ret;
1955
            }
1956
        }
1957
1958
        $this->popCallStack();
1959
1960
        return null;
1961
    }
1962
1963
    /**
1964
     * Compile children and throw exception if unexpected @return
1965
     *
1966
     * @param array                                  $stms
1967
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
1968
     * @param \ScssPhp\ScssPhp\Block                 $selfParent
1969
     * @param string                                 $traceName
1970
     *
1971
     * @throws \Exception
1972
     */
1973
    protected function compileChildrenNoReturn($stms, OutputBlock $out, $selfParent = null, $traceName = '')
1974
    {
1975
        $this->pushCallStack($traceName);
1976
1977
        foreach ($stms as $stm) {
1978
            if ($selfParent && isset($stm[1]) && \is_object($stm[1]) && $stm[1] instanceof Block) {
1979
                $stm[1]->selfParent = $selfParent;
1980
                $ret = $this->compileChild($stm, $out);
1981
                $stm[1]->selfParent = null;
1982
            } elseif ($selfParent && \in_array($stm[0], [TYPE::T_INCLUDE, TYPE::T_EXTEND])) {
1983
                $stm['selfParent'] = $selfParent;
1984
                $ret = $this->compileChild($stm, $out);
1985
                unset($stm['selfParent']);
1986
            } else {
1987
                $ret = $this->compileChild($stm, $out);
1988
            }
1989
1990
            if (isset($ret)) {
1991
                throw $this->error('@return may only be used within a function');
1992
            }
1993
        }
1994
1995
        $this->popCallStack();
1996
    }
1997
1998
1999
    /**
2000
     * evaluate media query : compile internal value keeping the structure inchanged
2001
     *
2002
     * @param array $queryList
2003
     *
2004
     * @return array
2005
     */
2006
    protected function evaluateMediaQuery($queryList)
2007
    {
2008
        static $parser = null;
2009
2010
        $outQueryList = [];
2011
2012
        foreach ($queryList as $kql => $query) {
2013
            $shouldReparse = false;
2014
2015
            foreach ($query as $kq => $q) {
2016
                for ($i = 1; $i < \count($q); $i++) {
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...
2017
                    $value = $this->compileValue($q[$i]);
2018
2019
                    // the parser had no mean to know if media type or expression if it was an interpolation
2020
                    // so you need to reparse if the T_MEDIA_TYPE looks like anything else a media type
2021
                    if (
2022
                        $q[0] == Type::T_MEDIA_TYPE &&
2023
                        (strpos($value, '(') !== false ||
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, 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

2023
                        (strpos(/** @scrutinizer ignore-type */ $value, '(') !== false ||
Loading history...
2024
                        strpos($value, ')') !== false ||
2025
                        strpos($value, ':') !== false ||
2026
                        strpos($value, ',') !== false)
2027
                    ) {
2028
                        $shouldReparse = true;
2029
                    }
2030
2031
                    $queryList[$kql][$kq][$i] = [Type::T_KEYWORD, $value];
2032
                }
2033
            }
2034
2035
            if ($shouldReparse) {
2036
                if (\is_null($parser)) {
2037
                    $parser = $this->parserFactory(__METHOD__);
2038
                }
2039
2040
                $queryString = $this->compileMediaQuery([$queryList[$kql]]);
2041
                $queryString = reset($queryString);
2042
2043
                if (strpos($queryString, '@media ') === 0) {
2044
                    $queryString = substr($queryString, 7);
2045
                    $queries = [];
2046
2047
                    if ($parser->parseMediaQueryList($queryString, $queries)) {
2048
                        $queries = $this->evaluateMediaQuery($queries[2]);
2049
2050
                        while (\count($queries)) {
2051
                            $outQueryList[] = array_shift($queries);
2052
                        }
2053
2054
                        continue;
2055
                    }
2056
                }
2057
            }
2058
2059
            $outQueryList[] = $queryList[$kql];
2060
        }
2061
2062
        return $outQueryList;
2063
    }
2064
2065
    /**
2066
     * Compile media query
2067
     *
2068
     * @param array $queryList
2069
     *
2070
     * @return array
2071
     */
2072
    protected function compileMediaQuery($queryList)
2073
    {
2074
        $start   = '@media ';
2075
        $default = trim($start);
2076
        $out     = [];
2077
        $current = '';
2078
2079
        foreach ($queryList as $query) {
2080
            $type = null;
2081
            $parts = [];
2082
2083
            $mediaTypeOnly = true;
2084
2085
            foreach ($query as $q) {
2086
                if ($q[0] !== Type::T_MEDIA_TYPE) {
2087
                    $mediaTypeOnly = false;
2088
                    break;
2089
                }
2090
            }
2091
2092
            foreach ($query as $q) {
2093
                switch ($q[0]) {
2094
                    case Type::T_MEDIA_TYPE:
2095
                        $newType = array_map([$this, 'compileValue'], \array_slice($q, 1));
2096
2097
                        // combining not and anything else than media type is too risky and should be avoided
2098
                        if (! $mediaTypeOnly) {
2099
                            if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, $type) )) {
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

2099
                            if (\in_array(Type::T_NOT, $newType) || ($type && \in_array(Type::T_NOT, /** @scrutinizer ignore-type */ $type) )) {
Loading history...
2100
                                if ($type) {
2101
                                    array_unshift($parts, implode(' ', array_filter($type)));
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

2101
                                    array_unshift($parts, implode(' ', array_filter(/** @scrutinizer ignore-type */ $type)));
Loading history...
2102
                                }
2103
2104
                                if (! empty($parts)) {
2105
                                    if (\strlen($current)) {
2106
                                        $current .= $this->formatter->tagSeparator;
2107
                                    }
2108
2109
                                    $current .= implode(' and ', $parts);
2110
                                }
2111
2112
                                if ($current) {
2113
                                    $out[] = $start . $current;
2114
                                }
2115
2116
                                $current = '';
2117
                                $type    = null;
2118
                                $parts   = [];
2119
                            }
2120
                        }
2121
2122
                        if ($newType === ['all'] && $default) {
2123
                            $default = $start . 'all';
2124
                        }
2125
2126
                        // all can be safely ignored and mixed with whatever else
2127
                        if ($newType !== ['all']) {
2128
                            if ($type) {
2129
                                $type = $this->mergeMediaTypes($type, $newType);
0 ignored issues
show
Bug introduced by
$type of type void is incompatible with the type array expected by parameter $type1 of ScssPhp\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

2129
                                $type = $this->mergeMediaTypes(/** @scrutinizer ignore-type */ $type, $newType);
Loading history...
2130
2131
                                if (empty($type)) {
2132
                                    // merge failed : ignore this query that is not valid, skip to the next one
2133
                                    $parts = [];
0 ignored issues
show
Unused Code introduced by
The assignment to $parts is dead and can be removed.
Loading history...
2134
                                    $default = ''; // if everything fail, no @media at all
2135
                                    continue 3;
2136
                                }
2137
                            } else {
2138
                                $type = $newType;
2139
                            }
2140
                        }
2141
                        break;
2142
2143
                    case Type::T_MEDIA_EXPRESSION:
2144
                        if (isset($q[2])) {
2145
                            $parts[] = '('
2146
                                . $this->compileValue($q[1])
2147
                                . $this->formatter->assignSeparator
2148
                                . $this->compileValue($q[2])
2149
                                . ')';
2150
                        } else {
2151
                            $parts[] = '('
2152
                                . $this->compileValue($q[1])
2153
                                . ')';
2154
                        }
2155
                        break;
2156
2157
                    case Type::T_MEDIA_VALUE:
2158
                        $parts[] = $this->compileValue($q[1]);
2159
                        break;
2160
                }
2161
            }
2162
2163
            if ($type) {
2164
                array_unshift($parts, implode(' ', array_filter($type)));
2165
            }
2166
2167
            if (! empty($parts)) {
2168
                if (\strlen($current)) {
2169
                    $current .= $this->formatter->tagSeparator;
2170
                }
2171
2172
                $current .= implode(' and ', $parts);
2173
            }
2174
        }
2175
2176
        if ($current) {
2177
            $out[] = $start . $current;
2178
        }
2179
2180
        // no @media type except all, and no conflict?
2181
        if (! $out && $default) {
2182
            $out[] = $default;
2183
        }
2184
2185
        return $out;
2186
    }
2187
2188
    /**
2189
     * Merge direct relationships between selectors
2190
     *
2191
     * @param array $selectors1
2192
     * @param array $selectors2
2193
     *
2194
     * @return array
2195
     */
2196
    protected function mergeDirectRelationships($selectors1, $selectors2)
2197
    {
2198
        if (empty($selectors1) || empty($selectors2)) {
2199
            return array_merge($selectors1, $selectors2);
2200
        }
2201
2202
        $part1 = end($selectors1);
2203
        $part2 = end($selectors2);
2204
2205
        if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2206
            return array_merge($selectors1, $selectors2);
2207
        }
2208
2209
        $merged = [];
2210
2211
        do {
2212
            $part1 = array_pop($selectors1);
2213
            $part2 = array_pop($selectors2);
2214
2215
            if (! $this->isImmediateRelationshipCombinator($part1[0]) && $part1 !== $part2) {
2216
                if ($this->isImmediateRelationshipCombinator(reset($merged)[0])) {
2217
                    array_unshift($merged, [$part1[0] . $part2[0]]);
2218
                    $merged = array_merge($selectors1, $selectors2, $merged);
2219
                } else {
2220
                    $merged = array_merge($selectors1, [$part1], $selectors2, [$part2], $merged);
2221
                }
2222
2223
                break;
2224
            }
2225
2226
            array_unshift($merged, $part1);
2227
        } while (! empty($selectors1) && ! empty($selectors2));
2228
2229
        return $merged;
2230
    }
2231
2232
    /**
2233
     * Merge media types
2234
     *
2235
     * @param array $type1
2236
     * @param array $type2
2237
     *
2238
     * @return array|null
2239
     */
2240
    protected function mergeMediaTypes($type1, $type2)
2241
    {
2242
        if (empty($type1)) {
2243
            return $type2;
2244
        }
2245
2246
        if (empty($type2)) {
2247
            return $type1;
2248
        }
2249
2250
        if (\count($type1) > 1) {
2251
            $m1 = strtolower($type1[0]);
2252
            $t1 = strtolower($type1[1]);
2253
        } else {
2254
            $m1 = '';
2255
            $t1 = strtolower($type1[0]);
2256
        }
2257
2258
        if (\count($type2) > 1) {
2259
            $m2 = strtolower($type2[0]);
2260
            $t2 = strtolower($type2[1]);
2261
        } else {
2262
            $m2 = '';
2263
            $t2 = strtolower($type2[0]);
2264
        }
2265
2266
        if (($m1 === Type::T_NOT) ^ ($m2 === Type::T_NOT)) {
2267
            if ($t1 === $t2) {
2268
                return null;
2269
            }
2270
2271
            return [
2272
                $m1 === Type::T_NOT ? $m2 : $m1,
2273
                $m1 === Type::T_NOT ? $t2 : $t1,
2274
            ];
2275
        }
2276
2277
        if ($m1 === Type::T_NOT && $m2 === Type::T_NOT) {
2278
            // CSS has no way of representing "neither screen nor print"
2279
            if ($t1 !== $t2) {
2280
                return null;
2281
            }
2282
2283
            return [Type::T_NOT, $t1];
2284
        }
2285
2286
        if ($t1 !== $t2) {
2287
            return null;
2288
        }
2289
2290
        // t1 == t2, neither m1 nor m2 are "not"
2291
        return [empty($m1) ? $m2 : $m1, $t1];
2292
    }
2293
2294
    /**
2295
     * Compile import; returns true if the value was something that could be imported
2296
     *
2297
     * @param array                                  $rawPath
2298
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2299
     * @param boolean                                $once
2300
     *
2301
     * @return boolean
2302
     */
2303
    protected function compileImport($rawPath, OutputBlock $out, $once = false)
2304
    {
2305
        if ($rawPath[0] === Type::T_STRING) {
2306
            $path = $this->compileStringContent($rawPath);
2307
2308
            if (strpos($path, 'url(') !== 0 && $path = $this->findImport($path)) {
2309
                if (! $once || ! \in_array($path, $this->importedFiles)) {
2310
                    $this->importFile($path, $out);
2311
                    $this->importedFiles[] = $path;
2312
                }
2313
2314
                return true;
2315
            }
2316
2317
            $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2318
2319
            return false;
2320
        }
2321
2322
        if ($rawPath[0] === Type::T_LIST) {
2323
            // handle a list of strings
2324
            if (\count($rawPath[2]) === 0) {
2325
                return false;
2326
            }
2327
2328
            foreach ($rawPath[2] as $path) {
2329
                if ($path[0] !== Type::T_STRING) {
2330
                    $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2331
2332
                    return false;
2333
                }
2334
            }
2335
2336
            foreach ($rawPath[2] as $path) {
2337
                $this->compileImport($path, $out, $once);
2338
            }
2339
2340
            return true;
2341
        }
2342
2343
        $this->appendRootDirective('@import ' . $this->compileImportPath($rawPath) . ';', $out);
2344
2345
        return false;
2346
    }
2347
2348
    /**
2349
     * @param $rawPath
2350
     * @return string
2351
     * @throws CompilerException
2352
     */
2353
    protected function compileImportPath($rawPath)
2354
    {
2355
        $path = $this->compileValue($rawPath);
2356
2357
        // case url() without quotes : supress \r \n remaining in the path
2358
        // if this is a real string there can not be CR or LF char
2359
        if (strpos($path, 'url(') === 0) {
0 ignored issues
show
Bug introduced by
It seems like $path can also be of type array; however, parameter $haystack of strpos() does only seem to accept string, 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

2359
        if (strpos(/** @scrutinizer ignore-type */ $path, 'url(') === 0) {
Loading history...
2360
            $path = str_replace(array("\r", "\n"), array('', ' '), $path);
2361
        } else {
2362
            // if this is a file name in a string, spaces shoudl be escaped
2363
            $path = $this->reduce($rawPath);
2364
            $path = $this->escapeImportPathString($path);
2365
            $path = $this->compileValue($path);
2366
        }
2367
2368
        return $path;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $path also could return the type array|array<integer,mixed|string> which is incompatible with the documented return type string.
Loading history...
2369
    }
2370
2371
    /**
2372
     * @param array $path
2373
     * @return array
2374
     * @throws CompilerException
2375
     */
2376
    protected function escapeImportPathString($path)
2377
    {
2378
        switch ($path[0]) {
2379
            case Type::T_LIST:
2380
                foreach ($path[2] as $k => $v) {
2381
                    $path[2][$k] = $this->escapeImportPathString($v);
2382
                }
2383
                break;
2384
            case Type::T_STRING:
2385
                if ($path[1]) {
2386
                    $path = $this->compileValue($path);
2387
                    $path = str_replace(' ', '\\ ', $path);
2388
                    $path = [Type::T_KEYWORD, $path];
2389
                }
2390
                break;
2391
        }
2392
2393
        return $path;
2394
    }
2395
2396
    /**
2397
     * Append a root directive like @import or @charset as near as the possible from the source code
2398
     * (keeping before comments, @import and @charset coming before in the source code)
2399
     *
2400
     * @param string                                        $line
2401
     * @param @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2402
     * @param array                                         $allowed
2403
     */
2404
    protected function appendRootDirective($line, $out, $allowed = [Type::T_COMMENT])
2405
    {
2406
        $root = $out;
2407
2408
        while ($root->parent) {
2409
            $root = $root->parent;
2410
        }
2411
2412
        $i = 0;
2413
2414
        while ($i < \count($root->children)) {
2415
            if (! isset($root->children[$i]->type) || ! \in_array($root->children[$i]->type, $allowed)) {
2416
                break;
2417
            }
2418
2419
            $i++;
2420
        }
2421
2422
        // remove incompatible children from the bottom of the list
2423
        $saveChildren = [];
2424
2425
        while ($i < \count($root->children)) {
2426
            $saveChildren[] = array_pop($root->children);
2427
        }
2428
2429
        // insert the directive as a comment
2430
        $child = $this->makeOutputBlock(Type::T_COMMENT);
2431
        $child->lines[]      = $line;
2432
        $child->sourceName   = $this->sourceNames[$this->sourceIndex];
2433
        $child->sourceLine   = $this->sourceLine;
2434
        $child->sourceColumn = $this->sourceColumn;
2435
2436
        $root->children[] = $child;
2437
2438
        // repush children
2439
        while (\count($saveChildren)) {
2440
            $root->children[] = array_pop($saveChildren);
2441
        }
2442
    }
2443
2444
    /**
2445
     * Append lines to the current output block:
2446
     * directly to the block or through a child if necessary
2447
     *
2448
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2449
     * @param string                                 $type
2450
     * @param string|mixed                           $line
2451
     */
2452
    protected function appendOutputLine(OutputBlock $out, $type, $line)
2453
    {
2454
        $outWrite = &$out;
2455
2456
        // check if it's a flat output or not
2457
        if (\count($out->children)) {
2458
            $lastChild = &$out->children[\count($out->children) - 1];
2459
2460
            if (
2461
                $lastChild->depth === $out->depth &&
2462
                \is_null($lastChild->selectors) &&
2463
                ! \count($lastChild->children)
2464
            ) {
2465
                $outWrite = $lastChild;
2466
            } else {
2467
                $nextLines = $this->makeOutputBlock($type);
2468
                $nextLines->parent = $out;
2469
                $nextLines->depth  = $out->depth;
2470
2471
                $out->children[] = $nextLines;
2472
                $outWrite = &$nextLines;
2473
            }
2474
        }
2475
2476
        $outWrite->lines[] = $line;
2477
    }
2478
2479
    /**
2480
     * Compile child; returns a value to halt execution
2481
     *
2482
     * @param array                                  $child
2483
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
2484
     *
2485
     * @return array
2486
     */
2487
    protected function compileChild($child, OutputBlock $out)
2488
    {
2489
        if (isset($child[Parser::SOURCE_LINE])) {
2490
            $this->sourceIndex  = isset($child[Parser::SOURCE_INDEX]) ? $child[Parser::SOURCE_INDEX] : null;
2491
            $this->sourceLine   = isset($child[Parser::SOURCE_LINE]) ? $child[Parser::SOURCE_LINE] : -1;
2492
            $this->sourceColumn = isset($child[Parser::SOURCE_COLUMN]) ? $child[Parser::SOURCE_COLUMN] : -1;
2493
        } elseif (\is_array($child) && isset($child[1]->sourceLine)) {
2494
            $this->sourceIndex  = $child[1]->sourceIndex;
2495
            $this->sourceLine   = $child[1]->sourceLine;
2496
            $this->sourceColumn = $child[1]->sourceColumn;
2497
        } elseif (! empty($out->sourceLine) && ! empty($out->sourceName)) {
2498
            $this->sourceLine   = $out->sourceLine;
2499
            $this->sourceIndex  = array_search($out->sourceName, $this->sourceNames);
2500
            $this->sourceColumn = $out->sourceColumn;
2501
2502
            if ($this->sourceIndex === false) {
2503
                $this->sourceIndex = null;
2504
            }
2505
        }
2506
2507
        switch ($child[0]) {
2508
            case Type::T_SCSSPHP_IMPORT_ONCE:
2509
                $rawPath = $this->reduce($child[1]);
2510
2511
                $this->compileImport($rawPath, $out, true);
2512
                break;
2513
2514
            case Type::T_IMPORT:
2515
                $rawPath = $this->reduce($child[1]);
2516
2517
                $this->compileImport($rawPath, $out);
2518
                break;
2519
2520
            case Type::T_DIRECTIVE:
2521
                $this->compileDirective($child[1], $out);
2522
                break;
2523
2524
            case Type::T_AT_ROOT:
2525
                $this->compileAtRoot($child[1]);
2526
                break;
2527
2528
            case Type::T_MEDIA:
2529
                $this->compileMedia($child[1]);
2530
                break;
2531
2532
            case Type::T_BLOCK:
2533
                $this->compileBlock($child[1]);
2534
                break;
2535
2536
            case Type::T_CHARSET:
2537
                if (! $this->charsetSeen) {
2538
                    $this->charsetSeen = true;
2539
                    $this->appendRootDirective('@charset ' . $this->compileValue($child[1]) . ';', $out);
2540
                }
2541
                break;
2542
2543
            case Type::T_CUSTOM_PROPERTY:
2544
                list(, $name, $value) = $child;
2545
                $compiledName = $this->compileValue($name);
2546
2547
                // if the value reduces to null from something else then
2548
                // the property should be discarded
2549
                if ($value[0] !== Type::T_NULL) {
2550
                    $value = $this->reduce($value);
2551
2552
                    if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2553
                        break;
2554
                    }
2555
                }
2556
2557
                $compiledValue = $this->compileValue($value);
2558
2559
                $line = $this->formatter->customProperty(
2560
                    $compiledName,
2561
                    $compiledValue
2562
                );
2563
2564
                $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2565
                break;
2566
2567
            case Type::T_ASSIGN:
2568
                list(, $name, $value) = $child;
2569
2570
                if ($name[0] === Type::T_VARIABLE) {
2571
                    $flags     = isset($child[3]) ? $child[3] : [];
2572
                    $isDefault = \in_array('!default', $flags);
2573
                    $isGlobal  = \in_array('!global', $flags);
2574
2575
                    if ($isGlobal) {
2576
                        $this->set($name[1], $this->reduce($value), false, $this->rootEnv, $value);
2577
                        break;
2578
                    }
2579
2580
                    $shouldSet = $isDefault &&
2581
                        (\is_null($result = $this->get($name[1], false)) ||
2582
                        $result === static::$null);
2583
2584
                    if (! $isDefault || $shouldSet) {
2585
                        $this->set($name[1], $this->reduce($value), true, null, $value);
2586
                    }
2587
                    break;
2588
                }
2589
2590
                $compiledName = $this->compileValue($name);
2591
2592
                // handle shorthand syntaxes : size / line-height...
2593
                if (\in_array($compiledName, ['font', 'grid-row', 'grid-column', 'border-radius'])) {
2594
                    if ($value[0] === Type::T_VARIABLE) {
2595
                        // if the font value comes from variable, the content is already reduced
2596
                        // (i.e., formulas were already calculated), so we need the original unreduced value
2597
                        $value = $this->get($value[1], true, null, true);
2598
                    }
2599
2600
                    $shorthandValue=&$value;
2601
2602
                    $shorthandDividerNeedsUnit = false;
2603
                    $maxListElements           = null;
2604
                    $maxShorthandDividers      = 1;
2605
2606
                    switch ($compiledName) {
2607
                        case 'border-radius':
2608
                            $maxListElements = 4;
2609
                            $shorthandDividerNeedsUnit = true;
2610
                            break;
2611
                    }
2612
2613
                    if ($compiledName === 'font' && $value[0] === Type::T_LIST && $value[1] === ',') {
2614
                        // this is the case if more than one font is given: example: "font: 400 1em/1.3 arial,helvetica"
2615
                        // we need to handle the first list element
2616
                        $shorthandValue=&$value[2][0];
2617
                    }
2618
2619
                    if ($shorthandValue[0] === Type::T_EXPRESSION && $shorthandValue[1] === '/') {
2620
                        $revert = true;
2621
2622
                        if ($shorthandDividerNeedsUnit) {
2623
                            $divider = $shorthandValue[3];
2624
2625
                            if (\is_array($divider)) {
2626
                                $divider = $this->reduce($divider, true);
2627
                            }
2628
2629
                            if (\intval($divider->dimension) && ! \count($divider->units)) {
2630
                                $revert = false;
2631
                            }
2632
                        }
2633
2634
                        if ($revert) {
2635
                            $shorthandValue = $this->expToString($shorthandValue);
2636
                        }
2637
                    } elseif ($shorthandValue[0] === Type::T_LIST) {
2638
                        foreach ($shorthandValue[2] as &$item) {
2639
                            if ($item[0] === Type::T_EXPRESSION && $item[1] === '/') {
2640
                                if ($maxShorthandDividers > 0) {
2641
                                    $revert = true;
2642
2643
                                    // if the list of values is too long, this has to be a shorthand,
2644
                                    // otherwise it could be a real division
2645
                                    if (\is_null($maxListElements) || \count($shorthandValue[2]) <= $maxListElements) {
2646
                                        if ($shorthandDividerNeedsUnit) {
2647
                                            $divider = $item[3];
2648
2649
                                            if (\is_array($divider)) {
2650
                                                $divider = $this->reduce($divider, true);
2651
                                            }
2652
2653
                                            if (\intval($divider->dimension) && ! \count($divider->units)) {
2654
                                                $revert = false;
2655
                                            }
2656
                                        }
2657
                                    }
2658
2659
                                    if ($revert) {
2660
                                        $item = $this->expToString($item);
2661
                                        $maxShorthandDividers--;
2662
                                    }
2663
                                }
2664
                            }
2665
                        }
2666
                    }
2667
                }
2668
2669
                // if the value reduces to null from something else then
2670
                // the property should be discarded
2671
                if ($value[0] !== Type::T_NULL) {
2672
                    $value = $this->reduce($value);
2673
2674
                    if ($value[0] === Type::T_NULL || $value === static::$nullString) {
2675
                        break;
2676
                    }
2677
                }
2678
2679
                $compiledValue = $this->compileValue($value);
2680
2681
                // ignore empty value
2682
                if (\strlen($compiledValue)) {
2683
                    $line = $this->formatter->property(
2684
                        $compiledName,
2685
                        $compiledValue
2686
                    );
2687
                    $this->appendOutputLine($out, Type::T_ASSIGN, $line);
2688
                }
2689
                break;
2690
2691
            case Type::T_COMMENT:
2692
                if ($out->type === Type::T_ROOT) {
2693
                    $this->compileComment($child);
2694
                    break;
2695
                }
2696
2697
                $line = $this->compileCommentValue($child, true);
2698
                $this->appendOutputLine($out, Type::T_COMMENT, $line);
2699
                break;
2700
2701
            case Type::T_MIXIN:
2702
            case Type::T_FUNCTION:
2703
                list(, $block) = $child;
2704
                // the block need to be able to go up to it's parent env to resolve vars
2705
                $block->parentEnv = $this->getStoreEnv();
2706
                $this->set(static::$namespaces[$block->type] . $block->name, $block, true);
2707
                break;
2708
2709
            case Type::T_EXTEND:
2710
                foreach ($child[1] as $sel) {
2711
                    $sel = $this->replaceSelfSelector($sel);
2712
                    $results = $this->evalSelectors([$sel]);
2713
2714
                    foreach ($results as $result) {
2715
                        // only use the first one
2716
                        $result = current($result);
2717
                        $selectors = $out->selectors;
2718
2719
                        if (! $selectors && isset($child['selfParent'])) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $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...
2720
                            $selectors = $this->multiplySelectors($this->env, $child['selfParent']);
2721
                        }
2722
2723
                        $this->pushExtends($result, $selectors, $child);
2724
                    }
2725
                }
2726
                break;
2727
2728
            case Type::T_IF:
2729
                list(, $if) = $child;
2730
2731
                if ($this->isTruthy($this->reduce($if->cond, true))) {
2732
                    return $this->compileChildren($if->children, $out);
2733
                }
2734
2735
                foreach ($if->cases as $case) {
2736
                    if (
2737
                        $case->type === Type::T_ELSE ||
2738
                        $case->type === Type::T_ELSEIF && $this->isTruthy($this->reduce($case->cond))
2739
                    ) {
2740
                        return $this->compileChildren($case->children, $out);
2741
                    }
2742
                }
2743
                break;
2744
2745
            case Type::T_EACH:
2746
                list(, $each) = $child;
2747
2748
                $list = $this->coerceList($this->reduce($each->list), ',', true);
2749
2750
                $this->pushEnv();
2751
2752
                foreach ($list[2] as $item) {
2753
                    if (\count($each->vars) === 1) {
2754
                        $this->set($each->vars[0], $item, true);
2755
                    } else {
2756
                        list(,, $values) = $this->coerceList($item);
2757
2758
                        foreach ($each->vars as $i => $var) {
2759
                            $this->set($var, isset($values[$i]) ? $values[$i] : static::$null, true);
2760
                        }
2761
                    }
2762
2763
                    $ret = $this->compileChildren($each->children, $out);
2764
2765
                    if ($ret) {
2766
                        if ($ret[0] !== Type::T_CONTROL) {
2767
                            $store = $this->env->store;
2768
                            $this->popEnv();
2769
                            $this->backPropagateEnv($store, $each->vars);
2770
2771
                            return $ret;
2772
                        }
2773
2774
                        if ($ret[1]) {
2775
                            break;
2776
                        }
2777
                    }
2778
                }
2779
                $store = $this->env->store;
2780
                $this->popEnv();
2781
                $this->backPropagateEnv($store, $each->vars);
2782
2783
                break;
2784
2785
            case Type::T_WHILE:
2786
                list(, $while) = $child;
2787
2788
                while ($this->isTruthy($this->reduce($while->cond, true))) {
2789
                    $ret = $this->compileChildren($while->children, $out);
2790
2791
                    if ($ret) {
2792
                        if ($ret[0] !== Type::T_CONTROL) {
2793
                            return $ret;
2794
                        }
2795
2796
                        if ($ret[1]) {
2797
                            break;
2798
                        }
2799
                    }
2800
                }
2801
                break;
2802
2803
            case Type::T_FOR:
2804
                list(, $for) = $child;
2805
2806
                $start = $this->reduce($for->start, true);
2807
                $end   = $this->reduce($for->end, true);
2808
2809
                if (! $start instanceof Node\Number) {
2810
                    throw $this->error('%s is not a number', $start[0]);
2811
                }
2812
2813
                if (! $end instanceof Node\Number) {
2814
                    throw $this->error('%s is not a number', $end[0]);
2815
                }
2816
2817
                if (! ($start[2] == $end[2] || $end->unitless())) {
2818
                    throw $this->error('Incompatible units: "%s" && "%s".', $start->unitStr(), $end->unitStr());
2819
                }
2820
2821
                $unit  = $start[2];
2822
                $start = $start[1];
2823
                $end   = $end[1];
2824
2825
                $d = $start < $end ? 1 : -1;
2826
2827
                $this->pushEnv();
2828
2829
                for (;;) {
2830
                    if (
2831
                        (! $for->until && $start - $d == $end) ||
2832
                        ($for->until && $start == $end)
2833
                    ) {
2834
                        break;
2835
                    }
2836
2837
                    $this->set($for->var, new Node\Number($start, $unit));
2838
                    $start += $d;
2839
2840
                    $ret = $this->compileChildren($for->children, $out);
2841
2842
                    if ($ret) {
2843
                        if ($ret[0] !== Type::T_CONTROL) {
2844
                            $store = $this->env->store;
2845
                            $this->popEnv();
2846
                            $this->backPropagateEnv($store, [$for->var]);
2847
2848
                            return $ret;
2849
                        }
2850
2851
                        if ($ret[1]) {
2852
                            break;
2853
                        }
2854
                    }
2855
                }
2856
2857
                $store = $this->env->store;
2858
                $this->popEnv();
2859
                $this->backPropagateEnv($store, [$for->var]);
2860
2861
                break;
2862
2863
            case Type::T_BREAK:
2864
                return [Type::T_CONTROL, true];
2865
2866
            case Type::T_CONTINUE:
2867
                return [Type::T_CONTROL, false];
2868
2869
            case Type::T_RETURN:
2870
                return $this->reduce($child[1], true);
2871
2872
            case Type::T_NESTED_PROPERTY:
2873
                $this->compileNestedPropertiesBlock($child[1], $out);
2874
                break;
2875
2876
            case Type::T_INCLUDE:
2877
                // including a mixin
2878
                list(, $name, $argValues, $content, $argUsing) = $child;
2879
2880
                $mixin = $this->get(static::$namespaces['mixin'] . $name, false);
2881
2882
                if (! $mixin) {
2883
                    throw $this->error("Undefined mixin $name");
2884
                }
2885
2886
                $callingScope = $this->getStoreEnv();
2887
2888
                // push scope, apply args
2889
                $this->pushEnv();
2890
                $this->env->depth--;
2891
2892
                // Find the parent selectors in the env to be able to know what '&' refers to in the mixin
2893
                // and assign this fake parent to childs
2894
                $selfParent = null;
2895
2896
                if (isset($child['selfParent']) && isset($child['selfParent']->selectors)) {
2897
                    $selfParent = $child['selfParent'];
2898
                } else {
2899
                    $parentSelectors = $this->multiplySelectors($this->env);
2900
2901
                    if ($parentSelectors) {
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...
2902
                        $parent = new Block();
2903
                        $parent->selectors = $parentSelectors;
2904
2905
                        foreach ($mixin->children as $k => $child) {
0 ignored issues
show
introduced by
$child is overwriting one of the parameters of this function.
Loading history...
2906
                            if (isset($child[1]) && \is_object($child[1]) && $child[1] instanceof Block) {
2907
                                $mixin->children[$k][1]->parent = $parent;
2908
                            }
2909
                        }
2910
                    }
2911
                }
2912
2913
                // clone the stored content to not have its scope spoiled by a further call to the same mixin
2914
                // i.e., recursive @include of the same mixin
2915
                if (isset($content)) {
2916
                    $copyContent = clone $content;
2917
                    $copyContent->scope = clone $callingScope;
2918
2919
                    $this->setRaw(static::$namespaces['special'] . 'content', $copyContent, $this->env);
2920
                } else {
2921
                    $this->setRaw(static::$namespaces['special'] . 'content', null, $this->env);
2922
                }
2923
2924
                // save the "using" argument list for applying it to when "@content" is invoked
2925
                if (isset($argUsing)) {
2926
                    $this->setRaw(static::$namespaces['special'] . 'using', $argUsing, $this->env);
2927
                } else {
2928
                    $this->setRaw(static::$namespaces['special'] . 'using', null, $this->env);
2929
                }
2930
2931
                if (isset($mixin->args)) {
2932
                    $this->applyArguments($mixin->args, $argValues);
2933
                }
2934
2935
                $this->env->marker = 'mixin';
0 ignored issues
show
Bug introduced by
The property marker does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
2936
2937
                if (! empty($mixin->parentEnv)) {
2938
                    $this->env->declarationScopeParent = $mixin->parentEnv;
0 ignored issues
show
Bug introduced by
The property declarationScopeParent does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
2939
                } else {
2940
                    throw $this->error("@mixin $name() without parentEnv");
2941
                }
2942
2943
                $this->compileChildrenNoReturn($mixin->children, $out, $selfParent, $this->env->marker . ' ' . $name);
2944
2945
                $this->popEnv();
2946
                break;
2947
2948
            case Type::T_MIXIN_CONTENT:
2949
                $env        = isset($this->storeEnv) ? $this->storeEnv : $this->env;
2950
                $content    = $this->get(static::$namespaces['special'] . 'content', false, $env);
2951
                $argUsing   = $this->get(static::$namespaces['special'] . 'using', false, $env);
2952
                $argContent = $child[1];
2953
2954
                if (! $content) {
2955
                    break;
2956
                }
2957
2958
                $storeEnv = $this->storeEnv;
2959
                $varsUsing = [];
2960
2961
                if (isset($argUsing) && isset($argContent)) {
2962
                    // Get the arguments provided for the content with the names provided in the "using" argument list
2963
                    $this->storeEnv = null;
2964
                    $varsUsing = $this->applyArguments($argUsing, $argContent, false);
2965
                }
2966
2967
                // restore the scope from the @content
2968
                $this->storeEnv = $content->scope;
2969
2970
                // append the vars from using if any
2971
                foreach ($varsUsing as $name => $val) {
2972
                    $this->set($name, $val, true, $this->storeEnv);
2973
                }
2974
2975
                $this->compileChildrenNoReturn($content->children, $out);
2976
2977
                $this->storeEnv = $storeEnv;
2978
                break;
2979
2980
            case Type::T_DEBUG:
2981
                list(, $value) = $child;
2982
2983
                $fname = $this->sourceNames[$this->sourceIndex];
2984
                $line  = $this->sourceLine;
2985
                $value = $this->compileDebugValue($value);
2986
2987
                fwrite($this->stderr, "$fname:$line DEBUG: $value\n");
0 ignored issues
show
Bug introduced by
It seems like $this->stderr can also be of type boolean; however, parameter $handle of fwrite() does only seem to accept resource, 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

2987
                fwrite(/** @scrutinizer ignore-type */ $this->stderr, "$fname:$line DEBUG: $value\n");
Loading history...
2988
                break;
2989
2990
            case Type::T_WARN:
2991
                list(, $value) = $child;
2992
2993
                $fname = $this->sourceNames[$this->sourceIndex];
2994
                $line  = $this->sourceLine;
2995
                $value = $this->compileDebugValue($value);
2996
2997
                fwrite($this->stderr, "WARNING: $value\n         on line $line of $fname\n\n");
2998
                break;
2999
3000
            case Type::T_ERROR:
3001
                list(, $value) = $child;
3002
3003
                $fname = $this->sourceNames[$this->sourceIndex];
3004
                $line  = $this->sourceLine;
3005
                $value = $this->compileValue($this->reduce($value, true));
3006
3007
                throw $this->error("File $fname on line $line ERROR: $value\n");
3008
3009
            case Type::T_CONTROL:
3010
                throw $this->error('@break/@continue not permitted in this scope');
3011
3012
            default:
3013
                throw $this->error("unknown child type: $child[0]");
3014
        }
3015
    }
3016
3017
    /**
3018
     * Reduce expression to string
3019
     *
3020
     * @param array $exp
3021
     *
3022
     * @return array
3023
     */
3024
    protected function expToString($exp)
3025
    {
3026
        list(, $op, $left, $right, /* $inParens */, $whiteLeft, $whiteRight) = $exp;
3027
3028
        $content = [$this->reduce($left)];
3029
3030
        if ($whiteLeft) {
3031
            $content[] = ' ';
3032
        }
3033
3034
        $content[] = $op;
3035
3036
        if ($whiteRight) {
3037
            $content[] = ' ';
3038
        }
3039
3040
        $content[] = $this->reduce($right);
3041
3042
        return [Type::T_STRING, '', $content];
3043
    }
3044
3045
    /**
3046
     * Is truthy?
3047
     *
3048
     * @param array $value
3049
     *
3050
     * @return boolean
3051
     */
3052
    protected function isTruthy($value)
3053
    {
3054
        return $value !== static::$false && $value !== static::$null;
3055
    }
3056
3057
    /**
3058
     * Is the value a direct relationship combinator?
3059
     *
3060
     * @param string $value
3061
     *
3062
     * @return boolean
3063
     */
3064
    protected function isImmediateRelationshipCombinator($value)
3065
    {
3066
        return $value === '>' || $value === '+' || $value === '~';
3067
    }
3068
3069
    /**
3070
     * Should $value cause its operand to eval
3071
     *
3072
     * @param array $value
3073
     *
3074
     * @return boolean
3075
     */
3076
    protected function shouldEval($value)
3077
    {
3078
        switch ($value[0]) {
3079
            case Type::T_EXPRESSION:
3080
                if ($value[1] === '/') {
3081
                    return $this->shouldEval($value[2]) || $this->shouldEval($value[3]);
3082
                }
3083
3084
                // fall-thru
3085
            case Type::T_VARIABLE:
3086
            case Type::T_FUNCTION_CALL:
3087
                return true;
3088
        }
3089
3090
        return false;
3091
    }
3092
3093
    /**
3094
     * Reduce value
3095
     *
3096
     * @param array   $value
3097
     * @param boolean $inExp
3098
     *
3099
     * @return null|string|array|\ScssPhp\ScssPhp\Node\Number
3100
     */
3101
    protected function reduce($value, $inExp = false)
3102
    {
3103
        if (\is_null($value)) {
0 ignored issues
show
introduced by
The condition is_null($value) is always false.
Loading history...
3104
            return null;
3105
        }
3106
3107
        switch ($value[0]) {
3108
            case Type::T_EXPRESSION:
3109
                list(, $op, $left, $right, $inParens) = $value;
3110
3111
                $opName = isset(static::$operatorNames[$op]) ? static::$operatorNames[$op] : $op;
3112
                $inExp = $inExp || $this->shouldEval($left) || $this->shouldEval($right);
3113
3114
                $left = $this->reduce($left, true);
3115
3116
                if ($op !== 'and' && $op !== 'or') {
3117
                    $right = $this->reduce($right, true);
3118
                }
3119
3120
                // special case: looks like css shorthand
3121
                if (
3122
                    $opName == 'div' && ! $inParens && ! $inExp && isset($right[2]) &&
3123
                    (($right[0] !== Type::T_NUMBER && $right[2] != '') ||
3124
                    ($right[0] === Type::T_NUMBER && ! $right->unitless()))
3125
                ) {
3126
                    return $this->expToString($value);
3127
                }
3128
3129
                $left  = $this->coerceForExpression($left);
3130
                $right = $this->coerceForExpression($right);
3131
                $ltype = $left[0];
3132
                $rtype = $right[0];
3133
3134
                $ucOpName = ucfirst($opName);
3135
                $ucLType  = ucfirst($ltype);
3136
                $ucRType  = ucfirst($rtype);
3137
3138
                // this tries:
3139
                // 1. op[op name][left type][right type]
3140
                // 2. op[left type][right type] (passing the op as first arg
3141
                // 3. op[op name]
3142
                $fn = "op${ucOpName}${ucLType}${ucRType}";
3143
3144
                if (
3145
                    \is_callable([$this, $fn]) ||
3146
                    (($fn = "op${ucLType}${ucRType}") &&
3147
                        \is_callable([$this, $fn]) &&
3148
                        $passOp = true) ||
3149
                    (($fn = "op${ucOpName}") &&
3150
                        \is_callable([$this, $fn]) &&
3151
                        $genOp = true)
3152
                ) {
3153
                    $coerceUnit = false;
3154
3155
                    if (
3156
                        ! isset($genOp) &&
3157
                        $left[0] === Type::T_NUMBER && $right[0] === Type::T_NUMBER
3158
                    ) {
3159
                        $coerceUnit = true;
3160
3161
                        switch ($opName) {
3162
                            case 'mul':
3163
                                $targetUnit = $left[2];
3164
3165
                                foreach ($right[2] as $unit => $exp) {
3166
                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) + $exp;
3167
                                }
3168
                                break;
3169
3170
                            case 'div':
3171
                                $targetUnit = $left[2];
3172
3173
                                foreach ($right[2] as $unit => $exp) {
3174
                                    $targetUnit[$unit] = (isset($targetUnit[$unit]) ? $targetUnit[$unit] : 0) - $exp;
3175
                                }
3176
                                break;
3177
3178
                            case 'mod':
3179
                                $targetUnit = $left[2];
3180
                                break;
3181
3182
                            default:
3183
                                $targetUnit = $left->unitless() ? $right[2] : $left[2];
3184
                        }
3185
3186
                        $baseUnitLeft = $left->isNormalizable();
3187
                        $baseUnitRight = $right->isNormalizable();
3188
3189
                        if ($baseUnitLeft && $baseUnitRight && $baseUnitLeft === $baseUnitRight) {
3190
                            $left = $left->normalize();
3191
                            $right = $right->normalize();
3192
                        } elseif ($coerceUnit) {
0 ignored issues
show
introduced by
The condition $coerceUnit is always true.
Loading history...
3193
                            $left = new Node\Number($left[1], []);
3194
                        }
3195
                    }
3196
3197
                    $shouldEval = $inParens || $inExp;
3198
3199
                    if (isset($passOp)) {
3200
                        $out = $this->$fn($op, $left, $right, $shouldEval);
3201
                    } else {
3202
                        $out = $this->$fn($left, $right, $shouldEval);
3203
                    }
3204
3205
                    if (isset($out)) {
3206
                        if ($coerceUnit && $out[0] === Type::T_NUMBER) {
3207
                            $out = $out->coerce($targetUnit);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $targetUnit does not seem to be defined for all execution paths leading up to this point.
Loading history...
3208
                        }
3209
3210
                        return $out;
3211
                    }
3212
                }
3213
3214
                return $this->expToString($value);
3215
3216
            case Type::T_UNARY:
3217
                list(, $op, $exp, $inParens) = $value;
3218
3219
                $inExp = $inExp || $this->shouldEval($exp);
3220
                $exp = $this->reduce($exp);
3221
3222
                if ($exp[0] === Type::T_NUMBER) {
3223
                    switch ($op) {
3224
                        case '+':
3225
                            return new Node\Number($exp[1], $exp[2]);
3226
3227
                        case '-':
3228
                            return new Node\Number(-$exp[1], $exp[2]);
3229
                    }
3230
                }
3231
3232
                if ($op === 'not') {
3233
                    if ($inExp || $inParens) {
3234
                        if ($exp === static::$false || $exp === static::$null) {
3235
                            return static::$true;
3236
                        }
3237
3238
                        return static::$false;
3239
                    }
3240
3241
                    $op = $op . ' ';
3242
                }
3243
3244
                return [Type::T_STRING, '', [$op, $exp]];
3245
3246
            case Type::T_VARIABLE:
3247
                return $this->reduce($this->get($value[1]));
3248
3249
            case Type::T_LIST:
3250
                foreach ($value[2] as &$item) {
3251
                    $item = $this->reduce($item);
3252
                }
3253
3254
                return $value;
3255
3256
            case Type::T_MAP:
3257
                foreach ($value[1] as &$item) {
3258
                    $item = $this->reduce($item);
3259
                }
3260
3261
                foreach ($value[2] as &$item) {
3262
                    $item = $this->reduce($item);
3263
                }
3264
3265
                return $value;
3266
3267
            case Type::T_STRING:
3268
                foreach ($value[2] as &$item) {
3269
                    if (\is_array($item) || $item instanceof \ArrayAccess) {
3270
                        $item = $this->reduce($item);
0 ignored issues
show
Bug introduced by
It seems like $item can also be of type ArrayAccess; however, parameter $value of ScssPhp\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

3270
                        $item = $this->reduce(/** @scrutinizer ignore-type */ $item);
Loading history...
3271
                    }
3272
                }
3273
3274
                return $value;
3275
3276
            case Type::T_INTERPOLATE:
3277
                $value[1] = $this->reduce($value[1]);
3278
3279
                if ($inExp) {
3280
                    return $value[1];
3281
                }
3282
3283
                return $value;
3284
3285
            case Type::T_FUNCTION_CALL:
3286
                return $this->fncall($value[1], $value[2]);
3287
3288
            case Type::T_SELF:
3289
                $selfParent = ! empty($this->env->block->selfParent) ? $this->env->block->selfParent : null;
3290
                $selfSelector = $this->multiplySelectors($this->env, $selfParent);
3291
                $selfSelector = $this->collapseSelectors($selfSelector, true);
3292
3293
                return $selfSelector;
3294
3295
            default:
3296
                return $value;
3297
        }
3298
    }
3299
3300
    /**
3301
     * Function caller
3302
     *
3303
     * @param string $name
3304
     * @param array  $argValues
3305
     *
3306
     * @return array|null
3307
     */
3308
    protected function fncall($functionReference, $argValues)
3309
    {
3310
        // a string means this is a static hard reference coming from the parsing
3311
        if (is_string($functionReference)) {
3312
            $name = $functionReference;
3313
3314
            $functionReference = $this->getFunctionReference($name);
3315
            if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3316
                $functionReference = [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
3317
            }
3318
        }
3319
3320
        // a function type means we just want a plain css function call
3321
        if ($functionReference[0] === Type::T_FUNCTION) {
3322
            // for CSS functions, simply flatten the arguments into a list
3323
            $listArgs = [];
3324
3325
            foreach ((array) $argValues as $arg) {
3326
                if (empty($arg[0]) || count($argValues) === 1) {
3327
                    $listArgs[] = $this->reduce($this->stringifyFncallArgs($arg[1]));
3328
                }
3329
            }
3330
3331
            return [Type::T_FUNCTION, $functionReference[1], [Type::T_LIST, ',', $listArgs]];
3332
        }
3333
3334
        if ($functionReference === static::$null || $functionReference[0] !== Type::T_FUNCTION_REFERENCE) {
3335
            return static::$defaultValue;
3336
        }
3337
3338
3339
        switch ($functionReference[1]) {
3340
            // SCSS @function
3341
            case 'scss':
3342
                return $this->callScssFunction($functionReference[3], $argValues);
3343
3344
            // native PHP functions
3345
            case 'user':
3346
            case 'native':
3347
                list(,,$name, $fn, $prototype) = $functionReference;
3348
                $returnValue = $this->callNativeFunction($name, $fn, $prototype, $argValues);
3349
3350
                if (! isset($returnValue)) {
3351
                    return $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $argValues);
3352
                }
3353
3354
                return $returnValue;
3355
3356
            default:
3357
                return static::$defaultValue;
3358
        }
3359
    }
3360
3361
    /**
3362
     * Reformat fncall arguments to proper css function output
3363
     * @param $arg
3364
     * @return array|\ArrayAccess|Node\Number|string|null
3365
     */
3366
    protected function stringifyFncallArgs($arg)
3367
    {
3368
3369
        switch ($arg[0]) {
3370
            case Type::T_LIST:
3371
                foreach ($arg[2] as $k => $v) {
3372
                    $arg[2][$k] = $this->stringifyFncallArgs($v);
3373
                }
3374
                break;
3375
3376
            case Type::T_EXPRESSION:
3377
                if ($arg[1] === '/') {
3378
                    $arg[2] = $this->stringifyFncallArgs($arg[2]);
3379
                    $arg[3] = $this->stringifyFncallArgs($arg[3]);
3380
                    $arg[5] = $arg[6] = false; // no space around /
3381
                    $arg = $this->expToString($arg);
3382
                }
3383
                break;
3384
3385
            case Type::T_FUNCTION_CALL:
3386
                $name = $arg[1];
3387
3388
                if (in_array($name, ['max', 'min', 'calc'])) {
3389
                    $args = $arg[2];
3390
                    $arg = $this->fncall([Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]], $args);
3391
                }
3392
                break;
3393
        }
3394
3395
        return $arg;
3396
    }
3397
3398
    /**
3399
     * Find a function reference
3400
     * @param string $name
3401
     * @param bool $safeCopy
3402
     * @return array
3403
     */
3404
    protected function getFunctionReference($name, $safeCopy = false)
3405
    {
3406
        // SCSS @function
3407
        if ($func = $this->get(static::$namespaces['function'] . $name, false)) {
3408
            if ($safeCopy) {
3409
                $func = clone $func;
3410
            }
3411
3412
            return [Type::T_FUNCTION_REFERENCE, 'scss', $name, $func];
3413
        }
3414
3415
        // native PHP functions
3416
3417
        // try to find a native lib function
3418
        $normalizedName = $this->normalizeName($name);
3419
        $libName = null;
0 ignored issues
show
Unused Code introduced by
The assignment to $libName is dead and can be removed.
Loading history...
3420
3421
        if (isset($this->userFunctions[$normalizedName])) {
3422
            // see if we can find a user function
3423
            list($f, $prototype) = $this->userFunctions[$normalizedName];
3424
3425
            return [Type::T_FUNCTION_REFERENCE, 'user', $name, $f, $prototype];
3426
        }
3427
3428
        if (($f = $this->getBuiltinFunction($normalizedName)) && \is_callable($f)) {
3429
            $libName   = $f[1];
3430
            $prototype = isset(static::$$libName) ? static::$$libName : null;
3431
3432
            return [Type::T_FUNCTION_REFERENCE, 'native', $name, $f, $prototype];
3433
        }
3434
3435
        return static::$null;
3436
    }
3437
3438
3439
    /**
3440
     * Normalize name
3441
     *
3442
     * @param string $name
3443
     *
3444
     * @return string
3445
     */
3446
    protected function normalizeName($name)
3447
    {
3448
        return str_replace('-', '_', $name);
3449
    }
3450
3451
    /**
3452
     * Normalize value
3453
     *
3454
     * @param array $value
3455
     *
3456
     * @return array
3457
     */
3458
    public function normalizeValue($value)
3459
    {
3460
        $value = $this->coerceForExpression($this->reduce($value));
3461
3462
        switch ($value[0]) {
3463
            case Type::T_LIST:
3464
                $value = $this->extractInterpolation($value);
3465
3466
                if ($value[0] !== Type::T_LIST) {
3467
                    return [Type::T_KEYWORD, $this->compileValue($value)];
3468
                }
3469
3470
                foreach ($value[2] as $key => $item) {
3471
                    $value[2][$key] = $this->normalizeValue($item);
3472
                }
3473
3474
                if (! empty($value['enclosing'])) {
3475
                    unset($value['enclosing']);
3476
                }
3477
3478
                return $value;
3479
3480
            case Type::T_STRING:
3481
                return [$value[0], '"', [$this->compileStringContent($value)]];
3482
3483
            case Type::T_NUMBER:
3484
                return $value->normalize();
3485
3486
            case Type::T_INTERPOLATE:
3487
                return [Type::T_KEYWORD, $this->compileValue($value)];
3488
3489
            default:
3490
                return $value;
3491
        }
3492
    }
3493
3494
    /**
3495
     * Add numbers
3496
     *
3497
     * @param array $left
3498
     * @param array $right
3499
     *
3500
     * @return \ScssPhp\ScssPhp\Node\Number
3501
     */
3502
    protected function opAddNumberNumber($left, $right)
3503
    {
3504
        return new Node\Number($left[1] + $right[1], $left[2]);
3505
    }
3506
3507
    /**
3508
     * Multiply numbers
3509
     *
3510
     * @param array $left
3511
     * @param array $right
3512
     *
3513
     * @return \ScssPhp\ScssPhp\Node\Number
3514
     */
3515
    protected function opMulNumberNumber($left, $right)
3516
    {
3517
        return new Node\Number($left[1] * $right[1], $left[2]);
3518
    }
3519
3520
    /**
3521
     * Subtract numbers
3522
     *
3523
     * @param array $left
3524
     * @param array $right
3525
     *
3526
     * @return \ScssPhp\ScssPhp\Node\Number
3527
     */
3528
    protected function opSubNumberNumber($left, $right)
3529
    {
3530
        return new Node\Number($left[1] - $right[1], $left[2]);
3531
    }
3532
3533
    /**
3534
     * Divide numbers
3535
     *
3536
     * @param array $left
3537
     * @param array $right
3538
     *
3539
     * @return array|\ScssPhp\ScssPhp\Node\Number
3540
     */
3541
    protected function opDivNumberNumber($left, $right)
3542
    {
3543
        if ($right[1] == 0) {
3544
            return ($left[1] == 0) ? static::$NaN : static::$Infinity;
3545
        }
3546
3547
        return new Node\Number($left[1] / $right[1], $left[2]);
3548
    }
3549
3550
    /**
3551
     * Mod numbers
3552
     *
3553
     * @param array $left
3554
     * @param array $right
3555
     *
3556
     * @return \ScssPhp\ScssPhp\Node\Number
3557
     */
3558
    protected function opModNumberNumber($left, $right)
3559
    {
3560
        if ($right[1] == 0) {
3561
            return static::$NaN;
0 ignored issues
show
Bug Best Practice introduced by
The expression return static::NaN returns the type array which is incompatible with the documented return type ScssPhp\ScssPhp\Node\Number.
Loading history...
3562
        }
3563
3564
        return new Node\Number($left[1] % $right[1], $left[2]);
3565
    }
3566
3567
    /**
3568
     * Add strings
3569
     *
3570
     * @param array $left
3571
     * @param array $right
3572
     *
3573
     * @return array|null
3574
     */
3575
    protected function opAdd($left, $right)
3576
    {
3577
        if ($strLeft = $this->coerceString($left)) {
3578
            if ($right[0] === Type::T_STRING) {
3579
                $right[1] = '';
3580
            }
3581
3582
            $strLeft[2][] = $right;
3583
3584
            return $strLeft;
3585
        }
3586
3587
        if ($strRight = $this->coerceString($right)) {
3588
            if ($left[0] === Type::T_STRING) {
3589
                $left[1] = '';
3590
            }
3591
3592
            array_unshift($strRight[2], $left);
3593
3594
            return $strRight;
3595
        }
3596
3597
        return null;
3598
    }
3599
3600
    /**
3601
     * Boolean and
3602
     *
3603
     * @param array   $left
3604
     * @param array   $right
3605
     * @param boolean $shouldEval
3606
     *
3607
     * @return array|null
3608
     */
3609
    protected function opAnd($left, $right, $shouldEval)
3610
    {
3611
        $truthy = ($left === static::$null || $right === static::$null) ||
3612
                  ($left === static::$false || $left === static::$true) &&
3613
                  ($right === static::$false || $right === static::$true);
3614
3615
        if (! $shouldEval) {
3616
            if (! $truthy) {
3617
                return null;
3618
            }
3619
        }
3620
3621
        if ($left !== static::$false && $left !== static::$null) {
3622
            return $this->reduce($right, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->reduce($right, true) also could return the type ScssPhp\ScssPhp\Node\Number|string which is incompatible with the documented return type array|null.
Loading history...
3623
        }
3624
3625
        return $left;
3626
    }
3627
3628
    /**
3629
     * Boolean or
3630
     *
3631
     * @param array   $left
3632
     * @param array   $right
3633
     * @param boolean $shouldEval
3634
     *
3635
     * @return array|null
3636
     */
3637
    protected function opOr($left, $right, $shouldEval)
3638
    {
3639
        $truthy = ($left === static::$null || $right === static::$null) ||
3640
                  ($left === static::$false || $left === static::$true) &&
3641
                  ($right === static::$false || $right === static::$true);
3642
3643
        if (! $shouldEval) {
3644
            if (! $truthy) {
3645
                return null;
3646
            }
3647
        }
3648
3649
        if ($left !== static::$false && $left !== static::$null) {
3650
            return $left;
3651
        }
3652
3653
        return $this->reduce($right, true);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->reduce($right, true) also could return the type ScssPhp\ScssPhp\Node\Number|string which is incompatible with the documented return type array|null.
Loading history...
3654
    }
3655
3656
    /**
3657
     * Compare colors
3658
     *
3659
     * @param string $op
3660
     * @param array  $left
3661
     * @param array  $right
3662
     *
3663
     * @return array
3664
     */
3665
    protected function opColorColor($op, $left, $right)
3666
    {
3667
        $out = [Type::T_COLOR];
3668
3669
        foreach ([1, 2, 3] as $i) {
3670
            $lval = isset($left[$i]) ? $left[$i] : 0;
3671
            $rval = isset($right[$i]) ? $right[$i] : 0;
3672
3673
            switch ($op) {
3674
                case '+':
3675
                    $out[] = $lval + $rval;
3676
                    break;
3677
3678
                case '-':
3679
                    $out[] = $lval - $rval;
3680
                    break;
3681
3682
                case '*':
3683
                    $out[] = $lval * $rval;
3684
                    break;
3685
3686
                case '%':
3687
                    if ($rval == 0) {
3688
                        throw $this->error("color: Can't take modulo by zero");
3689
                    }
3690
3691
                    $out[] = $lval % $rval;
3692
                    break;
3693
3694
                case '/':
3695
                    if ($rval == 0) {
3696
                        throw $this->error("color: Can't divide by zero");
3697
                    }
3698
3699
                    $out[] = (int) ($lval / $rval);
3700
                    break;
3701
3702
                case '==':
3703
                    return $this->opEq($left, $right);
3704
3705
                case '!=':
3706
                    return $this->opNeq($left, $right);
3707
3708
                default:
3709
                    throw $this->error("color: unknown op $op");
3710
            }
3711
        }
3712
3713
        if (isset($left[4])) {
3714
            $out[4] = $left[4];
3715
        } elseif (isset($right[4])) {
3716
            $out[4] = $right[4];
3717
        }
3718
3719
        return $this->fixColor($out);
3720
    }
3721
3722
    /**
3723
     * Compare color and number
3724
     *
3725
     * @param string $op
3726
     * @param array  $left
3727
     * @param array  $right
3728
     *
3729
     * @return array
3730
     */
3731
    protected function opColorNumber($op, $left, $right)
3732
    {
3733
        $value = $right[1];
3734
3735
        return $this->opColorColor(
3736
            $op,
3737
            $left,
3738
            [Type::T_COLOR, $value, $value, $value]
3739
        );
3740
    }
3741
3742
    /**
3743
     * Compare number and color
3744
     *
3745
     * @param string $op
3746
     * @param array  $left
3747
     * @param array  $right
3748
     *
3749
     * @return array
3750
     */
3751
    protected function opNumberColor($op, $left, $right)
3752
    {
3753
        $value = $left[1];
3754
3755
        return $this->opColorColor(
3756
            $op,
3757
            [Type::T_COLOR, $value, $value, $value],
3758
            $right
3759
        );
3760
    }
3761
3762
    /**
3763
     * Compare number1 == number2
3764
     *
3765
     * @param array $left
3766
     * @param array $right
3767
     *
3768
     * @return array
3769
     */
3770
    protected function opEq($left, $right)
3771
    {
3772
        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3773
            $lStr[1] = '';
3774
            $rStr[1] = '';
3775
3776
            $left = $this->compileValue($lStr);
3777
            $right = $this->compileValue($rStr);
3778
        }
3779
3780
        return $this->toBool($left === $right);
3781
    }
3782
3783
    /**
3784
     * Compare number1 != number2
3785
     *
3786
     * @param array $left
3787
     * @param array $right
3788
     *
3789
     * @return array
3790
     */
3791
    protected function opNeq($left, $right)
3792
    {
3793
        if (($lStr = $this->coerceString($left)) && ($rStr = $this->coerceString($right))) {
3794
            $lStr[1] = '';
3795
            $rStr[1] = '';
3796
3797
            $left = $this->compileValue($lStr);
3798
            $right = $this->compileValue($rStr);
3799
        }
3800
3801
        return $this->toBool($left !== $right);
3802
    }
3803
3804
    /**
3805
     * Compare number1 >= number2
3806
     *
3807
     * @param array $left
3808
     * @param array $right
3809
     *
3810
     * @return array
3811
     */
3812
    protected function opGteNumberNumber($left, $right)
3813
    {
3814
        return $this->toBool($left[1] >= $right[1]);
3815
    }
3816
3817
    /**
3818
     * Compare number1 > number2
3819
     *
3820
     * @param array $left
3821
     * @param array $right
3822
     *
3823
     * @return array
3824
     */
3825
    protected function opGtNumberNumber($left, $right)
3826
    {
3827
        return $this->toBool($left[1] > $right[1]);
3828
    }
3829
3830
    /**
3831
     * Compare number1 <= number2
3832
     *
3833
     * @param array $left
3834
     * @param array $right
3835
     *
3836
     * @return array
3837
     */
3838
    protected function opLteNumberNumber($left, $right)
3839
    {
3840
        return $this->toBool($left[1] <= $right[1]);
3841
    }
3842
3843
    /**
3844
     * Compare number1 < number2
3845
     *
3846
     * @param array $left
3847
     * @param array $right
3848
     *
3849
     * @return array
3850
     */
3851
    protected function opLtNumberNumber($left, $right)
3852
    {
3853
        return $this->toBool($left[1] < $right[1]);
3854
    }
3855
3856
    /**
3857
     * Three-way comparison, aka spaceship operator
3858
     *
3859
     * @param array $left
3860
     * @param array $right
3861
     *
3862
     * @return \ScssPhp\ScssPhp\Node\Number
3863
     */
3864
    protected function opCmpNumberNumber($left, $right)
3865
    {
3866
        $n = $left[1] - $right[1];
3867
3868
        return new Node\Number($n ? $n / abs($n) : 0, '');
3869
    }
3870
3871
    /**
3872
     * Cast to boolean
3873
     *
3874
     * @api
3875
     *
3876
     * @param mixed $thing
3877
     *
3878
     * @return array
3879
     */
3880
    public function toBool($thing)
3881
    {
3882
        return $thing ? static::$true : static::$false;
3883
    }
3884
3885
    /**
3886
     * Compiles a primitive value into a CSS property value.
3887
     *
3888
     * Values in scssphp are typed by being wrapped in arrays, their format is
3889
     * typically:
3890
     *
3891
     *     array(type, contents [, additional_contents]*)
3892
     *
3893
     * The input is expected to be reduced. This function will not work on
3894
     * things like expressions and variables.
3895
     *
3896
     * @api
3897
     *
3898
     * @param array $value
3899
     *
3900
     * @return string|array
3901
     */
3902
    public function compileValue($value)
3903
    {
3904
        $value = $this->reduce($value);
3905
3906
        switch ($value[0]) {
3907
            case Type::T_KEYWORD:
3908
                return $value[1];
3909
3910
            case Type::T_COLOR:
3911
                // [1] - red component (either number for a %)
3912
                // [2] - green component
3913
                // [3] - blue component
3914
                // [4] - optional alpha component
3915
                list(, $r, $g, $b) = $value;
3916
3917
                $r = $this->compileRGBAValue($r);
3918
                $g = $this->compileRGBAValue($g);
3919
                $b = $this->compileRGBAValue($b);
3920
3921
                if (\count($value) === 5) {
3922
                    $alpha = $this->compileRGBAValue($value[4], true);
3923
3924
                    if (! is_numeric($alpha) || $alpha < 1) {
3925
                        $colorName = Colors::RGBaToColorName($r, $g, $b, $alpha);
3926
3927
                        if (! \is_null($colorName)) {
3928
                            return $colorName;
3929
                        }
3930
3931
                        if (is_numeric($alpha)) {
3932
                            $a = new Node\Number($alpha, '');
3933
                        } else {
3934
                            $a = $alpha;
3935
                        }
3936
3937
                        return 'rgba(' . $r . ', ' . $g . ', ' . $b . ', ' . $a . ')';
3938
                    }
3939
                }
3940
3941
                if (! is_numeric($r) || ! is_numeric($g) || ! is_numeric($b)) {
3942
                    return 'rgb(' . $r . ', ' . $g . ', ' . $b . ')';
3943
                }
3944
3945
                $colorName = Colors::RGBaToColorName($r, $g, $b);
3946
3947
                if (! \is_null($colorName)) {
3948
                    return $colorName;
3949
                }
3950
3951
                $h = sprintf('#%02x%02x%02x', $r, $g, $b);
3952
3953
                // Converting hex color to short notation (e.g. #003399 to #039)
3954
                if ($h[1] === $h[2] && $h[3] === $h[4] && $h[5] === $h[6]) {
3955
                    $h = '#' . $h[1] . $h[3] . $h[5];
3956
                }
3957
3958
                return $h;
3959
3960
            case Type::T_NUMBER:
3961
                return $value->output($this);
3962
3963
            case Type::T_STRING:
3964
                $content = $this->compileStringContent($value);
3965
3966
                if ($value[1]) {
3967
                    // force double quote as string quote for the output in certain cases
3968
                    if (
3969
                        $value[1] === "'" &&
3970
                        strpos($content, '"') === false &&
3971
                        strpbrk($content, '{}') !== false
3972
                    ) {
3973
                        $value[1] = '"';
3974
                    }
3975
                    $content = str_replace(
3976
                        array('\\a', "\n", "\f" , '\\'  , "\r" , $value[1]),
3977
                        array("\r" , ' ' , '\\f', '\\\\', '\\a', '\\' . $value[1]),
3978
                        $content
3979
                    );
3980
                }
3981
3982
                return $value[1] . $content . $value[1];
3983
3984
            case Type::T_FUNCTION:
3985
                $args = ! empty($value[2]) ? $this->compileValue($value[2]) : '';
3986
3987
                return "$value[1]($args)";
3988
3989
            case Type::T_FUNCTION_REFERENCE:
3990
                $name = ! empty($value[2]) ? $value[2] : '';
3991
3992
                return "get-function(\"$name\")";
3993
3994
            case Type::T_LIST:
3995
                $value = $this->extractInterpolation($value);
3996
3997
                if ($value[0] !== Type::T_LIST) {
3998
                    return $this->compileValue($value);
3999
                }
4000
4001
                list(, $delim, $items) = $value;
4002
                $pre = $post = '';
4003
4004
                if (! empty($value['enclosing'])) {
4005
                    switch ($value['enclosing']) {
4006
                        case 'parent':
4007
                            //$pre = '(';
4008
                            //$post = ')';
4009
                            break;
4010
                        case 'forced_parent':
4011
                            $pre = '(';
4012
                            $post = ')';
4013
                            break;
4014
                        case 'bracket':
4015
                        case 'forced_bracket':
4016
                            $pre = '[';
4017
                            $post = ']';
4018
                            break;
4019
                    }
4020
                }
4021
4022
                $prefix_value = '';
4023
4024
                if ($delim !== ' ') {
4025
                    $prefix_value = ' ';
4026
                }
4027
4028
                $filtered = [];
4029
4030
                foreach ($items as $item) {
4031
                    if ($item[0] === Type::T_NULL) {
4032
                        continue;
4033
                    }
4034
4035
                    $compiled = $this->compileValue($item);
4036
4037
                    if ($prefix_value && \strlen($compiled)) {
4038
                        $compiled = $prefix_value . $compiled;
4039
                    }
4040
4041
                    $filtered[] = $compiled;
4042
                }
4043
4044
                return $pre . substr(implode("$delim", $filtered), \strlen($prefix_value)) . $post;
4045
4046
            case Type::T_MAP:
4047
                $keys     = $value[1];
4048
                $values   = $value[2];
4049
                $filtered = [];
4050
4051
                for ($i = 0, $s = \count($keys); $i < $s; $i++) {
4052
                    $filtered[$this->compileValue($keys[$i])] = $this->compileValue($values[$i]);
4053
                }
4054
4055
                array_walk($filtered, function (&$value, $key) {
4056
                    $value = $key . ': ' . $value;
4057
                });
4058
4059
                return '(' . implode(', ', $filtered) . ')';
4060
4061
            case Type::T_INTERPOLATED:
4062
                // node created by extractInterpolation
4063
                list(, $interpolate, $left, $right) = $value;
4064
                list(,, $whiteLeft, $whiteRight) = $interpolate;
4065
4066
                $delim = $left[1];
4067
4068
                if ($delim && $delim !== ' ' && ! $whiteLeft) {
4069
                    $delim .= ' ';
4070
                }
4071
4072
                $left = \count($left[2]) > 0
4073
                    ?  $this->compileValue($left) . $delim . $whiteLeft
4074
                    : '';
4075
4076
                $delim = $right[1];
4077
4078
                if ($delim && $delim !== ' ') {
4079
                    $delim .= ' ';
4080
                }
4081
4082
                $right = \count($right[2]) > 0 ?
4083
                    $whiteRight . $delim . $this->compileValue($right) : '';
4084
4085
                return $left . $this->compileValue($interpolate) . $right;
4086
4087
            case Type::T_INTERPOLATE:
4088
                // strip quotes if it's a string
4089
                $reduced = $this->reduce($value[1]);
4090
4091
                switch ($reduced[0]) {
4092
                    case Type::T_LIST:
4093
                        $reduced = $this->extractInterpolation($reduced);
4094
4095
                        if ($reduced[0] !== Type::T_LIST) {
4096
                            break;
4097
                        }
4098
4099
                        list(, $delim, $items) = $reduced;
4100
4101
                        if ($delim !== ' ') {
4102
                            $delim .= ' ';
4103
                        }
4104
4105
                        $filtered = [];
4106
4107
                        foreach ($items as $item) {
4108
                            if ($item[0] === Type::T_NULL) {
4109
                                continue;
4110
                            }
4111
4112
                            $temp = $this->compileValue([Type::T_KEYWORD, $item]);
4113
4114
                            if ($temp[0] === Type::T_STRING) {
4115
                                $filtered[] = $this->compileStringContent($temp);
4116
                            } elseif ($temp[0] === Type::T_KEYWORD) {
4117
                                $filtered[] = $temp[1];
4118
                            } else {
4119
                                $filtered[] = $this->compileValue($temp);
4120
                            }
4121
                        }
4122
4123
                        $reduced = [Type::T_KEYWORD, implode("$delim", $filtered)];
4124
                        break;
4125
4126
                    case Type::T_STRING:
4127
                        $reduced = [Type::T_KEYWORD, $this->compileStringContent($reduced)];
4128
                        break;
4129
4130
                    case Type::T_NULL:
4131
                        $reduced = [Type::T_KEYWORD, ''];
4132
                }
4133
4134
                return $this->compileValue($reduced);
4135
4136
            case Type::T_NULL:
4137
                return 'null';
4138
4139
            case Type::T_COMMENT:
4140
                return $this->compileCommentValue($value);
4141
4142
            default:
4143
                throw $this->error('unknown value type: ' . json_encode($value));
4144
        }
4145
    }
4146
4147
    /**
4148
     * @param array $value
4149
     *
4150
     * @return array|string
4151
     */
4152
    protected function compileDebugValue($value)
4153
    {
4154
        $value = $this->reduce($value, true);
4155
4156
        switch ($value[0]) {
4157
            case Type::T_STRING:
4158
                return $this->compileStringContent($value);
4159
4160
            default:
4161
                return $this->compileValue($value);
4162
        }
4163
    }
4164
4165
    /**
4166
     * Flatten list
4167
     *
4168
     * @param array $list
4169
     *
4170
     * @return string
4171
     */
4172
    protected function flattenList($list)
4173
    {
4174
        return $this->compileValue($list);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->compileValue($list) also could return the type array which is incompatible with the documented return type string.
Loading history...
4175
    }
4176
4177
    /**
4178
     * Compile string content
4179
     *
4180
     * @param array $string
4181
     *
4182
     * @return string
4183
     */
4184
    protected function compileStringContent($string)
4185
    {
4186
        $parts = [];
4187
4188
        foreach ($string[2] as $part) {
4189
            if (\is_array($part) || $part instanceof \ArrayAccess) {
4190
                $parts[] = $this->compileValue($part);
0 ignored issues
show
Bug introduced by
It seems like $part can also be of type ArrayAccess; however, parameter $value of ScssPhp\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

4190
                $parts[] = $this->compileValue(/** @scrutinizer ignore-type */ $part);
Loading history...
4191
            } else {
4192
                $parts[] = $part;
4193
            }
4194
        }
4195
4196
        return implode($parts);
4197
    }
4198
4199
    /**
4200
     * Extract interpolation; it doesn't need to be recursive, compileValue will handle that
4201
     *
4202
     * @param array $list
4203
     *
4204
     * @return array
4205
     */
4206
    protected function extractInterpolation($list)
4207
    {
4208
        $items = $list[2];
4209
4210
        foreach ($items as $i => $item) {
4211
            if ($item[0] === Type::T_INTERPOLATE) {
4212
                $before = [Type::T_LIST, $list[1], \array_slice($items, 0, $i)];
4213
                $after  = [Type::T_LIST, $list[1], \array_slice($items, $i + 1)];
4214
4215
                return [Type::T_INTERPOLATED, $item, $before, $after];
4216
            }
4217
        }
4218
4219
        return $list;
4220
    }
4221
4222
    /**
4223
     * Find the final set of selectors
4224
     *
4225
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4226
     * @param \ScssPhp\ScssPhp\Block                $selfParent
4227
     *
4228
     * @return array
4229
     */
4230
    protected function multiplySelectors(Environment $env, $selfParent = null)
4231
    {
4232
        $envs            = $this->compactEnv($env);
4233
        $selectors       = [];
4234
        $parentSelectors = [[]];
4235
4236
        $selfParentSelectors = null;
4237
4238
        if (! \is_null($selfParent) && $selfParent->selectors) {
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...
4239
            $selfParentSelectors = $this->evalSelectors($selfParent->selectors);
4240
        }
4241
4242
        while ($env = array_pop($envs)) {
4243
            if (empty($env->selectors)) {
4244
                continue;
4245
            }
4246
4247
            $selectors = $env->selectors;
4248
4249
            do {
4250
                $stillHasSelf  = false;
4251
                $prevSelectors = $selectors;
4252
                $selectors     = [];
4253
4254
                foreach ($parentSelectors as $parent) {
4255
                    foreach ($prevSelectors as $selector) {
4256
                        if ($selfParentSelectors) {
4257
                            foreach ($selfParentSelectors as $selfParent) {
4258
                                // if no '&' in the selector, each call will give same result, only add once
4259
                                $s = $this->joinSelectors($parent, $selector, $stillHasSelf, $selfParent);
4260
                                $selectors[serialize($s)] = $s;
4261
                            }
4262
                        } else {
4263
                            $s = $this->joinSelectors($parent, $selector, $stillHasSelf);
4264
                            $selectors[serialize($s)] = $s;
4265
                        }
4266
                    }
4267
                }
4268
            } while ($stillHasSelf);
4269
4270
            $parentSelectors = $selectors;
4271
        }
4272
4273
        $selectors = array_values($selectors);
4274
4275
        // case we are just starting a at-root : nothing to multiply but parentSelectors
4276
        if (! $selectors && $selfParentSelectors) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $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...
4277
            $selectors = $selfParentSelectors;
4278
        }
4279
4280
        return $selectors;
4281
    }
4282
4283
    /**
4284
     * Join selectors; looks for & to replace, or append parent before child
4285
     *
4286
     * @param array   $parent
4287
     * @param array   $child
4288
     * @param boolean $stillHasSelf
4289
     * @param array   $selfParentSelectors
4290
4291
     * @return array
4292
     */
4293
    protected function joinSelectors($parent, $child, &$stillHasSelf, $selfParentSelectors = null)
4294
    {
4295
        $setSelf = false;
4296
        $out = [];
4297
4298
        foreach ($child as $part) {
4299
            $newPart = [];
4300
4301
            foreach ($part as $p) {
4302
                // only replace & once and should be recalled to be able to make combinations
4303
                if ($p === static::$selfSelector && $setSelf) {
4304
                    $stillHasSelf = true;
4305
                }
4306
4307
                if ($p === static::$selfSelector && ! $setSelf) {
4308
                    $setSelf = true;
4309
4310
                    if (\is_null($selfParentSelectors)) {
4311
                        $selfParentSelectors = $parent;
4312
                    }
4313
4314
                    foreach ($selfParentSelectors as $i => $parentPart) {
0 ignored issues
show
Bug introduced by
The expression $selfParentSelectors of type null is not traversable.
Loading history...
4315
                        if ($i > 0) {
4316
                            $out[] = $newPart;
4317
                            $newPart = [];
4318
                        }
4319
4320
                        foreach ($parentPart as $pp) {
4321
                            if (\is_array($pp)) {
4322
                                $flatten = [];
4323
4324
                                array_walk_recursive($pp, function ($a) use (&$flatten) {
4325
                                    $flatten[] = $a;
4326
                                });
4327
4328
                                $pp = implode($flatten);
4329
                            }
4330
4331
                            $newPart[] = $pp;
4332
                        }
4333
                    }
4334
                } else {
4335
                    $newPart[] = $p;
4336
                }
4337
            }
4338
4339
            $out[] = $newPart;
4340
        }
4341
4342
        return $setSelf ? $out : array_merge($parent, $child);
4343
    }
4344
4345
    /**
4346
     * Multiply media
4347
     *
4348
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4349
     * @param array                                 $childQueries
4350
     *
4351
     * @return array
4352
     */
4353
    protected function multiplyMedia(Environment $env = null, $childQueries = null)
4354
    {
4355
        if (
4356
            ! isset($env) ||
4357
            ! empty($env->block->type) && $env->block->type !== Type::T_MEDIA
4358
        ) {
4359
            return $childQueries;
4360
        }
4361
4362
        // plain old block, skip
4363
        if (empty($env->block->type)) {
4364
            return $this->multiplyMedia($env->parent, $childQueries);
4365
        }
4366
4367
        $parentQueries = isset($env->block->queryList)
4368
            ? $env->block->queryList
4369
            : [[[Type::T_MEDIA_VALUE, $env->block->value]]];
0 ignored issues
show
Bug introduced by
The property value does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
4370
4371
        $store = [$this->env, $this->storeEnv];
4372
4373
        $this->env      = $env;
4374
        $this->storeEnv = null;
4375
        $parentQueries  = $this->evaluateMediaQuery($parentQueries);
4376
4377
        list($this->env, $this->storeEnv) = $store;
4378
4379
        if (\is_null($childQueries)) {
4380
            $childQueries = $parentQueries;
4381
        } else {
4382
            $originalQueries = $childQueries;
4383
            $childQueries = [];
4384
4385
            foreach ($parentQueries as $parentQuery) {
4386
                foreach ($originalQueries as $childQuery) {
4387
                    $childQueries[] = array_merge(
4388
                        $parentQuery,
4389
                        [[Type::T_MEDIA_TYPE, [Type::T_KEYWORD, 'all']]],
4390
                        $childQuery
4391
                    );
4392
                }
4393
            }
4394
        }
4395
4396
        return $this->multiplyMedia($env->parent, $childQueries);
4397
    }
4398
4399
    /**
4400
     * Convert env linked list to stack
4401
     *
4402
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4403
     *
4404
     * @return array
4405
     */
4406
    protected function compactEnv(Environment $env)
4407
    {
4408
        for ($envs = []; $env; $env = $env->parent) {
4409
            $envs[] = $env;
4410
        }
4411
4412
        return $envs;
4413
    }
4414
4415
    /**
4416
     * Convert env stack to singly linked list
4417
     *
4418
     * @param array $envs
4419
     *
4420
     * @return \ScssPhp\ScssPhp\Compiler\Environment
4421
     */
4422
    protected function extractEnv($envs)
4423
    {
4424
        for ($env = null; $e = array_pop($envs);) {
4425
            $e->parent = $env;
4426
            $env = $e;
4427
        }
4428
4429
        return $env;
4430
    }
4431
4432
    /**
4433
     * Push environment
4434
     *
4435
     * @param \ScssPhp\ScssPhp\Block $block
4436
     *
4437
     * @return \ScssPhp\ScssPhp\Compiler\Environment
4438
     */
4439
    protected function pushEnv(Block $block = null)
4440
    {
4441
        $env = new Environment();
4442
        $env->parent = $this->env;
4443
        $env->parentStore = $this->storeEnv;
0 ignored issues
show
Bug introduced by
The property parentStore does not exist on ScssPhp\ScssPhp\Compiler\Environment. Did you mean parent?
Loading history...
4444
        $env->store  = [];
4445
        $env->block  = $block;
4446
        $env->depth  = isset($this->env->depth) ? $this->env->depth + 1 : 0;
4447
4448
        $this->env = $env;
4449
        $this->storeEnv = null;
4450
4451
        return $env;
4452
    }
4453
4454
    /**
4455
     * Pop environment
4456
     */
4457
    protected function popEnv()
4458
    {
4459
        $this->storeEnv = $this->env->parentStore;
0 ignored issues
show
Bug introduced by
The property parentStore does not exist on ScssPhp\ScssPhp\Compiler\Environment. Did you mean parent?
Loading history...
4460
        $this->env = $this->env->parent;
4461
    }
4462
4463
    /**
4464
     * Propagate vars from a just poped Env (used in @each and @for)
4465
     *
4466
     * @param array      $store
4467
     * @param null|array $excludedVars
4468
     */
4469
    protected function backPropagateEnv($store, $excludedVars = null)
4470
    {
4471
        foreach ($store as $key => $value) {
4472
            if (empty($excludedVars) || ! \in_array($key, $excludedVars)) {
4473
                $this->set($key, $value, true);
4474
            }
4475
        }
4476
    }
4477
4478
    /**
4479
     * Get store environment
4480
     *
4481
     * @return \ScssPhp\ScssPhp\Compiler\Environment
4482
     */
4483
    protected function getStoreEnv()
4484
    {
4485
        return isset($this->storeEnv) ? $this->storeEnv : $this->env;
4486
    }
4487
4488
    /**
4489
     * Set variable
4490
     *
4491
     * @param string                                $name
4492
     * @param mixed                                 $value
4493
     * @param boolean                               $shadow
4494
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4495
     * @param mixed                                 $valueUnreduced
4496
     */
4497
    protected function set($name, $value, $shadow = false, Environment $env = null, $valueUnreduced = null)
4498
    {
4499
        $name = $this->normalizeName($name);
4500
4501
        if (! isset($env)) {
4502
            $env = $this->getStoreEnv();
4503
        }
4504
4505
        if ($shadow) {
4506
            $this->setRaw($name, $value, $env, $valueUnreduced);
4507
        } else {
4508
            $this->setExisting($name, $value, $env, $valueUnreduced);
4509
        }
4510
    }
4511
4512
    /**
4513
     * Set existing variable
4514
     *
4515
     * @param string                                $name
4516
     * @param mixed                                 $value
4517
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4518
     * @param mixed                                 $valueUnreduced
4519
     */
4520
    protected function setExisting($name, $value, Environment $env, $valueUnreduced = null)
4521
    {
4522
        $storeEnv = $env;
4523
        $specialContentKey = static::$namespaces['special'] . 'content';
4524
4525
        $hasNamespace = $name[0] === '^' || $name[0] === '@' || $name[0] === '%';
4526
4527
        $maxDepth = 10000;
4528
4529
        for (;;) {
4530
            if ($maxDepth-- <= 0) {
4531
                break;
4532
            }
4533
4534
            if (\array_key_exists($name, $env->store)) {
4535
                break;
4536
            }
4537
4538
            if (! $hasNamespace && isset($env->marker)) {
4539
                if (! empty($env->store[$specialContentKey])) {
4540
                    $env = $env->store[$specialContentKey]->scope;
4541
                    continue;
4542
                }
4543
4544
                if (! empty($env->declarationScopeParent)) {
4545
                    $env = $env->declarationScopeParent;
0 ignored issues
show
Bug introduced by
The property declarationScopeParent does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
4546
                    continue;
4547
                } else {
4548
                    $env = $storeEnv;
4549
                    break;
4550
                }
4551
            }
4552
4553
            if (isset($env->parentStore)) {
0 ignored issues
show
Bug introduced by
The property parentStore does not exist on ScssPhp\ScssPhp\Compiler\Environment. Did you mean parent?
Loading history...
4554
                $env = $env->parentStore;
4555
            } elseif (isset($env->parent)) {
4556
                $env = $env->parent;
4557
            } else {
4558
                $env = $storeEnv;
4559
                break;
4560
            }
4561
        }
4562
4563
        $env->store[$name] = $value;
4564
4565
        if ($valueUnreduced) {
4566
            $env->storeUnreduced[$name] = $valueUnreduced;
4567
        }
4568
    }
4569
4570
    /**
4571
     * Set raw variable
4572
     *
4573
     * @param string                                $name
4574
     * @param mixed                                 $value
4575
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4576
     * @param mixed                                 $valueUnreduced
4577
     */
4578
    protected function setRaw($name, $value, Environment $env, $valueUnreduced = null)
4579
    {
4580
        $env->store[$name] = $value;
4581
4582
        if ($valueUnreduced) {
4583
            $env->storeUnreduced[$name] = $valueUnreduced;
4584
        }
4585
    }
4586
4587
    /**
4588
     * Get variable
4589
     *
4590
     * @api
4591
     *
4592
     * @param string                                $name
4593
     * @param boolean                               $shouldThrow
4594
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4595
     * @param boolean                               $unreduced
4596
     *
4597
     * @return mixed|null
4598
     */
4599
    public function get($name, $shouldThrow = true, Environment $env = null, $unreduced = false)
4600
    {
4601
        $normalizedName = $this->normalizeName($name);
4602
        $specialContentKey = static::$namespaces['special'] . 'content';
4603
4604
        if (! isset($env)) {
4605
            $env = $this->getStoreEnv();
4606
        }
4607
4608
        $hasNamespace = $normalizedName[0] === '^' || $normalizedName[0] === '@' || $normalizedName[0] === '%';
4609
4610
        $maxDepth = 10000;
4611
4612
        for (;;) {
4613
            if ($maxDepth-- <= 0) {
4614
                break;
4615
            }
4616
4617
            if (\array_key_exists($normalizedName, $env->store)) {
4618
                if ($unreduced && isset($env->storeUnreduced[$normalizedName])) {
4619
                    return $env->storeUnreduced[$normalizedName];
4620
                }
4621
4622
                return $env->store[$normalizedName];
4623
            }
4624
4625
            if (! $hasNamespace && isset($env->marker)) {
4626
                if (! empty($env->store[$specialContentKey])) {
4627
                    $env = $env->store[$specialContentKey]->scope;
4628
                    continue;
4629
                }
4630
4631
                if (! empty($env->declarationScopeParent)) {
4632
                    $env = $env->declarationScopeParent;
0 ignored issues
show
Bug introduced by
The property declarationScopeParent does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
4633
                } else {
4634
                    $env = $this->rootEnv;
4635
                }
4636
                continue;
4637
            }
4638
4639
            if (isset($env->parentStore)) {
0 ignored issues
show
Bug introduced by
The property parentStore does not exist on ScssPhp\ScssPhp\Compiler\Environment. Did you mean parent?
Loading history...
4640
                $env = $env->parentStore;
4641
            } elseif (isset($env->parent)) {
4642
                $env = $env->parent;
4643
            } else {
4644
                break;
4645
            }
4646
        }
4647
4648
        if ($shouldThrow) {
4649
            throw $this->error("Undefined variable \$$name" . ($maxDepth <= 0 ? ' (infinite recursion)' : ''));
4650
        }
4651
4652
        // found nothing
4653
        return null;
4654
    }
4655
4656
    /**
4657
     * Has variable?
4658
     *
4659
     * @param string                                $name
4660
     * @param \ScssPhp\ScssPhp\Compiler\Environment $env
4661
     *
4662
     * @return boolean
4663
     */
4664
    protected function has($name, Environment $env = null)
4665
    {
4666
        return ! \is_null($this->get($name, false, $env));
4667
    }
4668
4669
    /**
4670
     * Inject variables
4671
     *
4672
     * @param array $args
4673
     */
4674
    protected function injectVariables(array $args)
4675
    {
4676
        if (empty($args)) {
4677
            return;
4678
        }
4679
4680
        $parser = $this->parserFactory(__METHOD__);
4681
4682
        foreach ($args as $name => $strValue) {
4683
            if ($name[0] === '$') {
4684
                $name = substr($name, 1);
4685
            }
4686
4687
            if (! $parser->parseValue($strValue, $value)) {
4688
                $value = $this->coerceValue($strValue);
4689
            }
4690
4691
            $this->set($name, $value);
4692
        }
4693
    }
4694
4695
    /**
4696
     * Set variables
4697
     *
4698
     * @api
4699
     *
4700
     * @param array $variables
4701
     */
4702
    public function setVariables(array $variables)
4703
    {
4704
        $this->registeredVars = array_merge($this->registeredVars, $variables);
4705
    }
4706
4707
    /**
4708
     * Unset variable
4709
     *
4710
     * @api
4711
     *
4712
     * @param string $name
4713
     */
4714
    public function unsetVariable($name)
4715
    {
4716
        unset($this->registeredVars[$name]);
4717
    }
4718
4719
    /**
4720
     * Returns list of variables
4721
     *
4722
     * @api
4723
     *
4724
     * @return array
4725
     */
4726
    public function getVariables()
4727
    {
4728
        return $this->registeredVars;
4729
    }
4730
4731
    /**
4732
     * Adds to list of parsed files
4733
     *
4734
     * @api
4735
     *
4736
     * @param string $path
4737
     */
4738
    public function addParsedFile($path)
4739
    {
4740
        if (isset($path) && is_file($path)) {
4741
            $this->parsedFiles[realpath($path)] = filemtime($path);
4742
        }
4743
    }
4744
4745
    /**
4746
     * Returns list of parsed files
4747
     *
4748
     * @api
4749
     *
4750
     * @return array
4751
     */
4752
    public function getParsedFiles()
4753
    {
4754
        return $this->parsedFiles;
4755
    }
4756
4757
    /**
4758
     * Add import path
4759
     *
4760
     * @api
4761
     *
4762
     * @param string|callable $path
4763
     */
4764
    public function addImportPath($path)
4765
    {
4766
        if (! \in_array($path, $this->importPaths)) {
4767
            $this->importPaths[] = $path;
4768
        }
4769
    }
4770
4771
    /**
4772
     * Set import paths
4773
     *
4774
     * @api
4775
     *
4776
     * @param string|array $path
4777
     */
4778
    public function setImportPaths($path)
4779
    {
4780
        $this->importPaths = (array) $path;
4781
    }
4782
4783
    /**
4784
     * Set number precision
4785
     *
4786
     * @api
4787
     *
4788
     * @param integer $numberPrecision
4789
     *
4790
     * @deprecated The number precision is not configurable anymore. The default is enough for all browsers.
4791
     */
4792
    public function setNumberPrecision($numberPrecision)
4793
    {
4794
        @trigger_error('The number precision is not configurable anymore. '
4795
            . 'The default is enough for all browsers.', E_USER_DEPRECATED);
4796
    }
4797
4798
    /**
4799
     * Set formatter
4800
     *
4801
     * @api
4802
     *
4803
     * @param string $formatterName
4804
     */
4805
    public function setFormatter($formatterName)
4806
    {
4807
        $this->formatter = $formatterName;
4808
    }
4809
4810
    /**
4811
     * Set line number style
4812
     *
4813
     * @api
4814
     *
4815
     * @param string $lineNumberStyle
4816
     */
4817
    public function setLineNumberStyle($lineNumberStyle)
4818
    {
4819
        $this->lineNumberStyle = $lineNumberStyle;
4820
    }
4821
4822
    /**
4823
     * Enable/disable source maps
4824
     *
4825
     * @api
4826
     *
4827
     * @param integer $sourceMap
4828
     */
4829
    public function setSourceMap($sourceMap)
4830
    {
4831
        $this->sourceMap = $sourceMap;
4832
    }
4833
4834
    /**
4835
     * Set source map options
4836
     *
4837
     * @api
4838
     *
4839
     * @param array $sourceMapOptions
4840
     */
4841
    public function setSourceMapOptions($sourceMapOptions)
4842
    {
4843
        $this->sourceMapOptions = $sourceMapOptions;
4844
    }
4845
4846
    /**
4847
     * Register function
4848
     *
4849
     * @api
4850
     *
4851
     * @param string   $name
4852
     * @param callable $func
4853
     * @param array    $prototype
4854
     */
4855
    public function registerFunction($name, $func, $prototype = null)
4856
    {
4857
        $this->userFunctions[$this->normalizeName($name)] = [$func, $prototype];
4858
    }
4859
4860
    /**
4861
     * Unregister function
4862
     *
4863
     * @api
4864
     *
4865
     * @param string $name
4866
     */
4867
    public function unregisterFunction($name)
4868
    {
4869
        unset($this->userFunctions[$this->normalizeName($name)]);
4870
    }
4871
4872
    /**
4873
     * Add feature
4874
     *
4875
     * @api
4876
     *
4877
     * @param string $name
4878
     */
4879
    public function addFeature($name)
4880
    {
4881
        $this->registeredFeatures[$name] = true;
4882
    }
4883
4884
    /**
4885
     * Import file
4886
     *
4887
     * @param string                                 $path
4888
     * @param \ScssPhp\ScssPhp\Formatter\OutputBlock $out
4889
     */
4890
    protected function importFile($path, OutputBlock $out)
4891
    {
4892
        $this->pushCallStack('import ' . $path);
4893
        // see if tree is cached
4894
        $realPath = realpath($path);
4895
4896
        if (isset($this->importCache[$realPath])) {
4897
            $this->handleImportLoop($realPath);
4898
4899
            $tree = $this->importCache[$realPath];
4900
        } else {
4901
            $code   = file_get_contents($path);
4902
            $parser = $this->parserFactory($path);
4903
            $tree   = $parser->parse($code);
4904
4905
            $this->importCache[$realPath] = $tree;
4906
        }
4907
4908
        $pi = pathinfo($path);
4909
4910
        array_unshift($this->importPaths, $pi['dirname']);
4911
        $this->compileChildrenNoReturn($tree->children, $out);
4912
        array_shift($this->importPaths);
4913
        $this->popCallStack();
4914
    }
4915
4916
    /**
4917
     * Return the file path for an import url if it exists
4918
     *
4919
     * @api
4920
     *
4921
     * @param string $url
4922
     *
4923
     * @return string|null
4924
     */
4925
    public function findImport($url)
4926
    {
4927
        $urls = [];
4928
4929
        $hasExtension = preg_match('/[.]s?css$/', $url);
4930
4931
        // for "normal" scss imports (ignore vanilla css and external requests)
4932
        if (! preg_match('~\.css$|^https?://|^//~', $url)) {
4933
            $isPartial = (strpos(basename($url), '_') === 0);
4934
4935
            // try both normal and the _partial filename
4936
            $urls = [$url . ($hasExtension ? '' : '.scss')];
4937
4938
            if (! $isPartial) {
4939
                $urls[] = preg_replace('~[^/]+$~', '_\0', $url) . ($hasExtension ? '' : '.scss');
4940
            }
4941
4942
            if (! $hasExtension) {
4943
                $urls[] = "$url/index.scss";
4944
                $urls[] = "$url/_index.scss";
4945
                // allow to find a plain css file, *if* no scss or partial scss is found
4946
                $urls[] .= $url . '.css';
4947
            }
4948
        }
4949
4950
        foreach ($this->importPaths as $dir) {
4951
            if (\is_string($dir)) {
4952
                // check urls for normal import paths
4953
                foreach ($urls as $full) {
4954
                    $separator = (
4955
                        ! empty($dir) &&
4956
                        substr($dir, -1) !== '/' &&
4957
                        substr($full, 0, 1) !== '/'
4958
                    ) ? '/' : '';
4959
                    $full = $dir . $separator . $full;
4960
4961
                    if (is_file($file = $full)) {
4962
                        return $file;
4963
                    }
4964
                }
4965
            } elseif (\is_callable($dir)) {
4966
                // check custom callback for import path
4967
                $file = \call_user_func($dir, $url);
4968
4969
                if (! \is_null($file)) {
4970
                    return $file;
4971
                }
4972
            }
4973
        }
4974
4975
        if ($urls) {
4976
            if (! $hasExtension || preg_match('/[.]scss$/', $url)) {
4977
                throw $this->error("`$url` file not found for @import");
4978
            }
4979
        }
4980
4981
        return null;
4982
    }
4983
4984
    /**
4985
     * Set encoding
4986
     *
4987
     * @api
4988
     *
4989
     * @param string $encoding
4990
     */
4991
    public function setEncoding($encoding)
4992
    {
4993
        $this->encoding = $encoding;
4994
    }
4995
4996
    /**
4997
     * Ignore errors?
4998
     *
4999
     * @api
5000
     *
5001
     * @param boolean $ignoreErrors
5002
     *
5003
     * @return \ScssPhp\ScssPhp\Compiler
5004
     *
5005
     * @deprecated Ignoring Sass errors is not longer supported.
5006
     */
5007
    public function setIgnoreErrors($ignoreErrors)
5008
    {
5009
        @trigger_error('Ignoring Sass errors is not longer supported.', E_USER_DEPRECATED);
5010
5011
        return $this;
5012
    }
5013
5014
    /**
5015
     * Get source position
5016
     *
5017
     * @api
5018
     *
5019
     * @return array
5020
     */
5021
    public function getSourcePosition()
5022
    {
5023
        $sourceFile = isset($this->sourceNames[$this->sourceIndex]) ? $this->sourceNames[$this->sourceIndex] : '';
5024
5025
        return [$sourceFile, $this->sourceLine, $this->sourceColumn];
5026
    }
5027
5028
    /**
5029
     * Throw error (exception)
5030
     *
5031
     * @api
5032
     *
5033
     * @param string $msg Message with optional sprintf()-style vararg parameters
5034
     *
5035
     * @throws \ScssPhp\ScssPhp\Exception\CompilerException
5036
     *
5037
     * @deprecated use "error" and throw the exception in the caller instead.
5038
     */
5039
    public function throwError($msg)
5040
    {
5041
        @trigger_error(
5042
            'The method "throwError" is deprecated. Use "error" and throw the exception in the caller instead',
5043
            E_USER_DEPRECATED
5044
        );
5045
5046
        throw $this->error(...func_get_args());
5047
    }
5048
5049
    /**
5050
     * Build an error (exception)
5051
     *
5052
     * @api
5053
     *
5054
     * @param string $msg Message with optional sprintf()-style vararg parameters
5055
     *
5056
     * @return CompilerException
5057
     */
5058
    public function error($msg, ...$args)
5059
    {
5060
        if ($args) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $args 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...
5061
            $msg = sprintf($msg, ...$args);
5062
        }
5063
5064
        if (! $this->ignoreCallStackMessage) {
5065
            $line   = $this->sourceLine;
5066
            $column = $this->sourceColumn;
5067
5068
            $loc = isset($this->sourceNames[$this->sourceIndex])
5069
                ? $this->sourceNames[$this->sourceIndex] . " on line $line, at column $column"
5070
                : "line: $line, column: $column";
5071
5072
            $msg = "$msg: $loc";
5073
5074
            $callStackMsg = $this->callStackMessage();
5075
5076
            if ($callStackMsg) {
5077
                $msg .= "\nCall Stack:\n" . $callStackMsg;
5078
            }
5079
        }
5080
5081
        return new CompilerException($msg);
5082
    }
5083
5084
    /**
5085
     * @param string $functionName
5086
     * @param array $ExpectedArgs
5087
     * @param int $nbActual
5088
     * @return CompilerException
5089
     */
5090
    public function errorArgsNumber($functionName, $ExpectedArgs, $nbActual)
5091
    {
5092
        $nbExpected = \count($ExpectedArgs);
5093
5094
        if ($nbActual > $nbExpected) {
5095
            return $this->error(
5096
                'Error: Only %d arguments allowed in %s(), but %d were passed.',
5097
                $nbExpected,
5098
                $functionName,
5099
                $nbActual
5100
            );
5101
        } else {
5102
            $missing = [];
5103
5104
            while (count($ExpectedArgs) && count($ExpectedArgs) > $nbActual) {
5105
                array_unshift($missing, array_pop($ExpectedArgs));
5106
            }
5107
5108
            return $this->error(
5109
                'Error: %s() argument%s %s missing.',
5110
                $functionName,
5111
                count($missing) > 1 ? 's' : '',
5112
                implode(', ', $missing)
5113
            );
5114
        }
5115
    }
5116
5117
    /**
5118
     * Beautify call stack for output
5119
     *
5120
     * @param boolean $all
5121
     * @param null    $limit
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...
5122
     *
5123
     * @return string
5124
     */
5125
    protected function callStackMessage($all = false, $limit = null)
5126
    {
5127
        $callStackMsg = [];
5128
        $ncall = 0;
5129
5130
        if ($this->callStack) {
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...
5131
            foreach (array_reverse($this->callStack) as $call) {
5132
                if ($all || (isset($call['n']) && $call['n'])) {
5133
                    $msg = '#' . $ncall++ . ' ' . $call['n'] . ' ';
5134
                    $msg .= (isset($this->sourceNames[$call[Parser::SOURCE_INDEX]])
5135
                          ? $this->sourceNames[$call[Parser::SOURCE_INDEX]]
5136
                          : '(unknown file)');
5137
                    $msg .= ' on line ' . $call[Parser::SOURCE_LINE];
5138
5139
                    $callStackMsg[] = $msg;
5140
5141
                    if (! \is_null($limit) && $ncall > $limit) {
5142
                        break;
5143
                    }
5144
                }
5145
            }
5146
        }
5147
5148
        return implode("\n", $callStackMsg);
5149
    }
5150
5151
    /**
5152
     * Handle import loop
5153
     *
5154
     * @param string $name
5155
     *
5156
     * @throws \Exception
5157
     */
5158
    protected function handleImportLoop($name)
5159
    {
5160
        for ($env = $this->env; $env; $env = $env->parent) {
5161
            if (! $env->block) {
5162
                continue;
5163
            }
5164
5165
            $file = $this->sourceNames[$env->block->sourceIndex];
5166
5167
            if (realpath($file) === $name) {
5168
                throw $this->error('An @import loop has been found: %s imports %s', $file, basename($file));
5169
            }
5170
        }
5171
    }
5172
5173
    /**
5174
     * Call SCSS @function
5175
     *
5176
     * @param Object $func
5177
     * @param array  $argValues
5178
     *
5179
     * @return array $returnValue
5180
     */
5181
    protected function callScssFunction($func, $argValues)
5182
    {
5183
        if (! $func) {
0 ignored issues
show
introduced by
$func is of type object, thus it always evaluated to true.
Loading history...
5184
            return static::$defaultValue;
5185
        }
5186
        $name = $func->name;
5187
5188
        $this->pushEnv();
5189
5190
        // set the args
5191
        if (isset($func->args)) {
5192
            $this->applyArguments($func->args, $argValues);
5193
        }
5194
5195
        // throw away lines and children
5196
        $tmp = new OutputBlock();
5197
        $tmp->lines    = [];
5198
        $tmp->children = [];
5199
5200
        $this->env->marker = 'function';
0 ignored issues
show
Bug introduced by
The property marker does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
5201
5202
        if (! empty($func->parentEnv)) {
5203
            $this->env->declarationScopeParent = $func->parentEnv;
0 ignored issues
show
Bug introduced by
The property declarationScopeParent does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
5204
        } else {
5205
            throw $this->error("@function $name() without parentEnv");
5206
        }
5207
5208
        $ret = $this->compileChildren($func->children, $tmp, $this->env->marker . ' ' . $name);
5209
5210
        $this->popEnv();
5211
5212
        return ! isset($ret) ? static::$defaultValue : $ret;
5213
    }
5214
5215
    /**
5216
     * Call built-in and registered (PHP) functions
5217
     *
5218
     * @param string $name
5219
     * @param string|array $function
5220
     * @param array  $prototype
5221
     * @param array  $args
5222
     *
5223
     * @return array
5224
     */
5225
    protected function callNativeFunction($name, $function, $prototype, $args)
5226
    {
5227
        $libName = (is_array($function) ? end($function) : null);
5228
        $sorted_kwargs = $this->sortNativeFunctionArgs($libName, $prototype, $args);
5229
5230
        if (\is_null($sorted_kwargs)) {
5231
            return null;
5232
        }
5233
        @list($sorted, $kwargs) = $sorted_kwargs;
5234
5235
        if ($name !== 'if' && $name !== 'call') {
5236
            $inExp = true;
5237
5238
            if ($name === 'join') {
5239
                $inExp = false;
5240
            }
5241
5242
            foreach ($sorted as &$val) {
5243
                $val = $this->reduce($val, $inExp);
5244
            }
5245
        }
5246
5247
        $returnValue = \call_user_func($function, $sorted, $kwargs);
5248
5249
        if (! isset($returnValue)) {
5250
            return null;
5251
        }
5252
5253
        return $this->coerceValue($returnValue);
0 ignored issues
show
Bug Best Practice introduced by
The expression return $this->coerceValue($returnValue) returns the type ScssPhp\ScssPhp\Node\Number which is incompatible with the documented return type array.
Loading history...
5254
    }
5255
5256
    /**
5257
     * Get built-in function
5258
     *
5259
     * @param string $name Normalized name
5260
     *
5261
     * @return array
5262
     */
5263
    protected function getBuiltinFunction($name)
5264
    {
5265
        $libName = 'lib' . preg_replace_callback(
5266
            '/_(.)/',
5267
            function ($m) {
5268
                return ucfirst($m[1]);
5269
            },
5270
            ucfirst($name)
5271
        );
5272
5273
        return [$this, $libName];
5274
    }
5275
5276
    /**
5277
     * Sorts keyword arguments
5278
     *
5279
     * @param string $functionName
5280
     * @param array  $prototypes
5281
     * @param array  $args
5282
     *
5283
     * @return array|null
5284
     */
5285
    protected function sortNativeFunctionArgs($functionName, $prototypes, $args)
5286
    {
5287
        static $parser = null;
5288
5289
        if (! isset($prototypes)) {
5290
            $keyArgs = [];
5291
            $posArgs = [];
5292
5293
            if (\is_array($args) && \count($args) && \end($args) === static::$null) {
5294
                array_pop($args);
5295
            }
5296
5297
            // separate positional and keyword arguments
5298
            foreach ($args as $arg) {
5299
                list($key, $value) = $arg;
5300
5301
                if (empty($key) or empty($key[1])) {
5302
                    $posArgs[] = empty($arg[2]) ? $value : $arg;
5303
                } else {
5304
                    $keyArgs[$key[1]] = $value;
5305
                }
5306
            }
5307
5308
            return [$posArgs, $keyArgs];
5309
        }
5310
5311
        // specific cases ?
5312
        if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
5313
            // notation 100 127 255 / 0 is in fact a simple list of 4 values
5314
            foreach ($args as $k => $arg) {
5315
                if ($arg[1][0] === Type::T_LIST && \count($arg[1][2]) === 3) {
5316
                    $last = end($arg[1][2]);
5317
5318
                    if ($last[0] === Type::T_EXPRESSION && $last[1] === '/') {
5319
                        array_pop($arg[1][2]);
5320
                        $arg[1][2][] = $last[2];
5321
                        $arg[1][2][] = $last[3];
5322
                        $args[$k] = $arg;
5323
                    }
5324
                }
5325
            }
5326
        }
5327
5328
        $finalArgs = [];
5329
5330
        if (! \is_array(reset($prototypes))) {
5331
            $prototypes = [$prototypes];
5332
        }
5333
5334
        $keyArgs = [];
5335
5336
        // trying each prototypes
5337
        $prototypeHasMatch = false;
5338
        $exceptionMessage = '';
5339
5340
        foreach ($prototypes as $prototype) {
5341
            $argDef = [];
5342
5343
            foreach ($prototype as $i => $p) {
5344
                $default = null;
5345
                $p       = explode(':', $p, 2);
5346
                $name    = array_shift($p);
5347
5348
                if (\count($p)) {
5349
                    $p = trim(reset($p));
5350
5351
                    if ($p === 'null') {
5352
                        // differentiate this null from the static::$null
5353
                        $default = [Type::T_KEYWORD, 'null'];
5354
                    } else {
5355
                        if (\is_null($parser)) {
5356
                            $parser = $this->parserFactory(__METHOD__);
5357
                        }
5358
5359
                        $parser->parseValue($p, $default);
5360
                    }
5361
                }
5362
5363
                $isVariable = false;
5364
5365
                if (substr($name, -3) === '...') {
5366
                    $isVariable = true;
5367
                    $name = substr($name, 0, -3);
5368
                }
5369
5370
                $argDef[] = [$name, $default, $isVariable];
5371
            }
5372
5373
            $ignoreCallStackMessage = $this->ignoreCallStackMessage;
5374
            $this->ignoreCallStackMessage = true;
5375
5376
            try {
5377
                if (\count($args) > \count($argDef)) {
5378
                    $lastDef = end($argDef);
5379
5380
                    // check that last arg is not a ...
5381
                    if (empty($lastDef[2])) {
5382
                        throw $this->errorArgsNumber($functionName, $argDef, \count($args));
5383
                    }
5384
                }
5385
                $vars = $this->applyArguments($argDef, $args, false, false);
5386
5387
                // ensure all args are populated
5388
                foreach ($prototype as $i => $p) {
5389
                    $name = explode(':', $p)[0];
0 ignored issues
show
Unused Code introduced by
The assignment to $name is dead and can be removed.
Loading history...
5390
5391
                    if (! isset($finalArgs[$i])) {
5392
                        $finalArgs[$i] = null;
5393
                    }
5394
                }
5395
5396
                // apply positional args
5397
                foreach (array_values($vars) as $i => $val) {
5398
                    $finalArgs[$i] = $val;
5399
                }
5400
5401
                $keyArgs = array_merge($keyArgs, $vars);
5402
                $prototypeHasMatch = true;
5403
5404
                // overwrite positional args with keyword args
5405
                foreach ($prototype as $i => $p) {
5406
                    $name = explode(':', $p)[0];
5407
5408
                    if (isset($keyArgs[$name])) {
5409
                        $finalArgs[$i] = $keyArgs[$name];
5410
                    }
5411
5412
                    // special null value as default: translate to real null here
5413
                    if ($finalArgs[$i] === [Type::T_KEYWORD, 'null']) {
5414
                        $finalArgs[$i] = null;
5415
                    }
5416
                }
5417
                // should we break if this prototype seems fulfilled?
5418
            } catch (CompilerException $e) {
5419
                $exceptionMessage = $e->getMessage();
5420
            }
5421
            $this->ignoreCallStackMessage = $ignoreCallStackMessage;
5422
        }
5423
5424
        if ($exceptionMessage && ! $prototypeHasMatch) {
5425
            if (\in_array($functionName, ['libRgb', 'libRgba', 'libHsl', 'libHsla'])) {
5426
                // if var() or calc() is used as an argument, return as a css function
5427
                foreach ($args as $arg) {
5428
                    if ($arg[1][0] === Type::T_FUNCTION_CALL && in_array($arg[1][1], ['var'])) {
5429
                        return null;
5430
                    }
5431
                }
5432
            }
5433
5434
            throw $this->error($exceptionMessage);
5435
        }
5436
5437
        return [$finalArgs, $keyArgs];
5438
    }
5439
5440
    /**
5441
     * Apply argument values per definition
5442
     *
5443
     * @param array   $argDef
5444
     * @param array   $argValues
5445
     * @param boolean $storeInEnv
5446
     * @param boolean $reduce
5447
     *   only used if $storeInEnv = false
5448
     *
5449
     * @return array
5450
     *
5451
     * @throws \Exception
5452
     */
5453
    protected function applyArguments($argDef, $argValues, $storeInEnv = true, $reduce = true)
5454
    {
5455
        $output = [];
5456
5457
        if (\is_array($argValues) && \count($argValues) && end($argValues) === static::$null) {
5458
            array_pop($argValues);
5459
        }
5460
5461
        if ($storeInEnv) {
5462
            $storeEnv = $this->getStoreEnv();
5463
5464
            $env = new Environment();
5465
            $env->store = $storeEnv->store;
5466
        }
5467
5468
        $hasVariable = false;
5469
        $args = [];
5470
5471
        foreach ($argDef as $i => $arg) {
5472
            list($name, $default, $isVariable) = $argDef[$i];
5473
5474
            $args[$name] = [$i, $name, $default, $isVariable];
5475
            $hasVariable |= $isVariable;
5476
        }
5477
5478
        $splatSeparator      = null;
5479
        $keywordArgs         = [];
5480
        $deferredKeywordArgs = [];
5481
        $deferredNamedKeywordArgs = [];
5482
        $remaining           = [];
5483
        $hasKeywordArgument  = false;
5484
5485
        // assign the keyword args
5486
        foreach ((array) $argValues as $arg) {
5487
            if (! empty($arg[0])) {
5488
                $hasKeywordArgument = true;
5489
5490
                $name = $arg[0][1];
5491
5492
                if (! isset($args[$name])) {
5493
                    foreach (array_keys($args) as $an) {
5494
                        if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
5495
                            $name = $an;
5496
                            break;
5497
                        }
5498
                    }
5499
                }
5500
5501
                if (! isset($args[$name]) || $args[$name][3]) {
5502
                    if ($hasVariable) {
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...
5503
                        $deferredNamedKeywordArgs[$name] = $arg[1];
5504
                    } else {
5505
                        throw $this->error("Mixin or function doesn't have an argument named $%s.", $arg[0][1]);
5506
                    }
5507
                } elseif ($args[$name][0] < \count($remaining)) {
5508
                    throw $this->error("The argument $%s was passed both by position and by name.", $arg[0][1]);
5509
                } else {
5510
                    $keywordArgs[$name] = $arg[1];
5511
                }
5512
            } elseif (! empty($arg[2])) {
5513
                // $arg[2] means a var followed by ... in the arg ($list... )
5514
                $val = $this->reduce($arg[1], true);
5515
5516
                if ($val[0] === Type::T_LIST) {
5517
                    foreach ($val[2] as $name => $item) {
5518
                        if (! is_numeric($name)) {
5519
                            if (! isset($args[$name])) {
5520
                                foreach (array_keys($args) as $an) {
5521
                                    if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
5522
                                        $name = $an;
5523
                                        break;
5524
                                    }
5525
                                }
5526
                            }
5527
5528
                            if ($hasVariable) {
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...
5529
                                $deferredKeywordArgs[$name] = $item;
5530
                            } else {
5531
                                $keywordArgs[$name] = $item;
5532
                            }
5533
                        } else {
5534
                            if (\is_null($splatSeparator)) {
5535
                                $splatSeparator = $val[1];
5536
                            }
5537
5538
                            $remaining[] = $item;
5539
                        }
5540
                    }
5541
                } elseif ($val[0] === Type::T_MAP) {
5542
                    foreach ($val[1] as $i => $name) {
0 ignored issues
show
Bug introduced by
The expression $val[1] of type double|integer is not traversable.
Loading history...
5543
                        $name = $this->compileStringContent($this->coerceString($name));
5544
                        $item = $val[2][$i];
5545
5546
                        if (! is_numeric($name)) {
5547
                            if (! isset($args[$name])) {
5548
                                foreach (array_keys($args) as $an) {
5549
                                    if (str_replace('_', '-', $an) === str_replace('_', '-', $name)) {
5550
                                        $name = $an;
5551
                                        break;
5552
                                    }
5553
                                }
5554
                            }
5555
5556
                            if ($hasVariable) {
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...
5557
                                $deferredKeywordArgs[$name] = $item;
5558
                            } else {
5559
                                $keywordArgs[$name] = $item;
5560
                            }
5561
                        } else {
5562
                            if (\is_null($splatSeparator)) {
5563
                                $splatSeparator = $val[1];
5564
                            }
5565
5566
                            $remaining[] = $item;
5567
                        }
5568
                    }
5569
                } else {
5570
                    $remaining[] = $val;
5571
                }
5572
            } elseif ($hasKeywordArgument) {
5573
                throw $this->error('Positional arguments must come before keyword arguments.');
5574
            } else {
5575
                $remaining[] = $arg[1];
5576
            }
5577
        }
5578
5579
        foreach ($args as $arg) {
5580
            list($i, $name, $default, $isVariable) = $arg;
5581
5582
            if ($isVariable) {
5583
                // only if more than one arg : can not be passed as position and value
5584
                // see https://github.com/sass/libsass/issues/2927
5585
                if (count($args) > 1) {
5586
                    if (isset($remaining[$i]) && isset($deferredNamedKeywordArgs[$name])) {
5587
                        throw $this->error("The argument $%s was passed both by position and by name.", $name);
5588
                    }
5589
                }
5590
5591
                $val = [Type::T_LIST, \is_null($splatSeparator) ? ',' : $splatSeparator , [], $isVariable];
5592
5593
                for ($count = \count($remaining); $i < $count; $i++) {
5594
                    $val[2][] = $remaining[$i];
5595
                }
5596
5597
                foreach ($deferredKeywordArgs as $itemName => $item) {
5598
                    $val[2][$itemName] = $item;
5599
                }
5600
5601
                foreach ($deferredNamedKeywordArgs as $itemName => $item) {
5602
                    $val[2][$itemName] = $item;
5603
                }
5604
            } elseif (isset($remaining[$i])) {
5605
                $val = $remaining[$i];
5606
            } elseif (isset($keywordArgs[$name])) {
5607
                $val = $keywordArgs[$name];
5608
            } elseif (! empty($default)) {
5609
                continue;
5610
            } else {
5611
                throw $this->error("Missing argument $name");
5612
            }
5613
5614
            if ($storeInEnv) {
5615
                $this->set($name, $this->reduce($val, true), true, $env);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $env does not seem to be defined for all execution paths leading up to this point.
Loading history...
5616
            } else {
5617
                $output[$name] = ($reduce ? $this->reduce($val, true) : $val);
5618
            }
5619
        }
5620
5621
        if ($storeInEnv) {
5622
            $storeEnv->store = $env->store;
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $storeEnv does not seem to be defined for all execution paths leading up to this point.
Loading history...
5623
        }
5624
5625
        foreach ($args as $arg) {
5626
            list($i, $name, $default, $isVariable) = $arg;
5627
5628
            if ($isVariable || isset($remaining[$i]) || isset($keywordArgs[$name]) || empty($default)) {
5629
                continue;
5630
            }
5631
5632
            if ($storeInEnv) {
5633
                $this->set($name, $this->reduce($default, true), true);
5634
            } else {
5635
                $output[$name] = ($reduce ? $this->reduce($default, true) : $default);
5636
            }
5637
        }
5638
5639
        return $output;
5640
    }
5641
5642
    /**
5643
     * Coerce a php value into a scss one
5644
     *
5645
     * @param mixed $value
5646
     *
5647
     * @return array|\ScssPhp\ScssPhp\Node\Number
5648
     */
5649
    protected function coerceValue($value)
5650
    {
5651
        if (\is_array($value) || $value instanceof \ArrayAccess) {
5652
            return $value;
5653
        }
5654
5655
        if (\is_bool($value)) {
5656
            return $this->toBool($value);
5657
        }
5658
5659
        if (\is_null($value)) {
5660
            return static::$null;
5661
        }
5662
5663
        if (is_numeric($value)) {
5664
            return new Node\Number($value, '');
5665
        }
5666
5667
        if ($value === '') {
5668
            return static::$emptyString;
5669
        }
5670
5671
        $value = [Type::T_KEYWORD, $value];
5672
        $color = $this->coerceColor($value);
5673
5674
        if ($color) {
5675
            return $color;
5676
        }
5677
5678
        return $value;
5679
    }
5680
5681
    /**
5682
     * Coerce something to map
5683
     *
5684
     * @param array $item
5685
     *
5686
     * @return array
5687
     */
5688
    protected function coerceMap($item)
5689
    {
5690
        if ($item[0] === Type::T_MAP) {
5691
            return $item;
5692
        }
5693
5694
        if (
5695
            $item[0] === static::$emptyList[0] &&
5696
            $item[1] === static::$emptyList[1] &&
5697
            $item[2] === static::$emptyList[2]
5698
        ) {
5699
            return static::$emptyMap;
5700
        }
5701
5702
        return $item;
5703
    }
5704
5705
    /**
5706
     * Coerce something to list
5707
     *
5708
     * @param array   $item
5709
     * @param string  $delim
5710
     * @param boolean $removeTrailingNull
5711
     *
5712
     * @return array
5713
     */
5714
    protected function coerceList($item, $delim = ',', $removeTrailingNull = false)
5715
    {
5716
        if (isset($item) && $item[0] === Type::T_LIST) {
5717
            // remove trailing null from the list
5718
            if ($removeTrailingNull && end($item[2]) === static::$null) {
5719
                array_pop($item[2]);
5720
            }
5721
5722
            return $item;
5723
        }
5724
5725
        if (isset($item) && $item[0] === Type::T_MAP) {
5726
            $keys = $item[1];
5727
            $values = $item[2];
5728
            $list = [];
5729
5730
            for ($i = 0, $s = \count($keys); $i < $s; $i++) {
5731
                $key = $keys[$i];
5732
                $value = $values[$i];
5733
5734
                switch ($key[0]) {
5735
                    case Type::T_LIST:
5736
                    case Type::T_MAP:
5737
                    case Type::T_STRING:
5738
                    case Type::T_NULL:
5739
                        break;
5740
5741
                    default:
5742
                        $key = [Type::T_KEYWORD, $this->compileStringContent($this->coerceString($key))];
5743
                        break;
5744
                }
5745
5746
                $list[] = [
5747
                    Type::T_LIST,
5748
                    '',
5749
                    [$key, $value]
5750
                ];
5751
            }
5752
5753
            return [Type::T_LIST, ',', $list];
5754
        }
5755
5756
        return [Type::T_LIST, $delim, ! isset($item) ? [] : [$item]];
5757
    }
5758
5759
    /**
5760
     * Coerce color for expression
5761
     *
5762
     * @param array $value
5763
     *
5764
     * @return array|null
5765
     */
5766
    protected function coerceForExpression($value)
5767
    {
5768
        if ($color = $this->coerceColor($value)) {
5769
            return $color;
5770
        }
5771
5772
        return $value;
5773
    }
5774
5775
    /**
5776
     * Coerce value to color
5777
     *
5778
     * @param array $value
5779
     *
5780
     * @return array|null
5781
     */
5782
    protected function coerceColor($value, $inRGBFunction = false)
5783
    {
5784
        switch ($value[0]) {
5785
            case Type::T_COLOR:
5786
                for ($i = 1; $i <= 3; $i++) {
5787
                    if (! is_numeric($value[$i])) {
5788
                        $cv = $this->compileRGBAValue($value[$i]);
5789
5790
                        if (! is_numeric($cv)) {
5791
                            return null;
5792
                        }
5793
5794
                        $value[$i] = $cv;
5795
                    }
5796
5797
                    if (isset($value[4])) {
5798
                        if (! is_numeric($value[4])) {
5799
                            $cv = $this->compileRGBAValue($value[4], true);
5800
5801
                            if (! is_numeric($cv)) {
5802
                                return null;
5803
                            }
5804
5805
                            $value[4] = $cv;
5806
                        }
5807
                    }
5808
                }
5809
5810
                return $value;
5811
5812
            case Type::T_LIST:
5813
                if ($inRGBFunction) {
5814
                    if (\count($value[2]) == 3 || \count($value[2]) == 4) {
5815
                        $color = $value[2];
5816
                        array_unshift($color, Type::T_COLOR);
5817
5818
                        return $this->coerceColor($color);
5819
                    }
5820
                }
5821
5822
                return null;
5823
5824
            case Type::T_KEYWORD:
5825
                if (! \is_string($value[1])) {
5826
                    return null;
5827
                }
5828
5829
                $name = strtolower($value[1]);
5830
5831
                // hexa color?
5832
                if (preg_match('/^#([0-9a-f]+)$/i', $name, $m)) {
5833
                    $nofValues = \strlen($m[1]);
5834
5835
                    if (\in_array($nofValues, [3, 4, 6, 8])) {
5836
                        $nbChannels = 3;
5837
                        $color      = [];
5838
                        $num        = hexdec($m[1]);
5839
5840
                        switch ($nofValues) {
5841
                            case 4:
5842
                                $nbChannels = 4;
5843
                                // then continuing with the case 3:
5844
                            case 3:
5845
                                for ($i = 0; $i < $nbChannels; $i++) {
5846
                                    $t = $num & 0xf;
5847
                                    array_unshift($color, $t << 4 | $t);
5848
                                    $num >>= 4;
5849
                                }
5850
5851
                                break;
5852
5853
                            case 8:
5854
                                $nbChannels = 4;
5855
                                // then continuing with the case 6:
5856
                            case 6:
5857
                                for ($i = 0; $i < $nbChannels; $i++) {
5858
                                    array_unshift($color, $num & 0xff);
5859
                                    $num >>= 8;
5860
                                }
5861
5862
                                break;
5863
                        }
5864
5865
                        if ($nbChannels === 4) {
5866
                            if ($color[3] === 255) {
5867
                                $color[3] = 1; // fully opaque
5868
                            } else {
5869
                                $color[3] = round($color[3] / 255, Node\Number::PRECISION);
5870
                            }
5871
                        }
5872
5873
                        array_unshift($color, Type::T_COLOR);
5874
5875
                        return $color;
5876
                    }
5877
                }
5878
5879
                if ($rgba = Colors::colorNameToRGBa($name)) {
5880
                    return isset($rgba[3])
5881
                        ? [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2], $rgba[3]]
5882
                        : [Type::T_COLOR, $rgba[0], $rgba[1], $rgba[2]];
5883
                }
5884
5885
                return null;
5886
        }
5887
5888
        return null;
5889
    }
5890
5891
    /**
5892
     * @param integer|\ScssPhp\ScssPhp\Node\Number $value
5893
     * @param boolean                              $isAlpha
5894
     *
5895
     * @return integer|mixed
5896
     */
5897
    protected function compileRGBAValue($value, $isAlpha = false)
5898
    {
5899
        if ($isAlpha) {
5900
            return $this->compileColorPartValue($value, 0, 1, false);
5901
        }
5902
5903
        return $this->compileColorPartValue($value, 0, 255, true);
5904
    }
5905
5906
    /**
5907
     * @param mixed         $value
5908
     * @param integer|float $min
5909
     * @param integer|float $max
5910
     * @param boolean       $isInt
5911
     * @param boolean       $clamp
5912
     * @param boolean       $modulo
5913
     *
5914
     * @return integer|mixed
5915
     */
5916
    protected function compileColorPartValue($value, $min, $max, $isInt = true, $clamp = true, $modulo = false)
5917
    {
5918
        if (! is_numeric($value)) {
5919
            if (\is_array($value)) {
5920
                $reduced = $this->reduce($value);
5921
5922
                if (\is_object($reduced) && $value->type === Type::T_NUMBER) {
5923
                    $value = $reduced;
5924
                }
5925
            }
5926
5927
            if (\is_object($value) && $value->type === Type::T_NUMBER) {
5928
                $num = $value->dimension;
5929
5930
                if (\count($value->units)) {
5931
                    $unit = array_keys($value->units);
5932
                    $unit = reset($unit);
5933
5934
                    switch ($unit) {
5935
                        case '%':
5936
                            $num *= $max / 100;
5937
                            break;
5938
                        default:
5939
                            break;
5940
                    }
5941
                }
5942
5943
                $value = $num;
5944
            } elseif (\is_array($value)) {
5945
                $value = $this->compileValue($value);
5946
            }
5947
        }
5948
5949
        if (is_numeric($value)) {
5950
            if ($isInt) {
5951
                $value = round($value);
5952
            }
5953
5954
            if ($clamp) {
5955
                $value = min($max, max($min, $value));
5956
            }
5957
5958
            if ($modulo) {
5959
                $value = $value % $max;
5960
5961
                // still negative?
5962
                while ($value < $min) {
5963
                    $value += $max;
5964
                }
5965
            }
5966
5967
            return $value;
5968
        }
5969
5970
        return $value;
5971
    }
5972
5973
    /**
5974
     * Coerce value to string
5975
     *
5976
     * @param array $value
5977
     *
5978
     * @return array|null
5979
     */
5980
    protected function coerceString($value)
5981
    {
5982
        if ($value[0] === Type::T_STRING) {
5983
            return $value;
5984
        }
5985
5986
        return [Type::T_STRING, '', [$this->compileValue($value)]];
5987
    }
5988
5989
    /**
5990
     * Coerce value to a percentage
5991
     *
5992
     * @param array $value
5993
     *
5994
     * @return integer|float
5995
     */
5996
    protected function coercePercent($value)
5997
    {
5998
        if ($value[0] === Type::T_NUMBER) {
5999
            if (! empty($value[2]['%'])) {
6000
                return $value[1] / 100;
6001
            }
6002
6003
            return $value[1];
6004
        }
6005
6006
        return 0;
6007
    }
6008
6009
    /**
6010
     * Assert value is a map
6011
     *
6012
     * @api
6013
     *
6014
     * @param array $value
6015
     *
6016
     * @return array
6017
     *
6018
     * @throws \Exception
6019
     */
6020
    public function assertMap($value)
6021
    {
6022
        $value = $this->coerceMap($value);
6023
6024
        if ($value[0] !== Type::T_MAP) {
6025
            throw $this->error('expecting map, %s received', $value[0]);
6026
        }
6027
6028
        return $value;
6029
    }
6030
6031
    /**
6032
     * Assert value is a list
6033
     *
6034
     * @api
6035
     *
6036
     * @param array $value
6037
     *
6038
     * @return array
6039
     *
6040
     * @throws \Exception
6041
     */
6042
    public function assertList($value)
6043
    {
6044
        if ($value[0] !== Type::T_LIST) {
6045
            throw $this->error('expecting list, %s received', $value[0]);
6046
        }
6047
6048
        return $value;
6049
    }
6050
6051
    /**
6052
     * Assert value is a color
6053
     *
6054
     * @api
6055
     *
6056
     * @param array $value
6057
     *
6058
     * @return array
6059
     *
6060
     * @throws \Exception
6061
     */
6062
    public function assertColor($value)
6063
    {
6064
        if ($color = $this->coerceColor($value)) {
6065
            return $color;
6066
        }
6067
6068
        throw $this->error('expecting color, %s received', $value[0]);
6069
    }
6070
6071
    /**
6072
     * Assert value is a number
6073
     *
6074
     * @api
6075
     *
6076
     * @param array $value
6077
     *
6078
     * @return integer|float
6079
     *
6080
     * @throws \Exception
6081
     */
6082
    public function assertNumber($value)
6083
    {
6084
        if ($value[0] !== Type::T_NUMBER) {
6085
            throw $this->error('expecting number, %s received', $value[0]);
6086
        }
6087
6088
        return $value[1];
6089
    }
6090
6091
    /**
6092
     * Make sure a color's components don't go out of bounds
6093
     *
6094
     * @param array $c
6095
     *
6096
     * @return array
6097
     */
6098
    protected function fixColor($c)
6099
    {
6100
        foreach ([1, 2, 3] as $i) {
6101
            if ($c[$i] < 0) {
6102
                $c[$i] = 0;
6103
            }
6104
6105
            if ($c[$i] > 255) {
6106
                $c[$i] = 255;
6107
            }
6108
        }
6109
6110
        return $c;
6111
    }
6112
6113
    /**
6114
     * Convert RGB to HSL
6115
     *
6116
     * @api
6117
     *
6118
     * @param integer $red
6119
     * @param integer $green
6120
     * @param integer $blue
6121
     *
6122
     * @return array
6123
     */
6124
    public function toHSL($red, $green, $blue)
6125
    {
6126
        $min = min($red, $green, $blue);
6127
        $max = max($red, $green, $blue);
6128
6129
        $l = $min + $max;
6130
        $d = $max - $min;
6131
6132
        if ((int) $d === 0) {
6133
            $h = $s = 0;
6134
        } else {
6135
            if ($l < 255) {
6136
                $s = $d / $l;
6137
            } else {
6138
                $s = $d / (510 - $l);
6139
            }
6140
6141
            if ($red == $max) {
6142
                $h = 60 * ($green - $blue) / $d;
6143
            } elseif ($green == $max) {
6144
                $h = 60 * ($blue - $red) / $d + 120;
6145
            } elseif ($blue == $max) {
6146
                $h = 60 * ($red - $green) / $d + 240;
6147
            }
6148
        }
6149
6150
        return [Type::T_HSL, fmod($h, 360), $s * 100, $l / 5.1];
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $h does not seem to be defined for all execution paths leading up to this point.
Loading history...
6151
    }
6152
6153
    /**
6154
     * Hue to RGB helper
6155
     *
6156
     * @param float $m1
6157
     * @param float $m2
6158
     * @param float $h
6159
     *
6160
     * @return float
6161
     */
6162
    protected function hueToRGB($m1, $m2, $h)
6163
    {
6164
        if ($h < 0) {
6165
            $h += 1;
6166
        } elseif ($h > 1) {
6167
            $h -= 1;
6168
        }
6169
6170
        if ($h * 6 < 1) {
6171
            return $m1 + ($m2 - $m1) * $h * 6;
6172
        }
6173
6174
        if ($h * 2 < 1) {
6175
            return $m2;
6176
        }
6177
6178
        if ($h * 3 < 2) {
6179
            return $m1 + ($m2 - $m1) * (2 / 3 - $h) * 6;
6180
        }
6181
6182
        return $m1;
6183
    }
6184
6185
    /**
6186
     * Convert HSL to RGB
6187
     *
6188
     * @api
6189
     *
6190
     * @param integer $hue        H from 0 to 360
6191
     * @param integer $saturation S from 0 to 100
6192
     * @param integer $lightness  L from 0 to 100
6193
     *
6194
     * @return array
6195
     */
6196
    public function toRGB($hue, $saturation, $lightness)
6197
    {
6198
        if ($hue < 0) {
6199
            $hue += 360;
6200
        }
6201
6202
        $h = $hue / 360;
6203
        $s = min(100, max(0, $saturation)) / 100;
6204
        $l = min(100, max(0, $lightness)) / 100;
6205
6206
        $m2 = $l <= 0.5 ? $l * ($s + 1) : $l + $s - $l * $s;
6207
        $m1 = $l * 2 - $m2;
6208
6209
        $r = $this->hueToRGB($m1, $m2, $h + 1 / 3) * 255;
6210
        $g = $this->hueToRGB($m1, $m2, $h) * 255;
6211
        $b = $this->hueToRGB($m1, $m2, $h - 1 / 3) * 255;
6212
6213
        $out = [Type::T_COLOR, $r, $g, $b];
6214
6215
        return $out;
6216
    }
6217
6218
    // Built in functions
6219
6220
    protected static $libCall = ['name', 'args...'];
6221
    protected function libCall($args, $kwargs)
6222
    {
6223
        $functionReference = $this->reduce(array_shift($args), true);
6224
6225
        if (in_array($functionReference[0], [Type::T_STRING, Type::T_KEYWORD])) {
6226
            $name = $this->compileStringContent($this->coerceString($this->reduce($functionReference, true)));
6227
            $warning = "DEPRECATION WARNING: Passing a string to call() is deprecated and will be illegal\n"
6228
                . "in Sass 4.0. Use call(function-reference($name)) instead.";
6229
            fwrite($this->stderr, "$warning\n\n");
0 ignored issues
show
Bug introduced by
It seems like $this->stderr can also be of type boolean; however, parameter $handle of fwrite() does only seem to accept resource, 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

6229
            fwrite(/** @scrutinizer ignore-type */ $this->stderr, "$warning\n\n");
Loading history...
6230
            $functionReference = $this->libGetFunction([$functionReference]);
6231
        }
6232
6233
        if ($functionReference === static::$null) {
6234
            return static::$null;
6235
        }
6236
6237
        if (! in_array($functionReference[0], [Type::T_FUNCTION_REFERENCE, Type::T_FUNCTION])) {
6238
            throw $this->error('Function reference expected, got ' . $functionReference[0]);
6239
        }
6240
6241
        $callArgs = [];
6242
6243
        // $kwargs['args'] is [Type::T_LIST, ',', [..]]
6244
        foreach ($kwargs['args'][2] as $varname => $arg) {
6245
            if (is_numeric($varname)) {
6246
                $varname = null;
6247
            } else {
6248
                $varname = [ 'var', $varname];
6249
            }
6250
6251
            $callArgs[] = [$varname, $arg, false];
6252
        }
6253
6254
        return $this->reduce([Type::T_FUNCTION_CALL, $functionReference, $callArgs]);
6255
    }
6256
6257
6258
    protected static $libGetFunction = [
6259
        ['name'],
6260
        ['name', 'css']
6261
    ];
6262
    protected function libGetFunction($args)
6263
    {
6264
        $name = $this->compileStringContent($this->coerceString($this->reduce(array_shift($args), true)));
6265
        $isCss = false;
6266
6267
        if (count($args)) {
6268
            $isCss = $this->reduce(array_shift($args), true);
6269
            $isCss = (($isCss === static::$true) ? true : false);
0 ignored issues
show
introduced by
The condition $isCss === static::true is always false.
Loading history...
6270
        }
6271
6272
        if ($isCss) {
0 ignored issues
show
introduced by
The condition $isCss is always false.
Loading history...
6273
            return [Type::T_FUNCTION, $name, [Type::T_LIST, ',', []]];
6274
        }
6275
6276
        return $this->getFunctionReference($name, true);
6277
    }
6278
6279
    protected static $libIf = ['condition', 'if-true', 'if-false:'];
6280
    protected function libIf($args)
6281
    {
6282
        list($cond, $t, $f) = $args;
6283
6284
        if (! $this->isTruthy($this->reduce($cond, true))) {
6285
            return $this->reduce($f, true);
6286
        }
6287
6288
        return $this->reduce($t, true);
6289
    }
6290
6291
    protected static $libIndex = ['list', 'value'];
6292
    protected function libIndex($args)
6293
    {
6294
        list($list, $value) = $args;
6295
6296
        if (
6297
            $list[0] === Type::T_MAP ||
6298
            $list[0] === Type::T_STRING ||
6299
            $list[0] === Type::T_KEYWORD ||
6300
            $list[0] === Type::T_INTERPOLATE
6301
        ) {
6302
            $list = $this->coerceList($list, ' ');
6303
        }
6304
6305
        if ($list[0] !== Type::T_LIST) {
6306
            return static::$null;
6307
        }
6308
6309
        $values = [];
6310
6311
        foreach ($list[2] as $item) {
6312
            $values[] = $this->normalizeValue($item);
6313
        }
6314
6315
        $key = array_search($this->normalizeValue($value), $values);
6316
6317
        return false === $key ? static::$null : $key + 1;
6318
    }
6319
6320
    protected static $libRgb = [
6321
        ['color'],
6322
        ['color', 'alpha'],
6323
        ['channels'],
6324
        ['red', 'green', 'blue'],
6325
        ['red', 'green', 'blue', 'alpha'] ];
6326
    protected function libRgb($args, $kwargs, $funcName = 'rgb')
6327
    {
6328
        switch (\count($args)) {
6329
            case 1:
6330
                if (! $color = $this->coerceColor($args[0], true)) {
6331
                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6332
                }
6333
                break;
6334
6335
            case 3:
6336
                $color = [Type::T_COLOR, $args[0], $args[1], $args[2]];
6337
6338
                if (! $color = $this->coerceColor($color)) {
6339
                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
6340
                }
6341
6342
                return $color;
6343
6344
            case 2:
6345
                if ($color = $this->coerceColor($args[0], true)) {
6346
                    $alpha = $this->compileRGBAValue($args[1], true);
6347
6348
                    if (is_numeric($alpha)) {
6349
                        $color[4] = $alpha;
6350
                    } else {
6351
                        $color = [Type::T_STRING, '',
6352
                            [$funcName . '(', $color[1], ', ', $color[2], ', ', $color[3], ', ', $alpha, ')']];
6353
                    }
6354
                } else {
6355
                    $color = [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6356
                }
6357
                break;
6358
6359
            case 4:
6360
            default:
6361
                $color = [Type::T_COLOR, $args[0], $args[1], $args[2], $args[3]];
6362
6363
                if (! $color = $this->coerceColor($color)) {
6364
                    $color = [Type::T_STRING, '',
6365
                        [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6366
                }
6367
                break;
6368
        }
6369
6370
        return $color;
6371
    }
6372
6373
    protected static $libRgba = [
6374
        ['color'],
6375
        ['color', 'alpha'],
6376
        ['channels'],
6377
        ['red', 'green', 'blue'],
6378
        ['red', 'green', 'blue', 'alpha'] ];
6379
    protected function libRgba($args, $kwargs)
6380
    {
6381
        return $this->libRgb($args, $kwargs, 'rgba');
6382
    }
6383
6384
    // helper function for adjust_color, change_color, and scale_color
6385
    protected function alterColor($args, $fn)
6386
    {
6387
        $color = $this->assertColor($args[0]);
6388
6389
        foreach ([1 => 1, 2 => 2, 3 => 3, 7 => 4] as $iarg => $irgba) {
6390
            if (isset($args[$iarg])) {
6391
                $val = $this->assertNumber($args[$iarg]);
6392
6393
                if (! isset($color[$irgba])) {
6394
                    $color[$irgba] = (($irgba < 4) ? 0 : 1);
6395
                }
6396
6397
                $color[$irgba] = \call_user_func($fn, $color[$irgba], $val, $iarg);
6398
            }
6399
        }
6400
6401
        if (! empty($args[4]) || ! empty($args[5]) || ! empty($args[6])) {
6402
            $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6403
6404
            foreach ([4 => 1, 5 => 2, 6 => 3] as $iarg => $ihsl) {
6405
                if (! empty($args[$iarg])) {
6406
                    $val = $this->assertNumber($args[$iarg]);
6407
                    $hsl[$ihsl] = \call_user_func($fn, $hsl[$ihsl], $val, $iarg);
6408
                }
6409
            }
6410
6411
            $rgb = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
0 ignored issues
show
Bug introduced by
$hsl[3] of type double is incompatible with the type integer expected by parameter $lightness of ScssPhp\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

6411
            $rgb = $this->toRGB($hsl[1], $hsl[2], /** @scrutinizer ignore-type */ $hsl[3]);
Loading history...
Bug introduced by
$hsl[1] of type double is incompatible with the type integer expected by parameter $hue of ScssPhp\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

6411
            $rgb = $this->toRGB(/** @scrutinizer ignore-type */ $hsl[1], $hsl[2], $hsl[3]);
Loading history...
6412
6413
            if (isset($color[4])) {
6414
                $rgb[4] = $color[4];
6415
            }
6416
6417
            $color = $rgb;
6418
        }
6419
6420
        return $color;
6421
    }
6422
6423
    protected static $libAdjustColor = [
6424
        'color', 'red:null', 'green:null', 'blue:null',
6425
        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6426
    ];
6427
    protected function libAdjustColor($args)
6428
    {
6429
        return $this->alterColor($args, function ($base, $alter, $i) {
6430
            return $base + $alter;
6431
        });
6432
    }
6433
6434
    protected static $libChangeColor = [
6435
        'color', 'red:null', 'green:null', 'blue:null',
6436
        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6437
    ];
6438
    protected function libChangeColor($args)
6439
    {
6440
        return $this->alterColor($args, function ($base, $alter, $i) {
6441
            return $alter;
6442
        });
6443
    }
6444
6445
    protected static $libScaleColor = [
6446
        'color', 'red:null', 'green:null', 'blue:null',
6447
        'hue:null', 'saturation:null', 'lightness:null', 'alpha:null'
6448
    ];
6449
    protected function libScaleColor($args)
6450
    {
6451
        return $this->alterColor($args, function ($base, $scale, $i) {
6452
            // 1, 2, 3 - rgb
6453
            // 4, 5, 6 - hsl
6454
            // 7 - a
6455
            switch ($i) {
6456
                case 1:
6457
                case 2:
6458
                case 3:
6459
                    $max = 255;
6460
                    break;
6461
6462
                case 4:
6463
                    $max = 360;
6464
                    break;
6465
6466
                case 7:
6467
                    $max = 1;
6468
                    break;
6469
6470
                default:
6471
                    $max = 100;
6472
            }
6473
6474
            $scale = $scale / 100;
6475
6476
            if ($scale < 0) {
6477
                return $base * $scale + $base;
6478
            }
6479
6480
            return ($max - $base) * $scale + $base;
6481
        });
6482
    }
6483
6484
    protected static $libIeHexStr = ['color'];
6485
    protected function libIeHexStr($args)
6486
    {
6487
        $color = $this->coerceColor($args[0]);
6488
6489
        if (\is_null($color)) {
6490
            $this->throwError('Error: argument `$color` of `ie-hex-str($color)` must be a color');
0 ignored issues
show
Deprecated Code introduced by
The function ScssPhp\ScssPhp\Compiler::throwError() has been deprecated: use "error" and throw the exception in the caller instead. ( Ignorable by Annotation )

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

6490
            /** @scrutinizer ignore-deprecated */ $this->throwError('Error: argument `$color` of `ie-hex-str($color)` must be a color');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
6491
        }
6492
6493
        $color[4] = isset($color[4]) ? round(255 * $color[4]) : 255;
6494
6495
        return [Type::T_STRING, '', [sprintf('#%02X%02X%02X%02X', $color[4], $color[1], $color[2], $color[3])]];
6496
    }
6497
6498
    protected static $libRed = ['color'];
6499
    protected function libRed($args)
6500
    {
6501
        $color = $this->coerceColor($args[0]);
6502
6503
        if (\is_null($color)) {
6504
            $this->throwError('Error: argument `$color` of `red($color)` must be a color');
0 ignored issues
show
Deprecated Code introduced by
The function ScssPhp\ScssPhp\Compiler::throwError() has been deprecated: use "error" and throw the exception in the caller instead. ( Ignorable by Annotation )

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

6504
            /** @scrutinizer ignore-deprecated */ $this->throwError('Error: argument `$color` of `red($color)` must be a color');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
6505
        }
6506
6507
        return $color[1];
6508
    }
6509
6510
    protected static $libGreen = ['color'];
6511
    protected function libGreen($args)
6512
    {
6513
        $color = $this->coerceColor($args[0]);
6514
6515
        if (\is_null($color)) {
6516
            $this->throwError('Error: argument `$color` of `green($color)` must be a color');
0 ignored issues
show
Deprecated Code introduced by
The function ScssPhp\ScssPhp\Compiler::throwError() has been deprecated: use "error" and throw the exception in the caller instead. ( Ignorable by Annotation )

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

6516
            /** @scrutinizer ignore-deprecated */ $this->throwError('Error: argument `$color` of `green($color)` must be a color');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
6517
        }
6518
6519
        return $color[2];
6520
    }
6521
6522
    protected static $libBlue = ['color'];
6523
    protected function libBlue($args)
6524
    {
6525
        $color = $this->coerceColor($args[0]);
6526
6527
        if (\is_null($color)) {
6528
            $this->throwError('Error: argument `$color` of `blue($color)` must be a color');
0 ignored issues
show
Deprecated Code introduced by
The function ScssPhp\ScssPhp\Compiler::throwError() has been deprecated: use "error" and throw the exception in the caller instead. ( Ignorable by Annotation )

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

6528
            /** @scrutinizer ignore-deprecated */ $this->throwError('Error: argument `$color` of `blue($color)` must be a color');

This function has been deprecated. The supplier of the function has supplied an explanatory message.

The explanatory message should give you some clue as to whether and when the function will be removed and what other function to use instead.

Loading history...
6529
        }
6530
6531
        return $color[3];
6532
    }
6533
6534
    protected static $libAlpha = ['color'];
6535
    protected function libAlpha($args)
6536
    {
6537
        if ($color = $this->coerceColor($args[0])) {
6538
            return isset($color[4]) ? $color[4] : 1;
6539
        }
6540
6541
        // this might be the IE function, so return value unchanged
6542
        return null;
6543
    }
6544
6545
    protected static $libOpacity = ['color'];
6546
    protected function libOpacity($args)
6547
    {
6548
        $value = $args[0];
6549
6550
        if ($value[0] === Type::T_NUMBER) {
6551
            return null;
6552
        }
6553
6554
        return $this->libAlpha($args);
6555
    }
6556
6557
    // mix two colors
6558
    protected static $libMix = [
6559
        ['color1', 'color2', 'weight:0.5'],
6560
        ['color-1', 'color-2', 'weight:0.5']
6561
        ];
6562
    protected function libMix($args)
6563
    {
6564
        list($first, $second, $weight) = $args;
6565
6566
        $first = $this->assertColor($first);
6567
        $second = $this->assertColor($second);
6568
6569
        if (! isset($weight)) {
6570
            $weight = 0.5;
6571
        } else {
6572
            $weight = $this->coercePercent($weight);
6573
        }
6574
6575
        $firstAlpha = isset($first[4]) ? $first[4] : 1;
6576
        $secondAlpha = isset($second[4]) ? $second[4] : 1;
6577
6578
        $w = $weight * 2 - 1;
6579
        $a = $firstAlpha - $secondAlpha;
6580
6581
        $w1 = (($w * $a === -1 ? $w : ($w + $a) / (1 + $w * $a)) + 1) / 2.0;
0 ignored issues
show
introduced by
The condition $w * $a === -1 is always false.
Loading history...
6582
        $w2 = 1.0 - $w1;
6583
6584
        $new = [Type::T_COLOR,
6585
            $w1 * $first[1] + $w2 * $second[1],
6586
            $w1 * $first[2] + $w2 * $second[2],
6587
            $w1 * $first[3] + $w2 * $second[3],
6588
        ];
6589
6590
        if ($firstAlpha != 1.0 || $secondAlpha != 1.0) {
6591
            $new[] = $firstAlpha * $weight + $secondAlpha * (1 - $weight);
6592
        }
6593
6594
        return $this->fixColor($new);
6595
    }
6596
6597
    protected static $libHsl = [
6598
        ['channels'],
6599
        ['hue', 'saturation', 'lightness'],
6600
        ['hue', 'saturation', 'lightness', 'alpha'] ];
6601
    protected function libHsl($args, $kwargs, $funcName = 'hsl')
6602
    {
6603
        $args_to_check = $args;
6604
6605
        if (\count($args) == 1) {
6606
            if ($args[0][0] !== Type::T_LIST || \count($args[0][2]) < 3 || \count($args[0][2]) > 4) {
6607
                return [Type::T_STRING, '', [$funcName . '(', $args[0], ')']];
6608
            }
6609
6610
            $args = $args[0][2];
6611
            $args_to_check = $kwargs['channels'][2];
6612
        }
6613
6614
        $hue = $this->compileColorPartValue($args[0], 0, 360, false, false, true);
6615
        $saturation = $this->compileColorPartValue($args[1], 0, 100, false);
6616
        $lightness = $this->compileColorPartValue($args[2], 0, 100, false);
6617
6618
        foreach ($kwargs as $k => $arg) {
6619
            if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
6620
                return null;
6621
            }
6622
        }
6623
6624
        foreach ($args_to_check as $k => $arg) {
6625
            if (in_array($arg[0], [Type::T_FUNCTION_CALL]) && in_array($arg[1], ['min', 'max'])) {
6626
                if (count($kwargs) > 1 || ($k >= 2 && count($args) === 4)) {
6627
                    return null;
6628
                }
6629
6630
                $args[$k] = $this->stringifyFncallArgs($arg);
6631
                $hue = '';
6632
            }
6633
6634
            if (
6635
                $k >= 2 && count($args) === 4 &&
6636
                in_array($arg[0], [Type::T_FUNCTION_CALL, Type::T_FUNCTION]) &&
6637
                in_array($arg[1], ['calc','env'])
6638
            ) {
6639
                return null;
6640
            }
6641
        }
6642
6643
        $alpha = null;
6644
6645
        if (\count($args) === 4) {
6646
            $alpha = $this->compileColorPartValue($args[3], 0, 100, false);
6647
6648
            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness) || ! is_numeric($alpha)) {
6649
                return [Type::T_STRING, '',
6650
                    [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ', ', $args[3], ')']];
6651
            }
6652
        } else {
6653
            if (! is_numeric($hue) || ! is_numeric($saturation) || ! is_numeric($lightness)) {
6654
                return [Type::T_STRING, '', [$funcName . '(', $args[0], ', ', $args[1], ', ', $args[2], ')']];
6655
            }
6656
        }
6657
6658
        $color = $this->toRGB($hue, $saturation, $lightness);
0 ignored issues
show
Bug introduced by
It seems like $hue can also be of type string; however, parameter $hue of ScssPhp\ScssPhp\Compiler::toRGB() does only seem to accept integer, 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

6658
        $color = $this->toRGB(/** @scrutinizer ignore-type */ $hue, $saturation, $lightness);
Loading history...
6659
6660
        if (! \is_null($alpha)) {
6661
            $color[4] = $alpha;
6662
        }
6663
6664
        return $color;
6665
    }
6666
6667
    protected static $libHsla = [
6668
            ['channels'],
6669
            ['hue', 'saturation', 'lightness'],
6670
            ['hue', 'saturation', 'lightness', 'alpha']];
6671
    protected function libHsla($args, $kwargs)
6672
    {
6673
        return $this->libHsl($args, $kwargs, 'hsla');
6674
    }
6675
6676
    protected static $libHue = ['color'];
6677
    protected function libHue($args)
6678
    {
6679
        $color = $this->assertColor($args[0]);
6680
        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6681
6682
        return new Node\Number($hsl[1], 'deg');
6683
    }
6684
6685
    protected static $libSaturation = ['color'];
6686
    protected function libSaturation($args)
6687
    {
6688
        $color = $this->assertColor($args[0]);
6689
        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6690
6691
        return new Node\Number($hsl[2], '%');
6692
    }
6693
6694
    protected static $libLightness = ['color'];
6695
    protected function libLightness($args)
6696
    {
6697
        $color = $this->assertColor($args[0]);
6698
        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6699
6700
        return new Node\Number($hsl[3], '%');
6701
    }
6702
6703
    protected function adjustHsl($color, $idx, $amount)
6704
    {
6705
        $hsl = $this->toHSL($color[1], $color[2], $color[3]);
6706
        $hsl[$idx] += $amount;
6707
        $out = $this->toRGB($hsl[1], $hsl[2], $hsl[3]);
0 ignored issues
show
Bug introduced by
$hsl[1] of type double is incompatible with the type integer expected by parameter $hue of ScssPhp\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

6707
        $out = $this->toRGB(/** @scrutinizer ignore-type */ $hsl[1], $hsl[2], $hsl[3]);
Loading history...
Bug introduced by
$hsl[3] of type double is incompatible with the type integer expected by parameter $lightness of ScssPhp\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

6707
        $out = $this->toRGB($hsl[1], $hsl[2], /** @scrutinizer ignore-type */ $hsl[3]);
Loading history...
6708
6709
        if (isset($color[4])) {
6710
            $out[4] = $color[4];
6711
        }
6712
6713
        return $out;
6714
    }
6715
6716
    protected static $libAdjustHue = ['color', 'degrees'];
6717
    protected function libAdjustHue($args)
6718
    {
6719
        $color = $this->assertColor($args[0]);
6720
        $degrees = $this->assertNumber($args[1]);
6721
6722
        return $this->adjustHsl($color, 1, $degrees);
6723
    }
6724
6725
    protected static $libLighten = ['color', 'amount'];
6726
    protected function libLighten($args)
6727
    {
6728
        $color = $this->assertColor($args[0]);
6729
        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
6730
6731
        return $this->adjustHsl($color, 3, $amount);
6732
    }
6733
6734
    protected static $libDarken = ['color', 'amount'];
6735
    protected function libDarken($args)
6736
    {
6737
        $color = $this->assertColor($args[0]);
6738
        $amount = Util::checkRange('amount', new Range(0, 100), $args[1], '%');
6739
6740
        return $this->adjustHsl($color, 3, -$amount);
6741
    }
6742
6743
    protected static $libSaturate = [['color', 'amount'], ['amount']];
6744
    protected function libSaturate($args)
6745
    {
6746
        $value = $args[0];
6747
6748
        if ($value[0] === Type::T_NUMBER) {
6749
            return null;
6750
        }
6751
6752
        if (count($args) === 1) {
6753
            $val = $this->compileValue($value);
6754
            throw $this->error("\$amount: $val is not a number");
6755
        }
6756
6757
        $color = $this->assertColor($value);
6758
        $amount = 100 * $this->coercePercent($args[1]);
6759
6760
        return $this->adjustHsl($color, 2, $amount);
6761
    }
6762
6763
    protected static $libDesaturate = ['color', 'amount'];
6764
    protected function libDesaturate($args)
6765
    {
6766
        $color = $this->assertColor($args[0]);
6767
        $amount = 100 * $this->coercePercent($args[1]);
6768
6769
        return $this->adjustHsl($color, 2, -$amount);
6770
    }
6771
6772
    protected static $libGrayscale = ['color'];
6773
    protected function libGrayscale($args)
6774
    {
6775
        $value = $args[0];
6776
6777
        if ($value[0] === Type::T_NUMBER) {
6778
            return null;
6779
        }
6780
6781
        return $this->adjustHsl($this->assertColor($value), 2, -100);
6782
    }
6783
6784
    protected static $libComplement = ['color'];
6785
    protected function libComplement($args)
6786
    {
6787
        return $this->adjustHsl($this->assertColor($args[0]), 1, 180);
6788
    }
6789
6790
    protected static $libInvert = ['color', 'weight:1'];
6791
    protected function libInvert($args)
6792
    {
6793
        list($value, $weight) = $args;
6794
6795
        if (! isset($weight)) {
6796
            $weight = 1;
6797
        } else {
6798
            $weight = $this->coercePercent($weight);
6799
        }
6800
6801
        if ($value[0] === Type::T_NUMBER) {
6802
            return null;
6803
        }
6804
6805
        $color = $this->assertColor($value);
6806
        $inverted = $color;
6807
        $inverted[1] = 255 - $inverted[1];
6808
        $inverted[2] = 255 - $inverted[2];
6809
        $inverted[3] = 255 - $inverted[3];
6810
6811
        if ($weight < 1) {
6812
            return $this->libMix([$inverted, $color, [Type::T_NUMBER, $weight]]);
6813
        }
6814
6815
        return $inverted;
6816
    }
6817
6818
    // increases opacity by amount
6819
    protected static $libOpacify = ['color', 'amount'];
6820
    protected function libOpacify($args)
6821
    {
6822
        $color = $this->assertColor($args[0]);
6823
        $amount = $this->coercePercent($args[1]);
6824
6825
        $color[4] = (isset($color[4]) ? $color[4] : 1) + $amount;
6826
        $color[4] = min(1, max(0, $color[4]));
6827
6828
        return $color;
6829
    }
6830
6831
    protected static $libFadeIn = ['color', 'amount'];
6832
    protected function libFadeIn($args)
6833
    {
6834
        return $this->libOpacify($args);
6835
    }
6836
6837
    // decreases opacity by amount
6838
    protected static $libTransparentize = ['color', 'amount'];
6839
    protected function libTransparentize($args)
6840
    {
6841
        $color = $this->assertColor($args[0]);
6842
        $amount = $this->coercePercent($args[1]);
6843
6844
        $color[4] = (isset($color[4]) ? $color[4] : 1) - $amount;
6845
        $color[4] = min(1, max(0, $color[4]));
6846
6847
        return $color;
6848
    }
6849
6850
    protected static $libFadeOut = ['color', 'amount'];
6851
    protected function libFadeOut($args)
6852
    {
6853
        return $this->libTransparentize($args);
6854
    }
6855
6856
    protected static $libUnquote = ['string'];
6857
    protected function libUnquote($args)
6858
    {
6859
        $str = $args[0];
6860
6861
        if ($str[0] === Type::T_STRING) {
6862
            $str[1] = '';
6863
        }
6864
6865
        return $str;
6866
    }
6867
6868
    protected static $libQuote = ['string'];
6869
    protected function libQuote($args)
6870
    {
6871
        $value = $args[0];
6872
6873
        if ($value[0] === Type::T_STRING && ! empty($value[1])) {
6874
            return $value;
6875
        }
6876
6877
        return [Type::T_STRING, '"', [$value]];
6878
    }
6879
6880
    protected static $libPercentage = ['number'];
6881
    protected function libPercentage($args)
6882
    {
6883
        return new Node\Number($this->coercePercent($args[0]) * 100, '%');
6884
    }
6885
6886
    protected static $libRound = ['number'];
6887
    protected function libRound($args)
6888
    {
6889
        $num = $args[0];
6890
6891
        return new Node\Number(round($num[1]), $num[2]);
6892
    }
6893
6894
    protected static $libFloor = ['number'];
6895
    protected function libFloor($args)
6896
    {
6897
        $num = $args[0];
6898
6899
        return new Node\Number(floor($num[1]), $num[2]);
6900
    }
6901
6902
    protected static $libCeil = ['number'];
6903
    protected function libCeil($args)
6904
    {
6905
        $num = $args[0];
6906
6907
        return new Node\Number(ceil($num[1]), $num[2]);
6908
    }
6909
6910
    protected static $libAbs = ['number'];
6911
    protected function libAbs($args)
6912
    {
6913
        $num = $args[0];
6914
6915
        return new Node\Number(abs($num[1]), $num[2]);
6916
    }
6917
6918
    protected function libMin($args)
6919
    {
6920
        $numbers = $this->getNormalizedNumbers($args);
6921
        $minOriginal = null;
6922
        $minNormalized = null;
6923
6924
        foreach ($numbers as $key => $pair) {
6925
            list($original, $normalized) = $pair;
6926
6927
            if (\is_null($normalized) || \is_null($minNormalized)) {
6928
                if (\is_null($minOriginal) || $original[1] <= $minOriginal[1]) {
6929
                    $minOriginal = $original;
6930
                    $minNormalized = $normalized;
6931
                }
6932
            } elseif ($normalized[1] <= $minNormalized[1]) {
6933
                $minOriginal = $original;
6934
                $minNormalized = $normalized;
6935
            }
6936
        }
6937
6938
        return $minOriginal;
6939
    }
6940
6941
    protected function libMax($args)
6942
    {
6943
        $numbers = $this->getNormalizedNumbers($args);
6944
        $maxOriginal = null;
6945
        $maxNormalized = null;
6946
6947
        foreach ($numbers as $key => $pair) {
6948
            list($original, $normalized) = $pair;
6949
6950
            if (\is_null($normalized) || \is_null($maxNormalized)) {
6951
                if (\is_null($maxOriginal) || $original[1] >= $maxOriginal[1]) {
6952
                    $maxOriginal = $original;
6953
                    $maxNormalized = $normalized;
6954
                }
6955
            } elseif ($normalized[1] >= $maxNormalized[1]) {
6956
                $maxOriginal = $original;
6957
                $maxNormalized = $normalized;
6958
            }
6959
        }
6960
6961
        return $maxOriginal;
6962
    }
6963
6964
    /**
6965
     * Helper to normalize args containing numbers
6966
     *
6967
     * @param array $args
6968
     *
6969
     * @return array
6970
     */
6971
    protected function getNormalizedNumbers($args)
6972
    {
6973
        $unit         = null;
6974
        $originalUnit = null;
6975
        $numbers      = [];
6976
6977
        foreach ($args as $key => $item) {
6978
            if ($item[0] !== Type::T_NUMBER) {
6979
                throw $this->error('%s is not a number', $item[0]);
6980
            }
6981
6982
            $number = $item->normalize();
6983
6984
            if (empty($unit)) {
6985
                $unit = $number[2];
6986
                $originalUnit = $item->unitStr();
6987
            } elseif ($number[1] && $unit !== $number[2] && ! empty($number[2])) {
6988
                throw $this->error('Incompatible units: "%s" and "%s".', $originalUnit, $item->unitStr());
6989
            }
6990
6991
            $numbers[$key] = [$args[$key], empty($number[2]) ? null : $number];
6992
        }
6993
6994
        return $numbers;
6995
    }
6996
6997
    protected static $libLength = ['list'];
6998
    protected function libLength($args)
6999
    {
7000
        $list = $this->coerceList($args[0], ',', true);
7001
7002
        return \count($list[2]);
7003
    }
7004
7005
    //protected static $libListSeparator = ['list...'];
7006
    protected function libListSeparator($args)
7007
    {
7008
        if (\count($args) > 1) {
7009
            return 'comma';
7010
        }
7011
7012
        $list = $this->coerceList($args[0]);
7013
7014
        if (\count($list[2]) <= 1) {
7015
            return 'space';
7016
        }
7017
7018
        if ($list[1] === ',') {
7019
            return 'comma';
7020
        }
7021
7022
        return 'space';
7023
    }
7024
7025
    protected static $libNth = ['list', 'n'];
7026
    protected function libNth($args)
7027
    {
7028
        $list = $this->coerceList($args[0], ',', false);
7029
        $n = $this->assertNumber($args[1]);
7030
7031
        if ($n > 0) {
7032
            $n--;
7033
        } elseif ($n < 0) {
7034
            $n += \count($list[2]);
7035
        }
7036
7037
        return isset($list[2][$n]) ? $list[2][$n] : static::$defaultValue;
7038
    }
7039
7040
    protected static $libSetNth = ['list', 'n', 'value'];
7041
    protected function libSetNth($args)
7042
    {
7043
        $list = $this->coerceList($args[0]);
7044
        $n = $this->assertNumber($args[1]);
7045
7046
        if ($n > 0) {
7047
            $n--;
7048
        } elseif ($n < 0) {
7049
            $n += \count($list[2]);
7050
        }
7051
7052
        if (! isset($list[2][$n])) {
7053
            throw $this->error('Invalid argument for "n"');
7054
        }
7055
7056
        $list[2][$n] = $args[2];
7057
7058
        return $list;
7059
    }
7060
7061
    protected static $libMapGet = ['map', 'key'];
7062
    protected function libMapGet($args)
7063
    {
7064
        $map = $this->assertMap($args[0]);
7065
        $key = $args[1];
7066
7067
        if (! \is_null($key)) {
7068
            $key = $this->compileStringContent($this->coerceString($key));
7069
7070
            for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
7071
                if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
7072
                    return $map[2][$i];
7073
                }
7074
            }
7075
        }
7076
7077
        return static::$null;
7078
    }
7079
7080
    protected static $libMapKeys = ['map'];
7081
    protected function libMapKeys($args)
7082
    {
7083
        $map = $this->assertMap($args[0]);
7084
        $keys = $map[1];
7085
7086
        return [Type::T_LIST, ',', $keys];
7087
    }
7088
7089
    protected static $libMapValues = ['map'];
7090
    protected function libMapValues($args)
7091
    {
7092
        $map = $this->assertMap($args[0]);
7093
        $values = $map[2];
7094
7095
        return [Type::T_LIST, ',', $values];
7096
    }
7097
7098
    protected static $libMapRemove = ['map', 'key...'];
7099
    protected function libMapRemove($args)
7100
    {
7101
        $map = $this->assertMap($args[0]);
7102
        $keyList = $this->assertList($args[1]);
7103
7104
        $keys = [];
7105
7106
        foreach ($keyList[2] as $key) {
7107
            $keys[] = $this->compileStringContent($this->coerceString($key));
7108
        }
7109
7110
        for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
7111
            if (in_array($this->compileStringContent($this->coerceString($map[1][$i])), $keys)) {
7112
                array_splice($map[1], $i, 1);
7113
                array_splice($map[2], $i, 1);
7114
            }
7115
        }
7116
7117
        return $map;
7118
    }
7119
7120
    protected static $libMapHasKey = ['map', 'key'];
7121
    protected function libMapHasKey($args)
7122
    {
7123
        $map = $this->assertMap($args[0]);
7124
        $key = $this->compileStringContent($this->coerceString($args[1]));
7125
7126
        for ($i = \count($map[1]) - 1; $i >= 0; $i--) {
7127
            if ($key === $this->compileStringContent($this->coerceString($map[1][$i]))) {
7128
                return true;
7129
            }
7130
        }
7131
7132
        return false;
7133
    }
7134
7135
    protected static $libMapMerge = [
7136
        ['map1', 'map2'],
7137
        ['map-1', 'map-2']
7138
    ];
7139
    protected function libMapMerge($args)
7140
    {
7141
        $map1 = $this->assertMap($args[0]);
7142
        $map2 = $this->assertMap($args[1]);
7143
7144
        foreach ($map2[1] as $i2 => $key2) {
7145
            $key = $this->compileStringContent($this->coerceString($key2));
7146
7147
            foreach ($map1[1] as $i1 => $key1) {
7148
                if ($key === $this->compileStringContent($this->coerceString($key1))) {
7149
                    $map1[2][$i1] = $map2[2][$i2];
7150
                    continue 2;
7151
                }
7152
            }
7153
7154
            $map1[1][] = $map2[1][$i2];
7155
            $map1[2][] = $map2[2][$i2];
7156
        }
7157
7158
        return $map1;
7159
    }
7160
7161
    protected static $libKeywords = ['args'];
7162
    protected function libKeywords($args)
7163
    {
7164
        $this->assertList($args[0]);
7165
7166
        $keys = [];
7167
        $values = [];
7168
7169
        foreach ($args[0][2] as $name => $arg) {
7170
            $keys[] = [Type::T_KEYWORD, $name];
7171
            $values[] = $arg;
7172
        }
7173
7174
        return [Type::T_MAP, $keys, $values];
7175
    }
7176
7177
    protected static $libIsBracketed = ['list'];
7178
    protected function libIsBracketed($args)
7179
    {
7180
        $list = $args[0];
7181
        $this->coerceList($list, ' ');
7182
7183
        if (! empty($list['enclosing']) && $list['enclosing'] === 'bracket') {
7184
            return true;
7185
        }
7186
7187
        return false;
7188
    }
7189
7190
    protected function listSeparatorForJoin($list1, $sep)
7191
    {
7192
        if (! isset($sep)) {
7193
            return $list1[1];
7194
        }
7195
7196
        switch ($this->compileValue($sep)) {
7197
            case 'comma':
7198
                return ',';
7199
7200
            case 'space':
7201
                return ' ';
7202
7203
            default:
7204
                return $list1[1];
7205
        }
7206
    }
7207
7208
    protected static $libJoin = ['list1', 'list2', 'separator:null', 'bracketed:auto'];
7209
    protected function libJoin($args)
7210
    {
7211
        list($list1, $list2, $sep, $bracketed) = $args;
7212
7213
        $list1 = $this->coerceList($list1, ' ', true);
7214
        $list2 = $this->coerceList($list2, ' ', true);
7215
        $sep   = $this->listSeparatorForJoin($list1, $sep);
7216
7217
        if ($bracketed === static::$true) {
7218
            $bracketed = true;
7219
        } elseif ($bracketed === static::$false) {
7220
            $bracketed = false;
7221
        } elseif ($bracketed === [Type::T_KEYWORD, 'auto']) {
7222
            $bracketed = 'auto';
7223
        } elseif ($bracketed === static::$null) {
7224
            $bracketed = false;
7225
        } else {
7226
            $bracketed = $this->compileValue($bracketed);
7227
            $bracketed = ! ! $bracketed;
7228
7229
            if ($bracketed === true) {
7230
                $bracketed = true;
7231
            }
7232
        }
7233
7234
        if ($bracketed === 'auto') {
7235
            $bracketed = false;
7236
7237
            if (! empty($list1['enclosing']) && $list1['enclosing'] === 'bracket') {
7238
                $bracketed = true;
7239
            }
7240
        }
7241
7242
        $res = [Type::T_LIST, $sep, array_merge($list1[2], $list2[2])];
7243
7244
        if (isset($list1['enclosing'])) {
7245
            $res['enlcosing'] = $list1['enclosing'];
7246
        }
7247
7248
        if ($bracketed) {
7249
            $res['enclosing'] = 'bracket';
7250
        }
7251
7252
        return $res;
7253
    }
7254
7255
    protected static $libAppend = ['list', 'val', 'separator:null'];
7256
    protected function libAppend($args)
7257
    {
7258
        list($list1, $value, $sep) = $args;
7259
7260
        $list1 = $this->coerceList($list1, ' ', true);
7261
        $sep   = $this->listSeparatorForJoin($list1, $sep);
7262
        $res   = [Type::T_LIST, $sep, array_merge($list1[2], [$value])];
7263
7264
        if (isset($list1['enclosing'])) {
7265
            $res['enclosing'] = $list1['enclosing'];
7266
        }
7267
7268
        return $res;
7269
    }
7270
7271
    protected function libZip($args)
7272
    {
7273
        foreach ($args as $key => $arg) {
7274
            $args[$key] = $this->coerceList($arg);
7275
        }
7276
7277
        $lists = [];
7278
        $firstList = array_shift($args);
7279
7280
        foreach ($firstList[2] as $key => $item) {
7281
            $list = [Type::T_LIST, '', [$item]];
7282
7283
            foreach ($args as $arg) {
7284
                if (isset($arg[2][$key])) {
7285
                    $list[2][] = $arg[2][$key];
7286
                } else {
7287
                    break 2;
7288
                }
7289
            }
7290
7291
            $lists[] = $list;
7292
        }
7293
7294
        return [Type::T_LIST, ',', $lists];
7295
    }
7296
7297
    protected static $libTypeOf = ['value'];
7298
    protected function libTypeOf($args)
7299
    {
7300
        $value = $args[0];
7301
7302
        switch ($value[0]) {
7303
            case Type::T_KEYWORD:
7304
                if ($value === static::$true || $value === static::$false) {
7305
                    return 'bool';
7306
                }
7307
7308
                if ($this->coerceColor($value)) {
7309
                    return 'color';
7310
                }
7311
7312
                // fall-thru
7313
            case Type::T_FUNCTION:
7314
                return 'string';
7315
7316
            case Type::T_FUNCTION_REFERENCE:
7317
                return 'function';
7318
7319
            case Type::T_LIST:
7320
                if (isset($value[3]) && $value[3]) {
7321
                    return 'arglist';
7322
                }
7323
7324
                // fall-thru
7325
            default:
7326
                return $value[0];
7327
        }
7328
    }
7329
7330
    protected static $libUnit = ['number'];
7331
    protected function libUnit($args)
7332
    {
7333
        $num = $args[0];
7334
7335
        if ($num[0] === Type::T_NUMBER) {
7336
            return [Type::T_STRING, '"', [$num->unitStr()]];
7337
        }
7338
7339
        return '';
7340
    }
7341
7342
    protected static $libUnitless = ['number'];
7343
    protected function libUnitless($args)
7344
    {
7345
        $value = $args[0];
7346
7347
        return $value[0] === Type::T_NUMBER && $value->unitless();
7348
    }
7349
7350
    protected static $libComparable = [
7351
        ['number1', 'number2'],
7352
        ['number-1', 'number-2']
7353
    ];
7354
    protected function libComparable($args)
7355
    {
7356
        list($number1, $number2) = $args;
7357
7358
        if (
7359
            ! isset($number1[0]) || $number1[0] !== Type::T_NUMBER ||
7360
            ! isset($number2[0]) || $number2[0] !== Type::T_NUMBER
7361
        ) {
7362
            throw $this->error('Invalid argument(s) for "comparable"');
7363
        }
7364
7365
        $number1 = $number1->normalize();
7366
        $number2 = $number2->normalize();
7367
7368
        return $number1[2] === $number2[2] || $number1->unitless() || $number2->unitless();
7369
    }
7370
7371
    protected static $libStrIndex = ['string', 'substring'];
7372
    protected function libStrIndex($args)
7373
    {
7374
        $string = $this->coerceString($args[0]);
7375
        $stringContent = $this->compileStringContent($string);
7376
7377
        $substring = $this->coerceString($args[1]);
7378
        $substringContent = $this->compileStringContent($substring);
7379
7380
        $result = strpos($stringContent, $substringContent);
7381
7382
        return $result === false ? static::$null : new Node\Number($result + 1, '');
7383
    }
7384
7385
    protected static $libStrInsert = ['string', 'insert', 'index'];
7386
    protected function libStrInsert($args)
7387
    {
7388
        $string = $this->coerceString($args[0]);
7389
        $stringContent = $this->compileStringContent($string);
7390
7391
        $insert = $this->coerceString($args[1]);
7392
        $insertContent = $this->compileStringContent($insert);
7393
7394
        list(, $index) = $args[2];
7395
7396
        $string[2] = [substr_replace($stringContent, $insertContent, $index - 1, 0)];
7397
7398
        return $string;
7399
    }
7400
7401
    protected static $libStrLength = ['string'];
7402
    protected function libStrLength($args)
7403
    {
7404
        $string = $this->coerceString($args[0]);
7405
        $stringContent = $this->compileStringContent($string);
7406
7407
        return new Node\Number(\strlen($stringContent), '');
7408
    }
7409
7410
    protected static $libStrSlice = ['string', 'start-at', 'end-at:-1'];
7411
    protected function libStrSlice($args)
7412
    {
7413
        if (isset($args[2]) && ! $args[2][1]) {
7414
            return static::$nullString;
7415
        }
7416
7417
        $string = $this->coerceString($args[0]);
7418
        $stringContent = $this->compileStringContent($string);
7419
7420
        $start = (int) $args[1][1];
7421
7422
        if ($start > 0) {
7423
            $start--;
7424
        }
7425
7426
        $end    = isset($args[2]) ? (int) $args[2][1] : -1;
7427
        $length = $end < 0 ? $end + 1 : ($end > 0 ? $end - $start : $end);
7428
7429
        $string[2] = $length
7430
            ? [substr($stringContent, $start, $length)]
7431
            : [substr($stringContent, $start)];
7432
7433
        return $string;
7434
    }
7435
7436
    protected static $libToLowerCase = ['string'];
7437
    protected function libToLowerCase($args)
7438
    {
7439
        $string = $this->coerceString($args[0]);
7440
        $stringContent = $this->compileStringContent($string);
7441
7442
        $string[2] = [\function_exists('mb_strtolower') ? mb_strtolower($stringContent) : strtolower($stringContent)];
7443
7444
        return $string;
7445
    }
7446
7447
    protected static $libToUpperCase = ['string'];
7448
    protected function libToUpperCase($args)
7449
    {
7450
        $string = $this->coerceString($args[0]);
7451
        $stringContent = $this->compileStringContent($string);
7452
7453
        $string[2] = [\function_exists('mb_strtoupper') ? mb_strtoupper($stringContent) : strtoupper($stringContent)];
7454
7455
        return $string;
7456
    }
7457
7458
    protected static $libFeatureExists = ['feature'];
7459
    protected function libFeatureExists($args)
7460
    {
7461
        $string = $this->coerceString($args[0]);
7462
        $name = $this->compileStringContent($string);
7463
7464
        return $this->toBool(
7465
            \array_key_exists($name, $this->registeredFeatures) ? $this->registeredFeatures[$name] : false
7466
        );
7467
    }
7468
7469
    protected static $libFunctionExists = ['name'];
7470
    protected function libFunctionExists($args)
7471
    {
7472
        $string = $this->coerceString($args[0]);
7473
        $name = $this->compileStringContent($string);
7474
7475
        // user defined functions
7476
        if ($this->has(static::$namespaces['function'] . $name)) {
7477
            return true;
7478
        }
7479
7480
        $name = $this->normalizeName($name);
7481
7482
        if (isset($this->userFunctions[$name])) {
7483
            return true;
7484
        }
7485
7486
        // built-in functions
7487
        $f = $this->getBuiltinFunction($name);
7488
7489
        return $this->toBool(\is_callable($f));
7490
    }
7491
7492
    protected static $libGlobalVariableExists = ['name'];
7493
    protected function libGlobalVariableExists($args)
7494
    {
7495
        $string = $this->coerceString($args[0]);
7496
        $name = $this->compileStringContent($string);
7497
7498
        return $this->has($name, $this->rootEnv);
7499
    }
7500
7501
    protected static $libMixinExists = ['name'];
7502
    protected function libMixinExists($args)
7503
    {
7504
        $string = $this->coerceString($args[0]);
7505
        $name = $this->compileStringContent($string);
7506
7507
        return $this->has(static::$namespaces['mixin'] . $name);
7508
    }
7509
7510
    protected static $libVariableExists = ['name'];
7511
    protected function libVariableExists($args)
7512
    {
7513
        $string = $this->coerceString($args[0]);
7514
        $name = $this->compileStringContent($string);
7515
7516
        return $this->has($name);
7517
    }
7518
7519
    /**
7520
     * Workaround IE7's content counter bug.
7521
     *
7522
     * @param array $args
7523
     *
7524
     * @return array
7525
     */
7526
    protected function libCounter($args)
7527
    {
7528
        $list = array_map([$this, 'compileValue'], $args);
7529
7530
        return [Type::T_STRING, '', ['counter(' . implode(',', $list) . ')']];
7531
    }
7532
7533
    protected static $libRandom = ['limit:1'];
7534
    protected function libRandom($args)
7535
    {
7536
        if (isset($args[0])) {
7537
            $n = $this->assertNumber($args[0]);
7538
7539
            if ($n < 1) {
7540
                throw $this->error("\$limit must be greater than or equal to 1");
7541
            }
7542
7543
            if ($n - \intval($n) > 0) {
7544
                throw $this->error("Expected \$limit to be an integer but got $n for `random`");
7545
            }
7546
7547
            return new Node\Number(mt_rand(1, \intval($n)), '');
7548
        }
7549
7550
        return new Node\Number(mt_rand(1, mt_getrandmax()), '');
7551
    }
7552
7553
    protected function libUniqueId()
7554
    {
7555
        static $id;
7556
7557
        if (! isset($id)) {
7558
            $id = PHP_INT_SIZE === 4
7559
                ? mt_rand(0, pow(36, 5)) . str_pad(mt_rand(0, pow(36, 5)) % 10000000, 7, '0', STR_PAD_LEFT)
7560
                : mt_rand(0, pow(36, 8));
7561
        }
7562
7563
        $id += mt_rand(0, 10) + 1;
7564
7565
        return [Type::T_STRING, '', ['u' . str_pad(base_convert($id, 10, 36), 8, '0', STR_PAD_LEFT)]];
7566
    }
7567
7568
    protected function inspectFormatValue($value, $force_enclosing_display = false)
7569
    {
7570
        if ($value === static::$null) {
7571
            $value = [Type::T_KEYWORD, 'null'];
7572
        }
7573
7574
        $stringValue = [$value];
7575
7576
        if ($value[0] === Type::T_LIST) {
7577
            if (end($value[2]) === static::$null) {
7578
                array_pop($value[2]);
7579
                $value[2][] = [Type::T_STRING, '', ['']];
7580
                $force_enclosing_display = true;
7581
            }
7582
7583
            if (
7584
                ! empty($value['enclosing']) &&
7585
                ($force_enclosing_display ||
7586
                    ($value['enclosing'] === 'bracket') ||
7587
                    ! \count($value[2]))
7588
            ) {
7589
                $value['enclosing'] = 'forced_' . $value['enclosing'];
7590
                $force_enclosing_display = true;
7591
            }
7592
7593
            foreach ($value[2] as $k => $listelement) {
7594
                $value[2][$k] = $this->inspectFormatValue($listelement, $force_enclosing_display);
7595
            }
7596
7597
            $stringValue = [$value];
7598
        }
7599
7600
        return [Type::T_STRING, '', $stringValue];
7601
    }
7602
7603
    protected static $libInspect = ['value'];
7604
    protected function libInspect($args)
7605
    {
7606
        $value = $args[0];
7607
7608
        return $this->inspectFormatValue($value);
7609
    }
7610
7611
    /**
7612
     * Preprocess selector args
7613
     *
7614
     * @param array $arg
7615
     *
7616
     * @return array|boolean
7617
     */
7618
    protected function getSelectorArg($arg)
7619
    {
7620
        static $parser = null;
7621
7622
        if (\is_null($parser)) {
7623
            $parser = $this->parserFactory(__METHOD__);
7624
        }
7625
7626
        $arg = $this->libUnquote([$arg]);
7627
        $arg = $this->compileValue($arg);
7628
7629
        $parsedSelector = [];
7630
7631
        if ($parser->parseSelector($arg, $parsedSelector)) {
7632
            $selector = $this->evalSelectors($parsedSelector);
7633
            $gluedSelector = $this->glueFunctionSelectors($selector);
7634
7635
            return $gluedSelector;
7636
        }
7637
7638
        return false;
7639
    }
7640
7641
    /**
7642
     * Postprocess selector to output in right format
7643
     *
7644
     * @param array $selectors
7645
     *
7646
     * @return string
7647
     */
7648
    protected function formatOutputSelector($selectors)
7649
    {
7650
        $selectors = $this->collapseSelectors($selectors, true);
7651
7652
        return $selectors;
7653
    }
7654
7655
    protected static $libIsSuperselector = ['super', 'sub'];
7656
    protected function libIsSuperselector($args)
7657
    {
7658
        list($super, $sub) = $args;
7659
7660
        $super = $this->getSelectorArg($super);
7661
        $sub = $this->getSelectorArg($sub);
7662
7663
        return $this->isSuperSelector($super, $sub);
0 ignored issues
show
Bug introduced by
It seems like $sub can also be of type false; however, parameter $sub of ScssPhp\ScssPhp\Compiler::isSuperSelector() 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

7663
        return $this->isSuperSelector($super, /** @scrutinizer ignore-type */ $sub);
Loading history...
Bug introduced by
It seems like $super can also be of type false; however, parameter $super of ScssPhp\ScssPhp\Compiler::isSuperSelector() 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

7663
        return $this->isSuperSelector(/** @scrutinizer ignore-type */ $super, $sub);
Loading history...
7664
    }
7665
7666
    /**
7667
     * Test a $super selector again $sub
7668
     *
7669
     * @param array $super
7670
     * @param array $sub
7671
     *
7672
     * @return boolean
7673
     */
7674
    protected function isSuperSelector($super, $sub)
7675
    {
7676
        // one and only one selector for each arg
7677
        if (! $super || \count($super) !== 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $super 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...
7678
            throw $this->error('Invalid super selector for isSuperSelector()');
7679
        }
7680
7681
        if (! $sub || \count($sub) !== 1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $sub 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...
7682
            throw $this->error('Invalid sub selector for isSuperSelector()');
7683
        }
7684
7685
        $super = reset($super);
7686
        $sub = reset($sub);
7687
7688
        $i = 0;
7689
        $nextMustMatch = false;
7690
7691
        foreach ($super as $node) {
7692
            $compound = '';
7693
7694
            array_walk_recursive(
7695
                $node,
7696
                function ($value, $key) use (&$compound) {
7697
                    $compound .= $value;
7698
                }
7699
            );
7700
7701
            if ($this->isImmediateRelationshipCombinator($compound)) {
7702
                if ($node !== $sub[$i]) {
7703
                    return false;
7704
                }
7705
7706
                $nextMustMatch = true;
7707
                $i++;
7708
            } else {
7709
                while ($i < \count($sub) && ! $this->isSuperPart($node, $sub[$i])) {
7710
                    if ($nextMustMatch) {
7711
                        return false;
7712
                    }
7713
7714
                    $i++;
7715
                }
7716
7717
                if ($i >= \count($sub)) {
7718
                    return false;
7719
                }
7720
7721
                $nextMustMatch = false;
7722
                $i++;
7723
            }
7724
        }
7725
7726
        return true;
7727
    }
7728
7729
    /**
7730
     * Test a part of super selector again a part of sub selector
7731
     *
7732
     * @param array $superParts
7733
     * @param array $subParts
7734
     *
7735
     * @return boolean
7736
     */
7737
    protected function isSuperPart($superParts, $subParts)
7738
    {
7739
        $i = 0;
7740
7741
        foreach ($superParts as $superPart) {
7742
            while ($i < \count($subParts) && $subParts[$i] !== $superPart) {
7743
                $i++;
7744
            }
7745
7746
            if ($i >= \count($subParts)) {
7747
                return false;
7748
            }
7749
7750
            $i++;
7751
        }
7752
7753
        return true;
7754
    }
7755
7756
    protected static $libSelectorAppend = ['selector...'];
7757
    protected function libSelectorAppend($args)
7758
    {
7759
        // get the selector... list
7760
        $args = reset($args);
7761
        $args = $args[2];
7762
7763
        if (\count($args) < 1) {
7764
            throw $this->error('selector-append() needs at least 1 argument');
7765
        }
7766
7767
        $selectors = array_map([$this, 'getSelectorArg'], $args);
7768
7769
        return $this->formatOutputSelector($this->selectorAppend($selectors));
7770
    }
7771
7772
    /**
7773
     * Append parts of the last selector in the list to the previous, recursively
7774
     *
7775
     * @param array $selectors
7776
     *
7777
     * @return array
7778
     *
7779
     * @throws \ScssPhp\ScssPhp\Exception\CompilerException
7780
     */
7781
    protected function selectorAppend($selectors)
7782
    {
7783
        $lastSelectors = array_pop($selectors);
7784
7785
        if (! $lastSelectors) {
7786
            throw $this->error('Invalid selector list in selector-append()');
7787
        }
7788
7789
        while (\count($selectors)) {
7790
            $previousSelectors = array_pop($selectors);
7791
7792
            if (! $previousSelectors) {
7793
                throw $this->error('Invalid selector list in selector-append()');
7794
            }
7795
7796
            // do the trick, happening $lastSelector to $previousSelector
7797
            $appended = [];
7798
7799
            foreach ($lastSelectors as $lastSelector) {
7800
                $previous = $previousSelectors;
7801
7802
                foreach ($lastSelector as $lastSelectorParts) {
7803
                    foreach ($lastSelectorParts as $lastSelectorPart) {
7804
                        foreach ($previous as $i => $previousSelector) {
7805
                            foreach ($previousSelector as $j => $previousSelectorParts) {
7806
                                $previous[$i][$j][] = $lastSelectorPart;
7807
                            }
7808
                        }
7809
                    }
7810
                }
7811
7812
                foreach ($previous as $ps) {
7813
                    $appended[] = $ps;
7814
                }
7815
            }
7816
7817
            $lastSelectors = $appended;
7818
        }
7819
7820
        return $lastSelectors;
7821
    }
7822
7823
    protected static $libSelectorExtend = [
7824
        ['selector', 'extendee', 'extender'],
7825
        ['selectors', 'extendee', 'extender']
7826
    ];
7827
    protected function libSelectorExtend($args)
7828
    {
7829
        list($selectors, $extendee, $extender) = $args;
7830
7831
        $selectors = $this->getSelectorArg($selectors);
7832
        $extendee  = $this->getSelectorArg($extendee);
7833
        $extender  = $this->getSelectorArg($extender);
7834
7835
        if (! $selectors || ! $extendee || ! $extender) {
7836
            throw $this->error('selector-extend() invalid arguments');
7837
        }
7838
7839
        $extended = $this->extendOrReplaceSelectors($selectors, $extendee, $extender);
7840
7841
        return $this->formatOutputSelector($extended);
7842
    }
7843
7844
    protected static $libSelectorReplace = [
7845
        ['selector', 'original', 'replacement'],
7846
        ['selectors', 'original', 'replacement']
7847
    ];
7848
    protected function libSelectorReplace($args)
7849
    {
7850
        list($selectors, $original, $replacement) = $args;
7851
7852
        $selectors   = $this->getSelectorArg($selectors);
7853
        $original    = $this->getSelectorArg($original);
7854
        $replacement = $this->getSelectorArg($replacement);
7855
7856
        if (! $selectors || ! $original || ! $replacement) {
7857
            throw $this->error('selector-replace() invalid arguments');
7858
        }
7859
7860
        $replaced = $this->extendOrReplaceSelectors($selectors, $original, $replacement, true);
7861
7862
        return $this->formatOutputSelector($replaced);
7863
    }
7864
7865
    /**
7866
     * Extend/replace in selectors
7867
     * used by selector-extend and selector-replace that use the same logic
7868
     *
7869
     * @param array   $selectors
7870
     * @param array   $extendee
7871
     * @param array   $extender
7872
     * @param boolean $replace
7873
     *
7874
     * @return array
7875
     */
7876
    protected function extendOrReplaceSelectors($selectors, $extendee, $extender, $replace = false)
7877
    {
7878
        $saveExtends = $this->extends;
7879
        $saveExtendsMap = $this->extendsMap;
7880
7881
        $this->extends = [];
7882
        $this->extendsMap = [];
7883
7884
        foreach ($extendee as $es) {
7885
            // only use the first one
7886
            $this->pushExtends(reset($es), $extender, null);
7887
        }
7888
7889
        $extended = [];
7890
7891
        foreach ($selectors as $selector) {
7892
            if (! $replace) {
7893
                $extended[] = $selector;
7894
            }
7895
7896
            $n = \count($extended);
7897
7898
            $this->matchExtends($selector, $extended);
7899
7900
            // if didnt match, keep the original selector if we are in a replace operation
7901
            if ($replace && \count($extended) === $n) {
7902
                $extended[] = $selector;
7903
            }
7904
        }
7905
7906
        $this->extends = $saveExtends;
7907
        $this->extendsMap = $saveExtendsMap;
7908
7909
        return $extended;
7910
    }
7911
7912
    protected static $libSelectorNest = ['selector...'];
7913
    protected function libSelectorNest($args)
7914
    {
7915
        // get the selector... list
7916
        $args = reset($args);
7917
        $args = $args[2];
7918
7919
        if (\count($args) < 1) {
7920
            throw $this->error('selector-nest() needs at least 1 argument');
7921
        }
7922
7923
        $selectorsMap = array_map([$this, 'getSelectorArg'], $args);
7924
        $envs = [];
7925
7926
        foreach ($selectorsMap as $selectors) {
7927
            $env = new Environment();
7928
            $env->selectors = $selectors;
0 ignored issues
show
Bug introduced by
The property selectors does not seem to exist on ScssPhp\ScssPhp\Compiler\Environment.
Loading history...
7929
7930
            $envs[] = $env;
7931
        }
7932
7933
        $envs            = array_reverse($envs);
7934
        $env             = $this->extractEnv($envs);
7935
        $outputSelectors = $this->multiplySelectors($env);
7936
7937
        return $this->formatOutputSelector($outputSelectors);
7938
    }
7939
7940
    protected static $libSelectorParse = [
7941
        ['selector'],
7942
        ['selectors']
7943
    ];
7944
    protected function libSelectorParse($args)
7945
    {
7946
        $selectors = reset($args);
7947
        $selectors = $this->getSelectorArg($selectors);
7948
7949
        return $this->formatOutputSelector($selectors);
0 ignored issues
show
Bug introduced by
It seems like $selectors can also be of type false; however, parameter $selectors of ScssPhp\ScssPhp\Compiler::formatOutputSelector() 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

7949
        return $this->formatOutputSelector(/** @scrutinizer ignore-type */ $selectors);
Loading history...
7950
    }
7951
7952
    protected static $libSelectorUnify = ['selectors1', 'selectors2'];
7953
    protected function libSelectorUnify($args)
7954
    {
7955
        list($selectors1, $selectors2) = $args;
7956
7957
        $selectors1 = $this->getSelectorArg($selectors1);
7958
        $selectors2 = $this->getSelectorArg($selectors2);
7959
7960
        if (! $selectors1 || ! $selectors2) {
7961
            throw $this->error('selector-unify() invalid arguments');
7962
        }
7963
7964
        // only consider the first compound of each
7965
        $compound1 = reset($selectors1);
7966
        $compound2 = reset($selectors2);
7967
7968
        // unify them and that's it
7969
        $unified = $this->unifyCompoundSelectors($compound1, $compound2);
7970
7971
        return $this->formatOutputSelector($unified);
7972
    }
7973
7974
    /**
7975
     * The selector-unify magic as its best
7976
     * (at least works as expected on test cases)
7977
     *
7978
     * @param array $compound1
7979
     * @param array $compound2
7980
     *
7981
     * @return array|mixed
7982
     */
7983
    protected function unifyCompoundSelectors($compound1, $compound2)
7984
    {
7985
        if (! \count($compound1)) {
7986
            return $compound2;
7987
        }
7988
7989
        if (! \count($compound2)) {
7990
            return $compound1;
7991
        }
7992
7993
        // check that last part are compatible
7994
        $lastPart1 = array_pop($compound1);
7995
        $lastPart2 = array_pop($compound2);
7996
        $last      = $this->mergeParts($lastPart1, $lastPart2);
7997
7998
        if (! $last) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $last 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...
7999
            return [[]];
8000
        }
8001
8002
        $unifiedCompound = [$last];
8003
        $unifiedSelectors = [$unifiedCompound];
8004
8005
        // do the rest
8006
        while (\count($compound1) || \count($compound2)) {
8007
            $part1 = end($compound1);
8008
            $part2 = end($compound2);
8009
8010
            if ($part1 && ($match2 = $this->matchPartInCompound($part1, $compound2))) {
8011
                list($compound2, $part2, $after2) = $match2;
8012
8013
                if ($after2) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $after2 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...
8014
                    $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after2);
8015
                }
8016
8017
                $c = $this->mergeParts($part1, $part2);
8018
                $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
8019
8020
                $part1 = $part2 = null;
8021
8022
                array_pop($compound1);
8023
            }
8024
8025
            if ($part2 && ($match1 = $this->matchPartInCompound($part2, $compound1))) {
8026
                list($compound1, $part1, $after1) = $match1;
8027
8028
                if ($after1) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $after1 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...
8029
                    $unifiedSelectors = $this->prependSelectors($unifiedSelectors, $after1);
8030
                }
8031
8032
                $c = $this->mergeParts($part2, $part1);
8033
                $unifiedSelectors = $this->prependSelectors($unifiedSelectors, [$c]);
8034
8035
                $part1 = $part2 = null;
8036
8037
                array_pop($compound2);
8038
            }
8039
8040
            $new = [];
8041
8042
            if ($part1 && $part2) {
8043
                array_pop($compound1);
8044
                array_pop($compound2);
8045
8046
                $s   = $this->prependSelectors($unifiedSelectors, [$part2]);
8047
                $new = array_merge($new, $this->prependSelectors($s, [$part1]));
8048
                $s   = $this->prependSelectors($unifiedSelectors, [$part1]);
8049
                $new = array_merge($new, $this->prependSelectors($s, [$part2]));
8050
            } elseif ($part1) {
8051
                array_pop($compound1);
8052
8053
                $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part1]));
8054
            } elseif ($part2) {
8055
                array_pop($compound2);
8056
8057
                $new = array_merge($new, $this->prependSelectors($unifiedSelectors, [$part2]));
8058
            }
8059
8060
            if ($new) {
8061
                $unifiedSelectors = $new;
8062
            }
8063
        }
8064
8065
        return $unifiedSelectors;
8066
    }
8067
8068
    /**
8069
     * Prepend each selector from $selectors with $parts
8070
     *
8071
     * @param array $selectors
8072
     * @param array $parts
8073
     *
8074
     * @return array
8075
     */
8076
    protected function prependSelectors($selectors, $parts)
8077
    {
8078
        $new = [];
8079
8080
        foreach ($selectors as $compoundSelector) {
8081
            array_unshift($compoundSelector, $parts);
8082
8083
            $new[] = $compoundSelector;
8084
        }
8085
8086
        return $new;
8087
    }
8088
8089
    /**
8090
     * Try to find a matching part in a compound:
8091
     * - with same html tag name
8092
     * - with some class or id or something in common
8093
     *
8094
     * @param array $part
8095
     * @param array $compound
8096
     *
8097
     * @return array|boolean
8098
     */
8099
    protected function matchPartInCompound($part, $compound)
8100
    {
8101
        $partTag = $this->findTagName($part);
8102
        $before  = $compound;
8103
        $after   = [];
8104
8105
        // try to find a match by tag name first
8106
        while (\count($before)) {
8107
            $p = array_pop($before);
8108
8109
            if ($partTag && $partTag !== '*' && $partTag == $this->findTagName($p)) {
8110
                return [$before, $p, $after];
8111
            }
8112
8113
            $after[] = $p;
8114
        }
8115
8116
        // try again matching a non empty intersection and a compatible tagname
8117
        $before = $compound;
8118
        $after = [];
8119
8120
        while (\count($before)) {
8121
            $p = array_pop($before);
8122
8123
            if ($this->checkCompatibleTags($partTag, $this->findTagName($p))) {
8124
                if (\count(array_intersect($part, $p))) {
8125
                    return [$before, $p, $after];
8126
                }
8127
            }
8128
8129
            $after[] = $p;
8130
        }
8131
8132
        return false;
8133
    }
8134
8135
    /**
8136
     * Merge two part list taking care that
8137
     * - the html tag is coming first - if any
8138
     * - the :something are coming last
8139
     *
8140
     * @param array $parts1
8141
     * @param array $parts2
8142
     *
8143
     * @return array
8144
     */
8145
    protected function mergeParts($parts1, $parts2)
8146
    {
8147
        $tag1 = $this->findTagName($parts1);
8148
        $tag2 = $this->findTagName($parts2);
8149
        $tag  = $this->checkCompatibleTags($tag1, $tag2);
8150
8151
        // not compatible tags
8152
        if ($tag === false) {
8153
            return [];
8154
        }
8155
8156
        if ($tag) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tag 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...
8157
            if ($tag1) {
8158
                $parts1 = array_diff($parts1, [$tag1]);
8159
            }
8160
8161
            if ($tag2) {
8162
                $parts2 = array_diff($parts2, [$tag2]);
8163
            }
8164
        }
8165
8166
        $mergedParts = array_merge($parts1, $parts2);
8167
        $mergedOrderedParts = [];
8168
8169
        foreach ($mergedParts as $part) {
8170
            if (strpos($part, ':') === 0) {
8171
                $mergedOrderedParts[] = $part;
8172
            }
8173
        }
8174
8175
        $mergedParts = array_diff($mergedParts, $mergedOrderedParts);
8176
        $mergedParts = array_merge($mergedParts, $mergedOrderedParts);
8177
8178
        if ($tag) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $tag 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...
8179
            array_unshift($mergedParts, $tag);
8180
        }
8181
8182
        return $mergedParts;
8183
    }
8184
8185
    /**
8186
     * Check the compatibility between two tag names:
8187
     * if both are defined they should be identical or one has to be '*'
8188
     *
8189
     * @param string $tag1
8190
     * @param string $tag2
8191
     *
8192
     * @return array|boolean
8193
     */
8194
    protected function checkCompatibleTags($tag1, $tag2)
8195
    {
8196
        $tags = [$tag1, $tag2];
8197
        $tags = array_unique($tags);
8198
        $tags = array_filter($tags);
8199
8200
        if (\count($tags) > 1) {
8201
            $tags = array_diff($tags, ['*']);
8202
        }
8203
8204
        // not compatible nodes
8205
        if (\count($tags) > 1) {
8206
            return false;
8207
        }
8208
8209
        return $tags;
8210
    }
8211
8212
    /**
8213
     * Find the html tag name in a selector parts list
8214
     *
8215
     * @param array $parts
8216
     *
8217
     * @return mixed|string
8218
     */
8219
    protected function findTagName($parts)
8220
    {
8221
        foreach ($parts as $part) {
8222
            if (! preg_match('/^[\[.:#%_-]/', $part)) {
8223
                return $part;
8224
            }
8225
        }
8226
8227
        return '';
8228
    }
8229
8230
    protected static $libSimpleSelectors = ['selector'];
8231
    protected function libSimpleSelectors($args)
8232
    {
8233
        $selector = reset($args);
8234
        $selector = $this->getSelectorArg($selector);
8235
8236
        // remove selectors list layer, keeping the first one
8237
        $selector = reset($selector);
0 ignored issues
show
Bug introduced by
It seems like $selector can also be of type false; however, parameter $array of reset() 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

8237
        $selector = reset(/** @scrutinizer ignore-type */ $selector);
Loading history...
8238
8239
        // remove parts list layer, keeping the first part
8240
        $part = reset($selector);
8241
8242
        $listParts = [];
8243
8244
        foreach ($part as $p) {
8245
            $listParts[] = [Type::T_STRING, '', [$p]];
8246
        }
8247
8248
        return [Type::T_LIST, ',', $listParts];
8249
    }
8250
8251
    protected static $libScssphpGlob = ['pattern'];
8252
    protected function libScssphpGlob($args)
8253
    {
8254
        $string = $this->coerceString($args[0]);
8255
        $pattern = $this->compileStringContent($string);
8256
        $matches = glob($pattern);
8257
        $listParts = [];
8258
8259
        foreach ($matches as $match) {
8260
            if (! is_file($match)) {
8261
                continue;
8262
            }
8263
8264
            $listParts[] = [Type::T_STRING, '"', [$match]];
8265
        }
8266
8267
        return [Type::T_LIST, ',', $listParts];
8268
    }
8269
}
8270