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

Parser::selectors()   A

Complexity

Conditions 5
Paths 4

Size

Total Lines 26
Code Lines 12

Duplication

Lines 0
Ratio 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
cc 5
eloc 12
c 1
b 0
f 0
nc 4
nop 2
dl 0
loc 26
rs 9.5555
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\Block;
16
use ScssPhp\ScssPhp\Cache;
17
use ScssPhp\ScssPhp\Compiler;
18
use ScssPhp\ScssPhp\Exception\ParserException;
19
use ScssPhp\ScssPhp\Node;
20
use ScssPhp\ScssPhp\Type;
21
22
/**
23
 * Parser
24
 *
25
 * @author Leaf Corcoran <[email protected]>
26
 */
27
class Parser
28
{
29
    const SOURCE_INDEX  = -1;
30
    const SOURCE_LINE   = -2;
31
    const SOURCE_COLUMN = -3;
32
33
    /**
34
     * @var array
35
     */
36
    protected static $precedence = [
37
        '='   => 0,
38
        'or'  => 1,
39
        'and' => 2,
40
        '=='  => 3,
41
        '!='  => 3,
42
        '<=>' => 3,
43
        '<='  => 4,
44
        '>='  => 4,
45
        '<'   => 4,
46
        '>'   => 4,
47
        '+'   => 5,
48
        '-'   => 5,
49
        '*'   => 6,
50
        '/'   => 6,
51
        '%'   => 6,
52
    ];
53
54
    protected static $commentPattern;
55
    protected static $operatorPattern;
56
    protected static $whitePattern;
57
58
    protected $cache;
59
60
    private $sourceName;
61
    private $sourceIndex;
62
    private $sourcePositions;
63
    private $charset;
64
    private $count;
65
    private $env;
66
    private $inParens;
67
    private $eatWhiteDefault;
68
    private $discardComments;
69
    private $allowVars;
70
    private $buffer;
71
    private $utf8;
72
    private $encoding;
73
    private $patternModifiers;
74
    private $commentsSeen;
75
76
    private $cssOnly;
77
78
    /**
79
     * Constructor
80
     *
81
     * @api
82
     *
83
     * @param string                 $sourceName
84
     * @param integer                $sourceIndex
85
     * @param string                 $encoding
86
     * @param \ScssPhp\ScssPhp\Cache $cache
87
     */
88
    public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8', $cache = null, $cssOnly = false)
89
    {
90
        $this->sourceName       = $sourceName ?: '(stdin)';
91
        $this->sourceIndex      = $sourceIndex;
92
        $this->charset          = null;
93
        $this->utf8             = ! $encoding || strtolower($encoding) === 'utf-8';
94
        $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
95
        $this->commentsSeen     = [];
96
        $this->commentsSeen     = [];
97
        $this->allowVars        = true;
98
        $this->cssOnly          = $cssOnly;
99
100
        if (empty(static::$operatorPattern)) {
101
            static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
102
103
            $commentSingle      = '\/\/';
104
            $commentMultiLeft   = '\/\*';
105
            $commentMultiRight  = '\*\/';
106
107
            static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
108
            static::$whitePattern = $this->utf8
109
                ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
110
                : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
111
        }
112
113
        if ($cache) {
114
            $this->cache = $cache;
115
        }
116
    }
117
118
    /**
119
     * Get source file name
120
     *
121
     * @api
122
     *
123
     * @return string
124
     */
125
    public function getSourceName()
126
    {
127
        return $this->sourceName;
128
    }
129
130
    /**
131
     * Throw parser error
132
     *
133
     * @api
134
     *
135
     * @param string $msg
136
     *
137
     * @throws \ScssPhp\ScssPhp\Exception\ParserException
138
     */
139
    public function throwParseError($msg = 'parse error')
140
    {
141
        list($line, $column) = $this->getSourcePosition($this->count);
142
143
        $loc = empty($this->sourceName)
144
             ? "line: $line, column: $column"
145
             : "$this->sourceName on line $line, at column $column";
146
147
        if ($this->peek('(.*?)(\n|$)', $m, $this->count)) {
148
            $this->restoreEncoding();
149
150
            $e = new ParserException("$msg: failed at `$m[1]` $loc");
151
            $e->setSourcePosition([$this->sourceName, $line, $column]);
152
153
            throw $e;
154
        }
155
156
        $this->restoreEncoding();
157
158
        $e = new ParserException("$msg: $loc");
159
        $e->setSourcePosition([$this->sourceName, $line, $column]);
160
161
        throw $e;
162
    }
163
164
    /**
165
     * Parser buffer
166
     *
167
     * @api
168
     *
169
     * @param string $buffer
170
     *
171
     * @return \ScssPhp\ScssPhp\Block
172
     */
173
    public function parse($buffer)
174
    {
175
        if ($this->cache) {
176
            $cacheKey = $this->sourceName . ':' . md5($buffer);
177
            $parseOptions = [
178
                'charset' => $this->charset,
179
                'utf8' => $this->utf8,
180
            ];
181
            $v = $this->cache->getCache('parse', $cacheKey, $parseOptions);
182
183
            if (! \is_null($v)) {
184
                return $v;
185
            }
186
        }
187
188
        // strip BOM (byte order marker)
189
        if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
190
            $buffer = substr($buffer, 3);
191
        }
192
193
        $this->buffer          = rtrim($buffer, "\x00..\x1f");
194
        $this->count           = 0;
195
        $this->env             = null;
196
        $this->inParens        = false;
197
        $this->eatWhiteDefault = true;
198
199
        $this->saveEncoding();
200
        $this->extractLineNumbers($buffer);
201
202
        $this->pushBlock(null); // root block
203
        $this->whitespace();
204
        $this->pushBlock(null);
205
        $this->popBlock();
206
207
        while ($this->parseChunk()) {
208
            ;
209
        }
210
211
        if ($this->count !== \strlen($this->buffer)) {
212
            $this->throwParseError();
213
        }
214
215
        if (! empty($this->env->parent)) {
216
            $this->throwParseError('unclosed block');
217
        }
218
219
        if ($this->charset) {
220
            array_unshift($this->env->children, $this->charset);
221
        }
222
223
        $this->restoreEncoding();
224
225
        if ($this->cache) {
226
            $this->cache->setCache('parse', $cacheKey, $this->env, $parseOptions);
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $cacheKey does not seem to be defined for all execution paths leading up to this point.
Loading history...
Comprehensibility Best Practice introduced by
The variable $parseOptions does not seem to be defined for all execution paths leading up to this point.
Loading history...
227
        }
228
229
        return $this->env;
230
    }
231
232
    /**
233
     * Parse a value or value list
234
     *
235
     * @api
236
     *
237
     * @param string       $buffer
238
     * @param string|array $out
239
     *
240
     * @return boolean
241
     */
242
    public function parseValue($buffer, &$out)
243
    {
244
        $this->count           = 0;
245
        $this->env             = null;
246
        $this->inParens        = false;
247
        $this->eatWhiteDefault = true;
248
        $this->buffer          = (string) $buffer;
249
250
        $this->saveEncoding();
251
252
        $list = $this->valueList($out);
0 ignored issues
show
Bug introduced by
It seems like $out can also be of type string; however, parameter $out of ScssPhp\ScssPhp\Parser::valueList() 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

252
        $list = $this->valueList(/** @scrutinizer ignore-type */ $out);
Loading history...
253
254
        $this->restoreEncoding();
255
256
        return $list;
257
    }
258
259
    /**
260
     * Parse a selector or selector list
261
     *
262
     * @api
263
     *
264
     * @param string       $buffer
265
     * @param string|array $out
266
     *
267
     * @return boolean
268
     */
269
    public function parseSelector($buffer, &$out)
270
    {
271
        $this->count           = 0;
272
        $this->env             = null;
273
        $this->inParens        = false;
274
        $this->eatWhiteDefault = true;
275
        $this->buffer          = (string) $buffer;
276
277
        $this->saveEncoding();
278
279
        $selector = $this->selectors($out);
0 ignored issues
show
Bug introduced by
It seems like $out can also be of type string; however, parameter $out of ScssPhp\ScssPhp\Parser::selectors() 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

279
        $selector = $this->selectors(/** @scrutinizer ignore-type */ $out);
Loading history...
280
281
        $this->restoreEncoding();
282
283
        return $selector;
284
    }
285
286
    /**
287
     * Parse a media Query
288
     *
289
     * @api
290
     *
291
     * @param string       $buffer
292
     * @param string|array $out
293
     *
294
     * @return boolean
295
     */
296
    public function parseMediaQueryList($buffer, &$out)
297
    {
298
        $this->count           = 0;
299
        $this->env             = null;
300
        $this->inParens        = false;
301
        $this->eatWhiteDefault = true;
302
        $this->buffer          = (string) $buffer;
303
304
        $this->saveEncoding();
305
306
        $isMediaQuery = $this->mediaQueryList($out);
0 ignored issues
show
Bug introduced by
It seems like $out can also be of type string; however, parameter $out of ScssPhp\ScssPhp\Parser::mediaQueryList() 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

306
        $isMediaQuery = $this->mediaQueryList(/** @scrutinizer ignore-type */ $out);
Loading history...
307
308
        $this->restoreEncoding();
309
310
        return $isMediaQuery;
311
    }
312
313
    /**
314
     * Parse a single chunk off the head of the buffer and append it to the
315
     * current parse environment.
316
     *
317
     * Returns false when the buffer is empty, or when there is an error.
318
     *
319
     * This function is called repeatedly until the entire document is
320
     * parsed.
321
     *
322
     * This parser is most similar to a recursive descent parser. Single
323
     * functions represent discrete grammatical rules for the language, and
324
     * they are able to capture the text that represents those rules.
325
     *
326
     * Consider the function Compiler::keyword(). (All parse functions are
327
     * structured the same.)
328
     *
329
     * The function takes a single reference argument. When calling the
330
     * function it will attempt to match a keyword on the head of the buffer.
331
     * If it is successful, it will place the keyword in the referenced
332
     * argument, advance the position in the buffer, and return true. If it
333
     * fails then it won't advance the buffer and it will return false.
334
     *
335
     * All of these parse functions are powered by Compiler::match(), which behaves
336
     * the same way, but takes a literal regular expression. Sometimes it is
337
     * more convenient to use match instead of creating a new function.
338
     *
339
     * Because of the format of the functions, to parse an entire string of
340
     * grammatical rules, you can chain them together using &&.
341
     *
342
     * But, if some of the rules in the chain succeed before one fails, then
343
     * the buffer position will be left at an invalid state. In order to
344
     * avoid this, Compiler::seek() is used to remember and set buffer positions.
345
     *
346
     * Before parsing a chain, use $s = $this->count to remember the current
347
     * position into $s. Then if a chain fails, use $this->seek($s) to
348
     * go back where we started.
349
     *
350
     * @return boolean
351
     */
352
    protected function parseChunk()
353
    {
354
        $s = $this->count;
355
356
        // the directives
357
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
358
            if (
359
                $this->literal('@at-root', 8) &&
360
                ($this->selectors($selector) || true) &&
361
                ($this->map($with) || true) &&
362
                (($this->matchChar('(') &&
363
                    $this->interpolation($with) &&
364
                    $this->matchChar(')')) || true) &&
365
                $this->matchChar('{', false)
366
            ) {
367
                if ($this->cssOnly) {
368
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
369
                }
370
371
                $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
372
                $atRoot->selector = $selector;
0 ignored issues
show
Bug introduced by
The property selector does not exist on ScssPhp\ScssPhp\Block. Did you mean selectors?
Loading history...
373
                $atRoot->with     = $with;
0 ignored issues
show
Bug introduced by
The property with does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
374
375
                return true;
376
            }
377
378
            $this->seek($s);
379
380
            if (
381
                $this->literal('@media', 6) &&
382
                $this->mediaQueryList($mediaQueryList) &&
383
                $this->matchChar('{', false)
384
            ) {
385
                $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
386
                $media->queryList = $mediaQueryList[2];
0 ignored issues
show
Bug introduced by
The property queryList does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
387
388
                return true;
389
            }
390
391
            $this->seek($s);
392
393
            if (
394
                $this->literal('@mixin', 6) &&
395
                $this->keyword($mixinName) &&
396
                ($this->argumentDef($args) || true) &&
397
                $this->matchChar('{', false)
398
            ) {
399
                if ($this->cssOnly) {
400
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
401
                }
402
403
                $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
404
                $mixin->name = $mixinName;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
405
                $mixin->args = $args;
0 ignored issues
show
Bug introduced by
The property args does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
406
407
                return true;
408
            }
409
410
            $this->seek($s);
411
412
            if (
413
                ($this->literal('@include', 8) &&
414
                    $this->keyword($mixinName) &&
415
                    ($this->matchChar('(') &&
416
                    ($this->argValues($argValues) || true) &&
417
                    $this->matchChar(')') || true) &&
418
                    ($this->end()) ||
419
                ($this->literal('using', 5) &&
420
                    $this->argumentDef($argUsing) &&
421
                    ($this->end() || $this->matchChar('{') && $hasBlock = true)) ||
422
                $this->matchChar('{') && $hasBlock = true)
423
            ) {
424
                if ($this->cssOnly) {
425
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
426
                }
427
428
                $child = [
429
                    Type::T_INCLUDE,
430
                    $mixinName,
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $mixinName does not seem to be defined for all execution paths leading up to this point.
Loading history...
431
                    isset($argValues) ? $argValues : null,
432
                    null,
433
                    isset($argUsing) ? $argUsing : null
434
                ];
435
436
                if (! empty($hasBlock)) {
437
                    $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
438
                    $include->child = $child;
0 ignored issues
show
Bug introduced by
The property child does not exist on ScssPhp\ScssPhp\Block. Did you mean children?
Loading history...
439
                } else {
440
                    $this->append($child, $s);
441
                }
442
443
                return true;
444
            }
445
446
            $this->seek($s);
447
448
            if (
449
                $this->literal('@scssphp-import-once', 20) &&
450
                $this->valueList($importPath) &&
451
                $this->end()
452
            ) {
453
                if ($this->cssOnly) {
454
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
455
                }
456
457
                $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
458
459
                return true;
460
            }
461
462
            $this->seek($s);
463
464
            if (
465
                $this->literal('@import', 7) &&
466
                $this->valueList($importPath) &&
467
                $importPath[0] !== Type::T_FUNCTION_CALL &&
468
                $this->end()
469
            ) {
470
                $this->append([Type::T_IMPORT, $importPath], $s);
471
472
                return true;
473
            }
474
475
            $this->seek($s);
476
477
            if (
478
                $this->literal('@import', 7) &&
479
                $this->url($importPath) &&
480
                $this->end()
481
            ) {
482
                if ($this->cssOnly) {
483
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
484
                }
485
486
                $this->append([Type::T_IMPORT, $importPath], $s);
487
488
                return true;
489
            }
490
491
            $this->seek($s);
492
493
            if (
494
                $this->literal('@extend', 7) &&
495
                $this->selectors($selectors) &&
496
                $this->end()
497
            ) {
498
                if ($this->cssOnly) {
499
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
500
                }
501
502
                // check for '!flag'
503
                $optional = $this->stripOptionalFlag($selectors);
504
                $this->append([Type::T_EXTEND, $selectors, $optional], $s);
505
506
                return true;
507
            }
508
509
            $this->seek($s);
510
511
            if (
512
                $this->literal('@function', 9) &&
513
                $this->keyword($fnName) &&
514
                $this->argumentDef($args) &&
515
                $this->matchChar('{', false)
516
            ) {
517
                if ($this->cssOnly) {
518
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
519
                }
520
521
                $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
522
                $func->name = $fnName;
523
                $func->args = $args;
524
525
                return true;
526
            }
527
528
            $this->seek($s);
529
530
            if (
531
                $this->literal('@break', 6) &&
532
                $this->end()
533
            ) {
534
                if ($this->cssOnly) {
535
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
536
                }
537
538
                $this->append([Type::T_BREAK], $s);
539
540
                return true;
541
            }
542
543
            $this->seek($s);
544
545
            if (
546
                $this->literal('@continue', 9) &&
547
                $this->end()
548
            ) {
549
                if ($this->cssOnly) {
550
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
551
                }
552
553
                $this->append([Type::T_CONTINUE], $s);
554
555
                return true;
556
            }
557
558
            $this->seek($s);
559
560
            if (
561
                $this->literal('@return', 7) &&
562
                ($this->valueList($retVal) || true) &&
563
                $this->end()
564
            ) {
565
                if ($this->cssOnly) {
566
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
567
                }
568
569
                $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
570
571
                return true;
572
            }
573
574
            $this->seek($s);
575
576
            if (
577
                $this->literal('@each', 5) &&
578
                $this->genericList($varNames, 'variable', ',', false) &&
579
                $this->literal('in', 2) &&
580
                $this->valueList($list) &&
581
                $this->matchChar('{', false)
582
            ) {
583
                if ($this->cssOnly) {
584
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
585
                }
586
587
                $each = $this->pushSpecialBlock(Type::T_EACH, $s);
588
589
                foreach ($varNames[2] as $varName) {
590
                    $each->vars[] = $varName[1];
0 ignored issues
show
Bug introduced by
The property vars does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
591
                }
592
593
                $each->list = $list;
0 ignored issues
show
Bug introduced by
The property list does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
594
595
                return true;
596
            }
597
598
            $this->seek($s);
599
600
            if (
601
                $this->literal('@while', 6) &&
602
                $this->expression($cond) &&
603
                $this->matchChar('{', false)
604
            ) {
605
                if ($this->cssOnly) {
606
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
607
                }
608
609
                while (
610
                    $cond[0] === Type::T_LIST &&
611
                    ! empty($cond['enclosing']) &&
612
                    $cond['enclosing'] === 'parent' &&
613
                    \count($cond[2]) == 1
614
                ) {
615
                    $cond = reset($cond[2]);
616
                }
617
618
                $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
619
                $while->cond = $cond;
0 ignored issues
show
Bug introduced by
The property cond does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
620
621
                return true;
622
            }
623
624
            $this->seek($s);
625
626
            if (
627
                $this->literal('@for', 4) &&
628
                $this->variable($varName) &&
629
                $this->literal('from', 4) &&
630
                $this->expression($start) &&
631
                ($this->literal('through', 7) ||
632
                    ($forUntil = true && $this->literal('to', 2))) &&
633
                $this->expression($end) &&
634
                $this->matchChar('{', false)
635
            ) {
636
                if ($this->cssOnly) {
637
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
638
                }
639
640
                $for = $this->pushSpecialBlock(Type::T_FOR, $s);
641
                $for->var   = $varName[1];
0 ignored issues
show
Bug introduced by
The property var does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
642
                $for->start = $start;
0 ignored issues
show
Bug introduced by
The property start does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
643
                $for->end   = $end;
0 ignored issues
show
Bug introduced by
The property end does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
644
                $for->until = isset($forUntil);
0 ignored issues
show
Bug introduced by
The property until does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
645
646
                return true;
647
            }
648
649
            $this->seek($s);
650
651
            if (
652
                $this->literal('@if', 3) &&
653
                $this->functionCallArgumentsList($cond, false) &&
654
                $this->matchChar('{', false)
655
            ) {
656
                if ($this->cssOnly) {
657
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
658
                }
659
660
                $if = $this->pushSpecialBlock(Type::T_IF, $s);
661
662
                while (
663
                    $cond[0] === Type::T_LIST &&
664
                    ! empty($cond['enclosing']) &&
665
                    $cond['enclosing'] === 'parent' &&
666
                    \count($cond[2]) == 1
667
                ) {
668
                    $cond = reset($cond[2]);
669
                }
670
671
                $if->cond  = $cond;
672
                $if->cases = [];
0 ignored issues
show
Bug introduced by
The property cases does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
673
674
                return true;
675
            }
676
677
            $this->seek($s);
678
679
            if (
680
                $this->literal('@debug', 6) &&
681
                $this->functionCallArgumentsList($value, false) &&
682
                $this->end()
683
            ) {
684
                if ($this->cssOnly) {
685
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
686
                }
687
688
                $this->append([Type::T_DEBUG, $value], $s);
689
690
                return true;
691
            }
692
693
            $this->seek($s);
694
695
            if (
696
                $this->literal('@warn', 5) &&
697
                $this->functionCallArgumentsList($value, false) &&
698
                $this->end()
699
            ) {
700
                if ($this->cssOnly) {
701
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
702
                }
703
704
                $this->append([Type::T_WARN, $value], $s);
705
706
                return true;
707
            }
708
709
            $this->seek($s);
710
711
            if (
712
                $this->literal('@error', 6) &&
713
                $this->functionCallArgumentsList($value, false) &&
714
                $this->end()
715
            ) {
716
                if ($this->cssOnly) {
717
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
718
                }
719
720
                $this->append([Type::T_ERROR, $value], $s);
721
722
                return true;
723
            }
724
725
            $this->seek($s);
726
727
            if (
728
                $this->literal('@content', 8) &&
729
                ($this->end() ||
730
                    $this->matchChar('(') &&
731
                    $this->argValues($argContent) &&
732
                    $this->matchChar(')') &&
733
                    $this->end())
734
            ) {
735
                if ($this->cssOnly) {
736
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
737
                }
738
739
                $this->append([Type::T_MIXIN_CONTENT, isset($argContent) ? $argContent : null], $s);
740
741
                return true;
742
            }
743
744
            $this->seek($s);
745
746
            $last = $this->last();
747
748
            if (isset($last) && $last[0] === Type::T_IF) {
749
                list(, $if) = $last;
750
751
                if ($this->literal('@else', 5)) {
752
                    if ($this->matchChar('{', false)) {
753
                        $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
754
                    } elseif (
755
                        $this->literal('if', 2) &&
756
                        $this->valueList($cond) &&
757
                        $this->matchChar('{', false)
758
                    ) {
759
                        $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
760
                        $else->cond = $cond;
761
                    }
762
763
                    if (isset($else)) {
764
                        $else->dontAppend = true;
765
                        $if->cases[] = $else;
766
767
                        return true;
768
                    }
769
                }
770
771
                $this->seek($s);
772
            }
773
774
            // only retain the first @charset directive encountered
775
            if (
776
                $this->literal('@charset', 8) &&
777
                $this->valueList($charset) &&
778
                $this->end()
779
            ) {
780
                if (! isset($this->charset)) {
781
                    $statement = [Type::T_CHARSET, $charset];
782
783
                    list($line, $column) = $this->getSourcePosition($s);
784
785
                    $statement[static::SOURCE_LINE]   = $line;
786
                    $statement[static::SOURCE_COLUMN] = $column;
787
                    $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
788
789
                    $this->charset = $statement;
790
                }
791
792
                return true;
793
            }
794
795
            $this->seek($s);
796
797
            if (
798
                $this->literal('@supports', 9) &&
799
                ($t1 = $this->supportsQuery($supportQuery)) &&
0 ignored issues
show
Unused Code introduced by
The assignment to $t1 is dead and can be removed.
Loading history...
800
                ($t2 = $this->matchChar('{', false))
0 ignored issues
show
Unused Code introduced by
The assignment to $t2 is dead and can be removed.
Loading history...
801
            ) {
802
                $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
803
                $directive->name  = 'supports';
804
                $directive->value = $supportQuery;
0 ignored issues
show
Bug introduced by
The property value does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
805
806
                return true;
807
            }
808
809
            $this->seek($s);
810
811
            // doesn't match built in directive, do generic one
812
            if (
813
                $this->matchChar('@', false) &&
814
                $this->keyword($dirName) &&
815
                $this->directiveValue($dirValue, '{')
816
            ) {
817
                if ($dirName === 'media') {
818
                    $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
819
                } else {
820
                    $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
821
                    $directive->name = $dirName;
822
                }
823
824
                if (isset($dirValue)) {
825
                    $directive->value = $dirValue;
826
                }
827
828
                return true;
829
            }
830
831
            $this->seek($s);
832
833
            // maybe it's a generic blockless directive
834
            if (
835
                $this->matchChar('@', false) &&
836
                $this->keyword($dirName) &&
837
                $this->directiveValue($dirValue) &&
838
                $this->end()
839
            ) {
840
                $this->append([Type::T_DIRECTIVE, [$dirName, $dirValue]], $s);
841
842
                return true;
843
            }
844
845
            $this->seek($s);
846
847
            return false;
848
        }
849
850
        $inCssSelector = null;
851
        if ($this->cssOnly) {
852
            $inCssSelector = (! empty($this->env->parent) &&
853
                ! in_array($this->env->type, [Type::T_DIRECTIVE, Type::T_MEDIA]));
854
        }
855
        // custom properties : right part is static
856
        if (
857
            ($this->customProperty($name) ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->customProperty($...->matchChar(':', false), Probably Intended Meaning: $this->customProperty($n...>matchChar(':', false))
Loading history...
858
                ($inCssSelector && $this->propertyName($name))) &&
859
            $this->matchChar(':', false)
860
        ) {
861
            $start = $this->count;
862
863
            // but can be complex and finish with ; or }
864
            foreach ([';','}'] as $ending) {
865
                if (
866
                    $this->openString($ending, $stringValue, '(', ')', false) &&
867
                    $this->end()
868
                ) {
869
                    $end = $this->count;
870
                    $value = $stringValue;
871
872
                    // check if we have only a partial value due to nested [] or { } to take in account
873
                    $nestingPairs = [['[', ']'], ['{', '}']];
874
875
                    foreach ($nestingPairs as $nestingPair) {
876
                        $p = strpos($this->buffer, $nestingPair[0], $start);
877
878
                        if ($p && $p < $end) {
879
                            $this->seek($start);
880
881
                            if (
882
                                $this->openString($ending, $stringValue, $nestingPair[0], $nestingPair[1], false) &&
883
                                $this->end() &&
884
                                $this->count > $end
885
                            ) {
886
                                $end = $this->count;
887
                                $value = $stringValue;
888
                            }
889
                        }
890
                    }
891
892
                    $this->seek($end);
893
                    $this->append([Type::T_CUSTOM_PROPERTY, $name, $value], $s);
894
895
                    return true;
896
                }
897
            }
898
899
            // TODO: output an error here if nothing found according to sass spec
900
        }
901
902
        $this->seek($s);
903
904
        // property shortcut
905
        // captures most properties before having to parse a selector
906
        if (
907
            $this->keyword($name, false) &&
908
            $this->literal(': ', 2) &&
909
            $this->valueList($value) &&
910
            $this->end()
911
        ) {
912
            $name = [Type::T_STRING, '', [$name]];
913
            $this->append([Type::T_ASSIGN, $name, $value], $s);
914
915
            return true;
916
        }
917
918
        $this->seek($s);
919
920
        // variable assigns
921
        if (
922
            $this->variable($name) &&
923
            $this->matchChar(':') &&
924
            $this->valueList($value) &&
925
            $this->end()
926
        ) {
927
            if ($this->cssOnly) {
928
                $this->throwParseError('SCSS syntax not allowed in CSS file');
929
            }
930
931
            // check for '!flag'
932
            $assignmentFlags = $this->stripAssignmentFlags($value);
933
            $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
934
935
            return true;
936
        }
937
938
        $this->seek($s);
939
940
        // misc
941
        if ($this->literal('-->', 3)) {
942
            return true;
943
        }
944
945
        // opening css block
946
        if (
947
            $this->selectors($selectors) &&
948
            $this->matchChar('{', false)
949
        ) {
950
            if ($this->cssOnly) {
951
                if ($inCssSelector) {
952
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
953
                }
954
            }
955
956
            $this->pushBlock($selectors, $s);
957
958
            if ($this->eatWhiteDefault) {
959
                $this->whitespace();
960
                $this->append(null); // collect comments at the beginning if needed
961
            }
962
963
            return true;
964
        }
965
966
        $this->seek($s);
967
968
        // property assign, or nested assign
969
        if (
970
            $this->propertyName($name) &&
971
            $this->matchChar(':')
972
        ) {
973
            $foundSomething = false;
974
975
            if ($this->valueList($value)) {
976
                if (empty($this->env->parent)) {
977
                    $this->throwParseError('expected "{"');
978
                }
979
980
                $this->append([Type::T_ASSIGN, $name, $value], $s);
981
                $foundSomething = true;
982
            }
983
984
            if ($this->matchChar('{', false)) {
985
                if ($this->cssOnly) {
986
                    $this->throwParseError('SCSS syntax not allowed in CSS file');
987
                }
988
989
                $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
990
                $propBlock->prefix = $name;
0 ignored issues
show
Bug introduced by
The property prefix does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
991
                $propBlock->hasValue = $foundSomething;
0 ignored issues
show
Bug introduced by
The property hasValue does not seem to exist on ScssPhp\ScssPhp\Block.
Loading history...
992
993
                $foundSomething = true;
994
            } elseif ($foundSomething) {
995
                $foundSomething = $this->end();
996
            }
997
998
            if ($foundSomething) {
999
                return true;
1000
            }
1001
        }
1002
1003
        $this->seek($s);
1004
1005
        // closing a block
1006
        if ($this->matchChar('}', false)) {
1007
            $block = $this->popBlock();
1008
1009
            if (! isset($block->type) || $block->type !== Type::T_IF) {
1010
                if ($this->env->parent) {
1011
                    $this->append(null); // collect comments before next statement if needed
1012
                }
1013
            }
1014
1015
            if (isset($block->type) && $block->type === Type::T_INCLUDE) {
1016
                $include = $block->child;
1017
                unset($block->child);
1018
                $include[3] = $block;
1019
                $this->append($include, $s);
1020
            } elseif (empty($block->dontAppend)) {
1021
                $type = isset($block->type) ? $block->type : Type::T_BLOCK;
1022
                $this->append([$type, $block], $s);
1023
            }
1024
1025
            // collect comments just after the block closing if needed
1026
            if ($this->eatWhiteDefault) {
1027
                $this->whitespace();
1028
1029
                if ($this->env->comments) {
1030
                    $this->append(null);
1031
                }
1032
            }
1033
1034
            return true;
1035
        }
1036
1037
        // extra stuff
1038
        if (
1039
            $this->matchChar(';') ||
1040
            $this->literal('<!--', 4)
1041
        ) {
1042
            return true;
1043
        }
1044
1045
        return false;
1046
    }
1047
1048
    /**
1049
     * Push block onto parse tree
1050
     *
1051
     * @param array   $selectors
1052
     * @param integer $pos
1053
     *
1054
     * @return \ScssPhp\ScssPhp\Block
1055
     */
1056
    protected function pushBlock($selectors, $pos = 0)
1057
    {
1058
        list($line, $column) = $this->getSourcePosition($pos);
1059
1060
        $b = new Block();
1061
        $b->sourceName   = $this->sourceName;
1062
        $b->sourceLine   = $line;
1063
        $b->sourceColumn = $column;
1064
        $b->sourceIndex  = $this->sourceIndex;
1065
        $b->selectors    = $selectors;
1066
        $b->comments     = [];
1067
        $b->parent       = $this->env;
1068
1069
        if (! $this->env) {
1070
            $b->children = [];
1071
        } elseif (empty($this->env->children)) {
1072
            $this->env->children = $this->env->comments;
1073
            $b->children = [];
1074
            $this->env->comments = [];
1075
        } else {
1076
            $b->children = $this->env->comments;
1077
            $this->env->comments = [];
1078
        }
1079
1080
        $this->env = $b;
1081
1082
        // collect comments at the beginning of a block if needed
1083
        if ($this->eatWhiteDefault) {
1084
            $this->whitespace();
1085
1086
            if ($this->env->comments) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->env->comments 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...
1087
                $this->append(null);
1088
            }
1089
        }
1090
1091
        return $b;
1092
    }
1093
1094
    /**
1095
     * Push special (named) block onto parse tree
1096
     *
1097
     * @param string  $type
1098
     * @param integer $pos
1099
     *
1100
     * @return \ScssPhp\ScssPhp\Block
1101
     */
1102
    protected function pushSpecialBlock($type, $pos)
1103
    {
1104
        $block = $this->pushBlock(null, $pos);
1105
        $block->type = $type;
1106
1107
        return $block;
1108
    }
1109
1110
    /**
1111
     * Pop scope and return last block
1112
     *
1113
     * @return \ScssPhp\ScssPhp\Block
1114
     *
1115
     * @throws \Exception
1116
     */
1117
    protected function popBlock()
1118
    {
1119
1120
        // collect comments ending just before of a block closing
1121
        if ($this->env->comments) {
1122
            $this->append(null);
1123
        }
1124
1125
        // pop the block
1126
        $block = $this->env;
1127
1128
        if (empty($block->parent)) {
1129
            $this->throwParseError('unexpected }');
1130
        }
1131
1132
        if ($block->type == Type::T_AT_ROOT) {
1133
            // keeps the parent in case of self selector &
1134
            $block->selfParent = $block->parent;
1135
        }
1136
1137
        $this->env = $block->parent;
1138
1139
        unset($block->parent);
1140
1141
        return $block;
1142
    }
1143
1144
    /**
1145
     * Peek input stream
1146
     *
1147
     * @param string  $regex
1148
     * @param array   $out
1149
     * @param integer $from
1150
     *
1151
     * @return integer
1152
     */
1153
    protected function peek($regex, &$out, $from = null)
1154
    {
1155
        if (! isset($from)) {
1156
            $from = $this->count;
1157
        }
1158
1159
        $r = '/' . $regex . '/' . $this->patternModifiers;
1160
        $result = preg_match($r, $this->buffer, $out, null, $from);
1161
1162
        return $result;
1163
    }
1164
1165
    /**
1166
     * Seek to position in input stream (or return current position in input stream)
1167
     *
1168
     * @param integer $where
1169
     */
1170
    protected function seek($where)
1171
    {
1172
        $this->count = $where;
1173
    }
1174
1175
    /**
1176
     * Match string looking for either ending delim, escape, or string interpolation
1177
     *
1178
     * {@internal This is a workaround for preg_match's 250K string match limit. }}
1179
     *
1180
     * @param array  $m     Matches (passed by reference)
1181
     * @param string $delim Delimiter
1182
     *
1183
     * @return boolean True if match; false otherwise
1184
     */
1185
    protected function matchString(&$m, $delim)
1186
    {
1187
        $token = null;
1188
1189
        $end = \strlen($this->buffer);
1190
1191
        // look for either ending delim, escape, or string interpolation
1192
        foreach (['#{', '\\', "\r", $delim] as $lookahead) {
1193
            $pos = strpos($this->buffer, $lookahead, $this->count);
1194
1195
            if ($pos !== false && $pos < $end) {
1196
                $end = $pos;
1197
                $token = $lookahead;
1198
            }
1199
        }
1200
1201
        if (! isset($token)) {
1202
            return false;
1203
        }
1204
1205
        $match = substr($this->buffer, $this->count, $end - $this->count);
1206
        $m = [
1207
            $match . $token,
1208
            $match,
1209
            $token
1210
        ];
1211
        $this->count = $end + \strlen($token);
1212
1213
        return true;
1214
    }
1215
1216
    /**
1217
     * Try to match something on head of buffer
1218
     *
1219
     * @param string  $regex
1220
     * @param array   $out
1221
     * @param boolean $eatWhitespace
1222
     *
1223
     * @return boolean
1224
     */
1225
    protected function match($regex, &$out, $eatWhitespace = null)
1226
    {
1227
        $r = '/' . $regex . '/' . $this->patternModifiers;
1228
1229
        if (! preg_match($r, $this->buffer, $out, null, $this->count)) {
1230
            return false;
1231
        }
1232
1233
        $this->count += \strlen($out[0]);
1234
1235
        if (! isset($eatWhitespace)) {
1236
            $eatWhitespace = $this->eatWhiteDefault;
1237
        }
1238
1239
        if ($eatWhitespace) {
1240
            $this->whitespace();
1241
        }
1242
1243
        return true;
1244
    }
1245
1246
    /**
1247
     * Match a single string
1248
     *
1249
     * @param string  $char
1250
     * @param boolean $eatWhitespace
1251
     *
1252
     * @return boolean
1253
     */
1254
    protected function matchChar($char, $eatWhitespace = null)
1255
    {
1256
        if (! isset($this->buffer[$this->count]) || $this->buffer[$this->count] !== $char) {
1257
            return false;
1258
        }
1259
1260
        $this->count++;
1261
1262
        if (! isset($eatWhitespace)) {
1263
            $eatWhitespace = $this->eatWhiteDefault;
1264
        }
1265
1266
        if ($eatWhitespace) {
1267
            $this->whitespace();
1268
        }
1269
1270
        return true;
1271
    }
1272
1273
    /**
1274
     * Match literal string
1275
     *
1276
     * @param string  $what
1277
     * @param integer $len
1278
     * @param boolean $eatWhitespace
1279
     *
1280
     * @return boolean
1281
     */
1282
    protected function literal($what, $len, $eatWhitespace = null)
1283
    {
1284
        if (strcasecmp(substr($this->buffer, $this->count, $len), $what) !== 0) {
1285
            return false;
1286
        }
1287
1288
        $this->count += $len;
1289
1290
        if (! isset($eatWhitespace)) {
1291
            $eatWhitespace = $this->eatWhiteDefault;
1292
        }
1293
1294
        if ($eatWhitespace) {
1295
            $this->whitespace();
1296
        }
1297
1298
        return true;
1299
    }
1300
1301
    /**
1302
     * Match some whitespace
1303
     *
1304
     * @return boolean
1305
     */
1306
    protected function whitespace()
1307
    {
1308
        $gotWhite = false;
1309
1310
        while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
1311
            if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
1312
                // comment that are kept in the output CSS
1313
                $comment = [];
1314
                $startCommentCount = $this->count;
1315
                $endCommentCount = $this->count + \strlen($m[1]);
1316
1317
                // find interpolations in comment
1318
                $p = strpos($this->buffer, '#{', $this->count);
1319
1320
                while ($p !== false && $p < $endCommentCount) {
1321
                    $c           = substr($this->buffer, $this->count, $p - $this->count);
1322
                    $comment[]   = $c;
1323
                    $this->count = $p;
1324
                    $out         = null;
1325
1326
                    if ($this->interpolation($out)) {
1327
                        // keep right spaces in the following string part
1328
                        if ($out[3]) {
1329
                            while ($this->buffer[$this->count - 1] !== '}') {
1330
                                $this->count--;
1331
                            }
1332
1333
                            $out[3] = '';
1334
                        }
1335
1336
                        $comment[] = [Type::T_COMMENT, substr($this->buffer, $p, $this->count - $p), $out];
1337
                    } else {
1338
                        $comment[] = substr($this->buffer, $this->count, 2);
1339
1340
                        $this->count += 2;
1341
                    }
1342
1343
                    $p = strpos($this->buffer, '#{', $this->count);
1344
                }
1345
1346
                // remaining part
1347
                $c = substr($this->buffer, $this->count, $endCommentCount - $this->count);
1348
1349
                if (! $comment) {
1350
                    // single part static comment
1351
                    $this->appendComment([Type::T_COMMENT, $c]);
1352
                } else {
1353
                    $comment[] = $c;
1354
                    $staticComment = substr($this->buffer, $startCommentCount, $endCommentCount - $startCommentCount);
1355
                    $this->appendComment([Type::T_COMMENT, $staticComment, [Type::T_STRING, '', $comment]]);
1356
                }
1357
1358
                $this->commentsSeen[$startCommentCount] = true;
1359
                $this->count = $endCommentCount;
1360
            } else {
1361
                // comment that are ignored and not kept in the output css
1362
                $this->count += \strlen($m[0]);
1363
            }
1364
1365
            $gotWhite = true;
1366
        }
1367
1368
        return $gotWhite;
1369
    }
1370
1371
    /**
1372
     * Append comment to current block
1373
     *
1374
     * @param array $comment
1375
     */
1376
    protected function appendComment($comment)
1377
    {
1378
        if (! $this->discardComments) {
1379
            $this->env->comments[] = $comment;
1380
        }
1381
    }
1382
1383
    /**
1384
     * Append statement to current block
1385
     *
1386
     * @param array   $statement
1387
     * @param integer $pos
1388
     */
1389
    protected function append($statement, $pos = null)
1390
    {
1391
        if (! \is_null($statement)) {
0 ignored issues
show
introduced by
The condition is_null($statement) is always false.
Loading history...
1392
            if (! \is_null($pos)) {
1393
                list($line, $column) = $this->getSourcePosition($pos);
1394
1395
                $statement[static::SOURCE_LINE]   = $line;
1396
                $statement[static::SOURCE_COLUMN] = $column;
1397
                $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
1398
            }
1399
1400
            $this->env->children[] = $statement;
1401
        }
1402
1403
        $comments = $this->env->comments;
1404
1405
        if ($comments) {
1406
            $this->env->children = array_merge($this->env->children, $comments);
1407
            $this->env->comments = [];
1408
        }
1409
    }
1410
1411
    /**
1412
     * Returns last child was appended
1413
     *
1414
     * @return array|null
1415
     */
1416
    protected function last()
1417
    {
1418
        $i = \count($this->env->children) - 1;
1419
1420
        if (isset($this->env->children[$i])) {
1421
            return $this->env->children[$i];
1422
        }
1423
    }
1424
1425
    /**
1426
     * Parse media query list
1427
     *
1428
     * @param array $out
1429
     *
1430
     * @return boolean
1431
     */
1432
    protected function mediaQueryList(&$out)
1433
    {
1434
        return $this->genericList($out, 'mediaQuery', ',', false);
1435
    }
1436
1437
    /**
1438
     * Parse media query
1439
     *
1440
     * @param array $out
1441
     *
1442
     * @return boolean
1443
     */
1444
    protected function mediaQuery(&$out)
1445
    {
1446
        $expressions = null;
1447
        $parts = [];
1448
1449
        if (
1450
            ($this->literal('only', 4) && ($only = true) ||
0 ignored issues
show
introduced by
Consider adding parentheses for clarity. Current Interpretation: ($this->literal('only', ...ixedKeyword($mediaType), Probably Intended Meaning: $this->literal('only', 4...xedKeyword($mediaType))
Loading history...
1451
            $this->literal('not', 3) && ($not = true) || true) &&
1452
            $this->mixedKeyword($mediaType)
1453
        ) {
1454
            $prop = [Type::T_MEDIA_TYPE];
1455
1456
            if (isset($only)) {
1457
                $prop[] = [Type::T_KEYWORD, 'only'];
1458
            }
1459
1460
            if (isset($not)) {
1461
                $prop[] = [Type::T_KEYWORD, 'not'];
1462
            }
1463
1464
            $media = [Type::T_LIST, '', []];
1465
1466
            foreach ((array) $mediaType as $type) {
1467
                if (\is_array($type)) {
1468
                    $media[2][] = $type;
1469
                } else {
1470
                    $media[2][] = [Type::T_KEYWORD, $type];
1471
                }
1472
            }
1473
1474
            $prop[]  = $media;
1475
            $parts[] = $prop;
1476
        }
1477
1478
        if (empty($parts) || $this->literal('and', 3)) {
1479
            $this->genericList($expressions, 'mediaExpression', 'and', false);
1480
1481
            if (\is_array($expressions)) {
1482
                $parts = array_merge($parts, $expressions[2]);
1483
            }
1484
        }
1485
1486
        $out = $parts;
1487
1488
        return true;
1489
    }
1490
1491
    /**
1492
     * Parse supports query
1493
     *
1494
     * @param array $out
1495
     *
1496
     * @return boolean
1497
     */
1498
    protected function supportsQuery(&$out)
1499
    {
1500
        $expressions = null;
1501
        $parts = [];
1502
1503
        $s = $this->count;
1504
1505
        $not = false;
1506
1507
        if (
1508
            ($this->literal('not', 3) && ($not = true) || true) &&
1509
            $this->matchChar('(') &&
1510
            ($this->expression($property)) &&
1511
            $this->literal(': ', 2) &&
1512
            $this->valueList($value) &&
1513
            $this->matchChar(')')
1514
        ) {
1515
            $support = [Type::T_STRING, '', [[Type::T_KEYWORD, ($not ? 'not ' : '') . '(']]];
1516
            $support[2][] = $property;
1517
            $support[2][] = [Type::T_KEYWORD, ': '];
1518
            $support[2][] = $value;
1519
            $support[2][] = [Type::T_KEYWORD, ')'];
1520
1521
            $parts[] = $support;
1522
            $s = $this->count;
1523
        } else {
1524
            $this->seek($s);
1525
        }
1526
1527
        if (
1528
            $this->matchChar('(') &&
1529
            $this->supportsQuery($subQuery) &&
1530
            $this->matchChar(')')
1531
        ) {
1532
            $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, '('], $subQuery, [Type::T_KEYWORD, ')']]];
1533
            $s = $this->count;
1534
        } else {
1535
            $this->seek($s);
1536
        }
1537
1538
        if (
1539
            $this->literal('not', 3) &&
1540
            $this->supportsQuery($subQuery)
1541
        ) {
1542
            $parts[] = [Type::T_STRING, '', [[Type::T_KEYWORD, 'not '], $subQuery]];
1543
            $s = $this->count;
1544
        } else {
1545
            $this->seek($s);
1546
        }
1547
1548
        if (
1549
            $this->literal('selector(', 9) &&
1550
            $this->selector($selector) &&
1551
            $this->matchChar(')')
1552
        ) {
1553
            $support = [Type::T_STRING, '', [[Type::T_KEYWORD, 'selector(']]];
1554
1555
            $selectorList = [Type::T_LIST, '', []];
1556
1557
            foreach ($selector as $sc) {
1558
                $compound = [Type::T_STRING, '', []];
1559
1560
                foreach ($sc as $scp) {
1561
                    if (\is_array($scp)) {
1562
                        $compound[2][] = $scp;
1563
                    } else {
1564
                        $compound[2][] = [Type::T_KEYWORD, $scp];
1565
                    }
1566
                }
1567
1568
                $selectorList[2][] = $compound;
1569
            }
1570
1571
            $support[2][] = $selectorList;
1572
            $support[2][] = [Type::T_KEYWORD, ')'];
1573
            $parts[] = $support;
1574
            $s = $this->count;
1575
        } else {
1576
            $this->seek($s);
1577
        }
1578
1579
        if ($this->variable($var) or $this->interpolation($var)) {
1580
            $parts[] = $var;
1581
            $s = $this->count;
1582
        } else {
1583
            $this->seek($s);
1584
        }
1585
1586
        if (
1587
            $this->literal('and', 3) &&
1588
            $this->genericList($expressions, 'supportsQuery', ' and', false)
1589
        ) {
1590
            array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1591
1592
            $parts = [$expressions];
1593
            $s = $this->count;
1594
        } else {
1595
            $this->seek($s);
1596
        }
1597
1598
        if (
1599
            $this->literal('or', 2) &&
1600
            $this->genericList($expressions, 'supportsQuery', ' or', false)
1601
        ) {
1602
            array_unshift($expressions[2], [Type::T_STRING, '', $parts]);
1603
1604
            $parts = [$expressions];
1605
            $s = $this->count;
0 ignored issues
show
Unused Code introduced by
The assignment to $s is dead and can be removed.
Loading history...
1606
        } else {
1607
            $this->seek($s);
1608
        }
1609
1610
        if (\count($parts)) {
1611
            if ($this->eatWhiteDefault) {
1612
                $this->whitespace();
1613
            }
1614
1615
            $out = [Type::T_STRING, '', $parts];
1616
1617
            return true;
1618
        }
1619
1620
        return false;
1621
    }
1622
1623
1624
    /**
1625
     * Parse media expression
1626
     *
1627
     * @param array $out
1628
     *
1629
     * @return boolean
1630
     */
1631
    protected function mediaExpression(&$out)
1632
    {
1633
        $s = $this->count;
1634
        $value = null;
1635
1636
        if (
1637
            $this->matchChar('(') &&
1638
            $this->expression($feature) &&
1639
            ($this->matchChar(':') &&
1640
                $this->expression($value) || true) &&
1641
            $this->matchChar(')')
1642
        ) {
1643
            $out = [Type::T_MEDIA_EXPRESSION, $feature];
1644
1645
            if ($value) {
1646
                $out[] = $value;
1647
            }
1648
1649
            return true;
1650
        }
1651
1652
        $this->seek($s);
1653
1654
        return false;
1655
    }
1656
1657
    /**
1658
     * Parse argument values
1659
     *
1660
     * @param array $out
1661
     *
1662
     * @return boolean
1663
     */
1664
    protected function argValues(&$out)
1665
    {
1666
        $discardComments = $this->discardComments;
1667
        $this->discardComments = true;
1668
1669
        if ($this->genericList($list, 'argValue', ',', false)) {
1670
            $out = $list[2];
1671
1672
            $this->discardComments = $discardComments;
1673
1674
            return true;
1675
        }
1676
1677
        $this->discardComments = $discardComments;
1678
1679
        return false;
1680
    }
1681
1682
    /**
1683
     * Parse argument value
1684
     *
1685
     * @param array $out
1686
     *
1687
     * @return boolean
1688
     */
1689
    protected function argValue(&$out)
1690
    {
1691
        $s = $this->count;
1692
1693
        $keyword = null;
1694
1695
        if (! $this->variable($keyword) || ! $this->matchChar(':')) {
1696
            $this->seek($s);
1697
1698
            $keyword = null;
1699
        }
1700
1701
        if ($this->genericList($value, 'expression', '', true)) {
1702
            $out = [$keyword, $value, false];
1703
            $s = $this->count;
1704
1705
            if ($this->literal('...', 3)) {
1706
                $out[2] = true;
1707
            } else {
1708
                $this->seek($s);
1709
            }
1710
1711
            return true;
1712
        }
1713
1714
        return false;
1715
    }
1716
1717
    /**
1718
     * Parse directive value list that considers $vars as keyword
1719
     *
1720
     * @param array          $out
1721
     * @param boolean|string $endChar
1722
     *
1723
     * @return boolean
1724
     */
1725
    protected function directiveValue(&$out, $endChar = false)
1726
    {
1727
        $s = $this->count;
1728
1729
        if ($this->variable($out)) {
1730
            if ($endChar && $this->matchChar($endChar, false)) {
0 ignored issues
show
Bug introduced by
It seems like $endChar can also be of type true; however, parameter $char of ScssPhp\ScssPhp\Parser::matchChar() 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

1730
            if ($endChar && $this->matchChar(/** @scrutinizer ignore-type */ $endChar, false)) {
Loading history...
1731
                return true;
1732
            }
1733
1734
            if (! $endChar && $this->end()) {
1735
                return true;
1736
            }
1737
        }
1738
1739
        $this->seek($s);
1740
1741
        if ($endChar && $this->openString($endChar, $out)) {
0 ignored issues
show
Bug introduced by
It seems like $endChar can also be of type true; however, parameter $end of ScssPhp\ScssPhp\Parser::openString() 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

1741
        if ($endChar && $this->openString(/** @scrutinizer ignore-type */ $endChar, $out)) {
Loading history...
1742
            if ($this->matchChar($endChar, false)) {
1743
                return true;
1744
            }
1745
        }
1746
1747
        $this->seek($s);
1748
1749
        $allowVars = $this->allowVars;
1750
        $this->allowVars = false;
1751
1752
        $res = $this->genericList($out, 'spaceList', ',');
1753
        $this->allowVars = $allowVars;
1754
1755
        if ($res) {
1756
            if ($endChar && $this->matchChar($endChar, false)) {
1757
                return true;
1758
            }
1759
1760
            if (! $endChar && $this->end()) {
1761
                return true;
1762
            }
1763
        }
1764
1765
        $this->seek($s);
1766
1767
        if ($endChar && $this->matchChar($endChar, false)) {
1768
            return true;
1769
        }
1770
1771
        return false;
1772
    }
1773
1774
    /**
1775
     * Parse comma separated value list
1776
     *
1777
     * @param array $out
1778
     *
1779
     * @return boolean
1780
     */
1781
    protected function valueList(&$out)
1782
    {
1783
        $discardComments = $this->discardComments;
1784
        $this->discardComments = true;
1785
        $res = $this->genericList($out, 'spaceList', ',');
1786
        $this->discardComments = $discardComments;
1787
1788
        return $res;
1789
    }
1790
1791
    /**
1792
     * Parse a function call, where externals () are part of the call
1793
     * and not of the value list
1794
     *
1795
     * @param $out
1796
     * @param bool $mandatoryParenthesis
1797
     * @return bool
1798
     */
1799
    protected function functionCallArgumentsList(&$out, $mandatoryParenthesis = true)
1800
    {
1801
        $s = $this->count;
1802
1803
        if (
1804
            $this->matchChar('(') &&
1805
            $this->valueList($out) &&
1806
            $this->matchChar(')')
1807
        ) {
1808
            return true;
1809
        }
1810
1811
        if (! $mandatoryParenthesis) {
1812
            $this->seek($s);
1813
1814
            if ($this->valueList($out)) {
1815
                return true;
1816
            }
1817
        }
1818
1819
        $this->seek($s);
1820
1821
        return false;
1822
    }
1823
1824
    /**
1825
     * Parse space separated value list
1826
     *
1827
     * @param array $out
1828
     *
1829
     * @return boolean
1830
     */
1831
    protected function spaceList(&$out)
1832
    {
1833
        return $this->genericList($out, 'expression');
1834
    }
1835
1836
    /**
1837
     * Parse generic list
1838
     *
1839
     * @param array    $out
1840
     * @param callable $parseItem
1841
     * @param string   $delim
1842
     * @param boolean  $flatten
1843
     *
1844
     * @return boolean
1845
     */
1846
    protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1847
    {
1848
        $s     = $this->count;
1849
        $items = [];
1850
        $value = null;
1851
1852
        while ($this->$parseItem($value)) {
1853
            $trailing_delim = false;
1854
            $items[] = $value;
1855
1856
            if ($delim) {
1857
                if (! $this->literal($delim, \strlen($delim))) {
1858
                    break;
1859
                }
1860
1861
                $trailing_delim = true;
1862
            } else {
1863
                // if no delim watch that a keyword didn't eat the single/double quote
1864
                // from the following starting string
1865
                if ($value[0] === Type::T_KEYWORD) {
1866
                    $word = $value[1];
1867
1868
                    $last_char = substr($word, -1);
1869
1870
                    if (
1871
                        strlen($word) > 1 &&
1872
                        in_array($last_char, [ "'", '"']) &&
1873
                        substr($word, -2, 1) !== '\\'
1874
                    ) {
1875
                        // if there is a non escaped opening quote in the keyword, this seems unlikely a mistake
1876
                        $word = str_replace('\\' . $last_char, '\\\\', $word);
1877
                        if (strpos($word, $last_char) < strlen($word) - 1) {
1878
                            continue;
1879
                        }
1880
1881
                        $currentCount = $this->count;
1882
1883
                        // let's try to rewind to previous char and try a parse
1884
                        $this->count--;
1885
                        // in case the keyword also eat spaces
1886
                        while (substr($this->buffer, $this->count, 1) !== $last_char) {
1887
                            $this->count--;
1888
                        }
1889
1890
                        $nextValue = null;
1891
                        if ($this->$parseItem($nextValue)) {
1892
                            if ($nextValue[0] === Type::T_KEYWORD && $nextValue[1] === $last_char) {
1893
                                // bad try, forget it
1894
                                $this->seek($currentCount);
1895
                                continue;
1896
                            }
1897
                            if ($nextValue[0] !== Type::T_STRING) {
1898
                                // bad try, forget it
1899
                                $this->seek($currentCount);
1900
                                continue;
1901
                            }
1902
1903
                            // OK it was a good idea
1904
                            $value[1] = substr($value[1], 0, -1);
1905
                            array_pop($items);
1906
                            $items[] = $value;
1907
                            $items[] = $nextValue;
1908
                        } else {
1909
                            // bad try, forget it
1910
                            $this->seek($currentCount);
1911
                            continue;
1912
                        }
1913
                    }
1914
                }
1915
            }
1916
        }
1917
1918
        if (! $items) {
1919
            $this->seek($s);
1920
1921
            return false;
1922
        }
1923
1924
        if ($trailing_delim) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $trailing_delim does not seem to be defined for all execution paths leading up to this point.
Loading history...
1925
            $items[] = [Type::T_NULL];
1926
        }
1927
1928
        if ($flatten && \count($items) === 1) {
1929
            $out = $items[0];
1930
        } else {
1931
            $out = [Type::T_LIST, $delim, $items];
1932
        }
1933
1934
        return true;
1935
    }
1936
1937
    /**
1938
     * Parse expression
1939
     *
1940
     * @param array   $out
1941
     * @param boolean $listOnly
1942
     * @param boolean $lookForExp
1943
     *
1944
     * @return boolean
1945
     */
1946
    protected function expression(&$out, $listOnly = false, $lookForExp = true)
1947
    {
1948
        $s = $this->count;
1949
        $discard = $this->discardComments;
1950
        $this->discardComments = true;
1951
        $allowedTypes = ($listOnly ? [Type::T_LIST] : [Type::T_LIST, Type::T_MAP]);
1952
1953
        if ($this->matchChar('(')) {
1954
            if ($this->enclosedExpression($lhs, $s, ')', $allowedTypes)) {
1955
                if ($lookForExp) {
1956
                    $out = $this->expHelper($lhs, 0);
1957
                } else {
1958
                    $out = $lhs;
1959
                }
1960
1961
                $this->discardComments = $discard;
1962
1963
                return true;
1964
            }
1965
1966
            $this->seek($s);
1967
        }
1968
1969
        if (\in_array(Type::T_LIST, $allowedTypes) && $this->matchChar('[')) {
1970
            if ($this->enclosedExpression($lhs, $s, ']', [Type::T_LIST])) {
1971
                if ($lookForExp) {
1972
                    $out = $this->expHelper($lhs, 0);
1973
                } else {
1974
                    $out = $lhs;
1975
                }
1976
1977
                $this->discardComments = $discard;
1978
1979
                return true;
1980
            }
1981
1982
            $this->seek($s);
1983
        }
1984
1985
        if (! $listOnly && $this->value($lhs)) {
1986
            if ($lookForExp) {
1987
                $out = $this->expHelper($lhs, 0);
1988
            } else {
1989
                $out = $lhs;
1990
            }
1991
1992
            $this->discardComments = $discard;
1993
1994
            return true;
1995
        }
1996
1997
        $this->discardComments = $discard;
1998
1999
        return false;
2000
    }
2001
2002
    /**
2003
     * Parse expression specifically checking for lists in parenthesis or brackets
2004
     *
2005
     * @param array   $out
2006
     * @param integer $s
2007
     * @param string  $closingParen
2008
     * @param array   $allowedTypes
2009
     *
2010
     * @return boolean
2011
     */
2012
    protected function enclosedExpression(&$out, $s, $closingParen = ')', $allowedTypes = [Type::T_LIST, Type::T_MAP])
2013
    {
2014
        if ($this->matchChar($closingParen) && \in_array(Type::T_LIST, $allowedTypes)) {
2015
            $out = [Type::T_LIST, '', []];
2016
2017
            switch ($closingParen) {
2018
                case ')':
2019
                    $out['enclosing'] = 'parent'; // parenthesis list
2020
                    break;
2021
2022
                case ']':
2023
                    $out['enclosing'] = 'bracket'; // bracketed list
2024
                    break;
2025
            }
2026
2027
            return true;
2028
        }
2029
2030
        if (
2031
            $this->valueList($out) &&
2032
            $this->matchChar($closingParen) && ! ($closingParen === ')' &&
2033
            \in_array($out[0], [Type::T_EXPRESSION, Type::T_UNARY])) &&
2034
            \in_array(Type::T_LIST, $allowedTypes)
2035
        ) {
2036
            if ($out[0] !== Type::T_LIST || ! empty($out['enclosing'])) {
2037
                $out = [Type::T_LIST, '', [$out]];
2038
            }
2039
2040
            switch ($closingParen) {
2041
                case ')':
2042
                    $out['enclosing'] = 'parent'; // parenthesis list
2043
                    break;
2044
2045
                case ']':
2046
                    $out['enclosing'] = 'bracket'; // bracketed list
2047
                    break;
2048
            }
2049
2050
            return true;
2051
        }
2052
2053
        $this->seek($s);
2054
2055
        if (\in_array(Type::T_MAP, $allowedTypes) && $this->map($out)) {
2056
            return true;
2057
        }
2058
2059
        return false;
2060
    }
2061
2062
    /**
2063
     * Parse left-hand side of subexpression
2064
     *
2065
     * @param array   $lhs
2066
     * @param integer $minP
2067
     *
2068
     * @return array
2069
     */
2070
    protected function expHelper($lhs, $minP)
2071
    {
2072
        $operators = static::$operatorPattern;
2073
2074
        $ss = $this->count;
2075
        $whiteBefore = isset($this->buffer[$this->count - 1]) &&
2076
            ctype_space($this->buffer[$this->count - 1]);
2077
2078
        while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) {
2079
            $whiteAfter = isset($this->buffer[$this->count]) &&
2080
                ctype_space($this->buffer[$this->count]);
2081
            $varAfter = isset($this->buffer[$this->count]) &&
2082
                $this->buffer[$this->count] === '$';
2083
2084
            $this->whitespace();
2085
2086
            $op = $m[1];
2087
2088
            // don't turn negative numbers into expressions
2089
            if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
2090
                break;
2091
            }
2092
2093
            if (! $this->value($rhs) && ! $this->expression($rhs, true, false)) {
2094
                break;
2095
            }
2096
2097
            // peek and see if rhs belongs to next operator
2098
            if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
2099
                $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
2100
            }
2101
2102
            $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
2103
            $ss = $this->count;
2104
            $whiteBefore = isset($this->buffer[$this->count - 1]) &&
2105
                ctype_space($this->buffer[$this->count - 1]);
2106
        }
2107
2108
        $this->seek($ss);
2109
2110
        return $lhs;
2111
    }
2112
2113
    /**
2114
     * Parse value
2115
     *
2116
     * @param array $out
2117
     *
2118
     * @return boolean
2119
     */
2120
    protected function value(&$out)
2121
    {
2122
        if (! isset($this->buffer[$this->count])) {
2123
            return false;
2124
        }
2125
2126
        $s = $this->count;
2127
        $char = $this->buffer[$this->count];
2128
2129
        if (
2130
            $this->literal('url(', 4) &&
2131
            $this->match('data:([a-z]+)\/([a-z0-9.+-]+);base64,', $m, false)
2132
        ) {
2133
            $len = strspn(
2134
                $this->buffer,
2135
                'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwyxz0123456789+/=',
2136
                $this->count
2137
            );
2138
2139
            $this->count += $len;
2140
2141
            if ($this->matchChar(')')) {
2142
                $content = substr($this->buffer, $s, $this->count - $s);
2143
                $out = [Type::T_KEYWORD, $content];
2144
2145
                return true;
2146
            }
2147
        }
2148
2149
        $this->seek($s);
2150
2151
        if (
2152
            $this->literal('url(', 4, false) &&
2153
            $this->match('\s*(\/\/[^\s\)]+)\s*', $m)
2154
        ) {
2155
            $content = 'url(' . $m[1];
2156
2157
            if ($this->matchChar(')')) {
2158
                $content .= ')';
2159
                $out = [Type::T_KEYWORD, $content];
2160
2161
                return true;
2162
            }
2163
        }
2164
2165
        $this->seek($s);
2166
2167
        // not
2168
        if ($char === 'n' && $this->literal('not', 3, false)) {
2169
            if (
2170
                $this->whitespace() &&
2171
                $this->value($inner)
2172
            ) {
2173
                $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
2174
2175
                return true;
2176
            }
2177
2178
            $this->seek($s);
2179
2180
            if ($this->parenValue($inner)) {
2181
                $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
2182
2183
                return true;
2184
            }
2185
2186
            $this->seek($s);
2187
        }
2188
2189
        // addition
2190
        if ($char === '+') {
2191
            $this->count++;
2192
2193
            $follow_white = $this->whitespace();
2194
2195
            if ($this->value($inner)) {
2196
                $out = [Type::T_UNARY, '+', $inner, $this->inParens];
2197
2198
                return true;
2199
            }
2200
2201
            if ($follow_white) {
2202
                $out = [Type::T_KEYWORD, $char];
2203
                return  true;
2204
            }
2205
2206
            $this->seek($s);
2207
2208
            return false;
2209
        }
2210
2211
        // negation
2212
        if ($char === '-') {
2213
            if ($this->customProperty($out)) {
2214
                return true;
2215
            }
2216
2217
            $this->count++;
2218
2219
            $follow_white = $this->whitespace();
2220
2221
            if ($this->variable($inner) || $this->unit($inner) || $this->parenValue($inner)) {
2222
                $out = [Type::T_UNARY, '-', $inner, $this->inParens];
2223
2224
                return true;
2225
            }
2226
2227
            if (
2228
                $this->keyword($inner) &&
2229
                ! $this->func($inner, $out)
2230
            ) {
2231
                $out = [Type::T_UNARY, '-', $inner, $this->inParens];
2232
2233
                return true;
2234
            }
2235
2236
            if ($follow_white) {
2237
                $out = [Type::T_KEYWORD, $char];
2238
2239
                return  true;
2240
            }
2241
2242
            $this->seek($s);
2243
        }
2244
2245
        // paren
2246
        if ($char === '(' && $this->parenValue($out)) {
2247
            return true;
2248
        }
2249
2250
        if ($char === '#') {
2251
            if ($this->interpolation($out) || $this->color($out)) {
2252
                return true;
2253
            }
2254
2255
            $this->count++;
2256
2257
            if ($this->keyword($keyword)) {
2258
                $out = [Type::T_KEYWORD, '#' . $keyword];
2259
2260
                return true;
2261
            }
2262
2263
            $this->count--;
2264
        }
2265
2266
        if ($this->matchChar('&', true)) {
2267
            $out = [Type::T_SELF];
2268
2269
            return true;
2270
        }
2271
2272
        if ($char === '$' && $this->variable($out)) {
2273
            return true;
2274
        }
2275
2276
        if ($char === 'p' && $this->progid($out)) {
2277
            return true;
2278
        }
2279
2280
        if (($char === '"' || $char === "'") && $this->string($out)) {
2281
            return true;
2282
        }
2283
2284
        if ($this->unit($out)) {
2285
            return true;
2286
        }
2287
2288
        // unicode range with wildcards
2289
        if (
2290
            $this->literal('U+', 2) &&
2291
            $this->match('([0-9A-F]+\?*)(-([0-9A-F]+))?', $m, false)
2292
        ) {
2293
            $out = [Type::T_KEYWORD, 'U+' . $m[0]];
2294
2295
            return true;
2296
        }
2297
2298
        if ($this->keyword($keyword, false)) {
2299
            if ($this->func($keyword, $out)) {
2300
                return true;
2301
            }
2302
2303
            $this->whitespace();
2304
2305
            if ($keyword === 'null') {
2306
                $out = [Type::T_NULL];
2307
            } else {
2308
                $out = [Type::T_KEYWORD, $keyword];
2309
            }
2310
2311
            return true;
2312
        }
2313
2314
        return false;
2315
    }
2316
2317
    /**
2318
     * Parse parenthesized value
2319
     *
2320
     * @param array $out
2321
     *
2322
     * @return boolean
2323
     */
2324
    protected function parenValue(&$out)
2325
    {
2326
        $s = $this->count;
2327
2328
        $inParens = $this->inParens;
2329
2330
        if ($this->matchChar('(')) {
2331
            if ($this->matchChar(')')) {
2332
                $out = [Type::T_LIST, '', []];
2333
2334
                return true;
2335
            }
2336
2337
            $this->inParens = true;
2338
2339
            if (
2340
                $this->expression($exp) &&
2341
                $this->matchChar(')')
2342
            ) {
2343
                $out = $exp;
2344
                $this->inParens = $inParens;
2345
2346
                return true;
2347
            }
2348
        }
2349
2350
        $this->inParens = $inParens;
2351
        $this->seek($s);
2352
2353
        return false;
2354
    }
2355
2356
    /**
2357
     * Parse "progid:"
2358
     *
2359
     * @param array $out
2360
     *
2361
     * @return boolean
2362
     */
2363
    protected function progid(&$out)
2364
    {
2365
        $s = $this->count;
2366
2367
        if (
2368
            $this->literal('progid:', 7, false) &&
2369
            $this->openString('(', $fn) &&
2370
            $this->matchChar('(')
2371
        ) {
2372
            $this->openString(')', $args, '(');
2373
2374
            if ($this->matchChar(')')) {
2375
                $out = [Type::T_STRING, '', [
2376
                    'progid:', $fn, '(', $args, ')'
2377
                ]];
2378
2379
                return true;
2380
            }
2381
        }
2382
2383
        $this->seek($s);
2384
2385
        return false;
2386
    }
2387
2388
    /**
2389
     * Parse function call
2390
     *
2391
     * @param string $name
2392
     * @param array  $func
2393
     *
2394
     * @return boolean
2395
     */
2396
    protected function func($name, &$func)
2397
    {
2398
        $s = $this->count;
2399
2400
        if ($this->matchChar('(')) {
2401
            if ($name === 'alpha' && $this->argumentList($args)) {
2402
                $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
2403
2404
                return true;
2405
            }
2406
2407
            if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
2408
                $ss = $this->count;
2409
2410
                if (
2411
                    $this->argValues($args) &&
2412
                    $this->matchChar(')')
2413
                ) {
2414
                    $func = [Type::T_FUNCTION_CALL, $name, $args];
2415
2416
                    return true;
2417
                }
2418
2419
                $this->seek($ss);
2420
            }
2421
2422
            if (
2423
                ($this->openString(')', $str, '(') || true) &&
2424
                $this->matchChar(')')
2425
            ) {
2426
                $args = [];
2427
2428
                if (! empty($str)) {
2429
                    $args[] = [null, [Type::T_STRING, '', [$str]]];
2430
                }
2431
2432
                $func = [Type::T_FUNCTION_CALL, $name, $args];
2433
2434
                return true;
2435
            }
2436
        }
2437
2438
        $this->seek($s);
2439
2440
        return false;
2441
    }
2442
2443
    /**
2444
     * Parse function call argument list
2445
     *
2446
     * @param array $out
2447
     *
2448
     * @return boolean
2449
     */
2450
    protected function argumentList(&$out)
2451
    {
2452
        $s = $this->count;
2453
        $this->matchChar('(');
2454
2455
        $args = [];
2456
2457
        while ($this->keyword($var)) {
2458
            if (
2459
                $this->matchChar('=') &&
2460
                $this->expression($exp)
2461
            ) {
2462
                $args[] = [Type::T_STRING, '', [$var . '=']];
2463
                $arg = $exp;
2464
            } else {
2465
                break;
2466
            }
2467
2468
            $args[] = $arg;
2469
2470
            if (! $this->matchChar(',')) {
2471
                break;
2472
            }
2473
2474
            $args[] = [Type::T_STRING, '', [', ']];
2475
        }
2476
2477
        if (! $this->matchChar(')') || ! $args) {
2478
            $this->seek($s);
2479
2480
            return false;
2481
        }
2482
2483
        $out = $args;
2484
2485
        return true;
2486
    }
2487
2488
    /**
2489
     * Parse mixin/function definition  argument list
2490
     *
2491
     * @param array $out
2492
     *
2493
     * @return boolean
2494
     */
2495
    protected function argumentDef(&$out)
2496
    {
2497
        $s = $this->count;
2498
        $this->matchChar('(');
2499
2500
        $args = [];
2501
2502
        while ($this->variable($var)) {
2503
            $arg = [$var[1], null, false];
2504
2505
            $ss = $this->count;
2506
2507
            if (
2508
                $this->matchChar(':') &&
2509
                $this->genericList($defaultVal, 'expression', '', true)
2510
            ) {
2511
                $arg[1] = $defaultVal;
2512
            } else {
2513
                $this->seek($ss);
2514
            }
2515
2516
            $ss = $this->count;
2517
2518
            if ($this->literal('...', 3)) {
2519
                $sss = $this->count;
2520
2521
                if (! $this->matchChar(')')) {
2522
                    $this->throwParseError('... has to be after the final argument');
2523
                }
2524
2525
                $arg[2] = true;
2526
2527
                $this->seek($sss);
2528
            } else {
2529
                $this->seek($ss);
2530
            }
2531
2532
            $args[] = $arg;
2533
2534
            if (! $this->matchChar(',')) {
2535
                break;
2536
            }
2537
        }
2538
2539
        if (! $this->matchChar(')')) {
2540
            $this->seek($s);
2541
2542
            return false;
2543
        }
2544
2545
        $out = $args;
2546
2547
        return true;
2548
    }
2549
2550
    /**
2551
     * Parse map
2552
     *
2553
     * @param array $out
2554
     *
2555
     * @return boolean
2556
     */
2557
    protected function map(&$out)
2558
    {
2559
        $s = $this->count;
2560
2561
        if (! $this->matchChar('(')) {
2562
            return false;
2563
        }
2564
2565
        $keys = [];
2566
        $values = [];
2567
2568
        while (
2569
            $this->genericList($key, 'expression', '', true) &&
2570
            $this->matchChar(':') &&
2571
            $this->genericList($value, 'expression', '', true)
2572
        ) {
2573
            $keys[] = $key;
2574
            $values[] = $value;
2575
2576
            if (! $this->matchChar(',')) {
2577
                break;
2578
            }
2579
        }
2580
2581
        if (! $keys || ! $this->matchChar(')')) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $keys 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...
introduced by
$keys is an empty array, thus ! $keys is always true.
Loading history...
2582
            $this->seek($s);
2583
2584
            return false;
2585
        }
2586
2587
        $out = [Type::T_MAP, $keys, $values];
2588
2589
        return true;
2590
    }
2591
2592
    /**
2593
     * Parse color
2594
     *
2595
     * @param array $out
2596
     *
2597
     * @return boolean
2598
     */
2599
    protected function color(&$out)
2600
    {
2601
        $s = $this->count;
2602
2603
        if ($this->match('(#([0-9a-f]+)\b)', $m)) {
2604
            if (\in_array(\strlen($m[2]), [3,4,6,8])) {
2605
                $out = [Type::T_KEYWORD, $m[0]];
2606
2607
                return true;
2608
            }
2609
2610
            $this->seek($s);
2611
2612
            return false;
2613
        }
2614
2615
        return false;
2616
    }
2617
2618
    /**
2619
     * Parse number with unit
2620
     *
2621
     * @param array $unit
2622
     *
2623
     * @return boolean
2624
     */
2625
    protected function unit(&$unit)
2626
    {
2627
        $s = $this->count;
2628
2629
        if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m, false)) {
2630
            if (\strlen($this->buffer) === $this->count || ! ctype_digit($this->buffer[$this->count])) {
2631
                $this->whitespace();
2632
2633
                $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
2634
2635
                return true;
2636
            }
2637
2638
            $this->seek($s);
2639
        }
2640
2641
        return false;
2642
    }
2643
2644
    /**
2645
     * Parse string
2646
     *
2647
     * @param array $out
2648
     *
2649
     * @return boolean
2650
     */
2651
    protected function string(&$out)
2652
    {
2653
        $s = $this->count;
2654
2655
        if ($this->matchChar('"', false)) {
2656
            $delim = '"';
2657
        } elseif ($this->matchChar("'", false)) {
2658
            $delim = "'";
2659
        } else {
2660
            return false;
2661
        }
2662
2663
        $content = [];
2664
        $oldWhite = $this->eatWhiteDefault;
2665
        $this->eatWhiteDefault = false;
2666
        $hasInterpolation = false;
2667
2668
        while ($this->matchString($m, $delim)) {
2669
            if ($m[1] !== '') {
2670
                $content[] = $m[1];
2671
            }
2672
2673
            if ($m[2] === '#{') {
2674
                $this->count -= \strlen($m[2]);
2675
2676
                if ($this->interpolation($inter, false)) {
2677
                    $content[] = $inter;
2678
                    $hasInterpolation = true;
2679
                } else {
2680
                    $this->count += \strlen($m[2]);
2681
                    $content[] = '#{'; // ignore it
2682
                }
2683
            } elseif ($m[2] === "\r") {
2684
                $content[] = '\\a';
2685
                // TODO : warning
2686
                # DEPRECATION WARNING on line x, column y of zzz:
2687
                # Unescaped multiline strings are deprecated and will be removed in a future version of Sass.
2688
                # To include a newline in a string, use "\a" or "\a " as in CSS.
2689
                if ($this->matchChar("\n", false)) {
2690
                    $content[] = ' ';
2691
                }
2692
            } elseif ($m[2] === '\\') {
2693
                if (
2694
                    $this->literal("\r\n", 2, false) ||
2695
                    $this->matchChar("\r", false) ||
2696
                    $this->matchChar("\n", false) ||
2697
                    $this->matchChar("\f", false)
2698
                ) {
2699
                    // this is a continuation escaping, to be ignored
2700
                } elseif ($this->matchEscapeCharacter($c)) {
2701
                    $content[] = $c;
2702
                } else {
2703
                    $this->throwParseError('Unterminated escape sequence');
2704
                }
2705
            } else {
2706
                $this->count -= \strlen($delim);
2707
                break; // delim
2708
            }
2709
        }
2710
2711
        $this->eatWhiteDefault = $oldWhite;
2712
2713
        if ($this->literal($delim, \strlen($delim))) {
2714
            if ($hasInterpolation) {
2715
                $delim = '"';
2716
            }
2717
2718
            $out = [Type::T_STRING, $delim, $content];
2719
2720
            return true;
2721
        }
2722
2723
        $this->seek($s);
2724
2725
        return false;
2726
    }
2727
2728
    protected function matchEscapeCharacter(&$out)
2729
    {
2730
        if ($this->match('[a-f0-9]', $m, false)) {
2731
            $hex = $m[0];
2732
2733
            for ($i = 5; $i--;) {
2734
                if ($this->match('[a-f0-9]', $m, false)) {
2735
                    $hex .= $m[0];
2736
                } else {
2737
                    break;
2738
                }
2739
            }
2740
2741
            $value = hexdec($hex);
2742
2743
            if ($value == 0 || ($value >= 0xD800 && $value <= 0xDFFF) || $value >= 0x10FFFF) {
2744
                $out = "\u{FFFD}";
2745
            } else {
2746
                $out = Util::mbChr($value);
0 ignored issues
show
Bug introduced by
It seems like $value can also be of type double; however, parameter $code of ScssPhp\ScssPhp\Util::mbChr() 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

2746
                $out = Util::mbChr(/** @scrutinizer ignore-type */ $value);
Loading history...
2747
            }
2748
2749
            return true;
2750
        }
2751
2752
        if ($this->match('.', $m, false)) {
2753
            $out = $m[0];
2754
2755
            return true;
2756
        }
2757
2758
        return false;
2759
    }
2760
2761
    /**
2762
     * Parse keyword or interpolation
2763
     *
2764
     * @param array   $out
2765
     * @param boolean $restricted
2766
     *
2767
     * @return boolean
2768
     */
2769
    protected function mixedKeyword(&$out, $restricted = false)
2770
    {
2771
        $parts = [];
2772
2773
        $oldWhite = $this->eatWhiteDefault;
2774
        $this->eatWhiteDefault = false;
2775
2776
        for (;;) {
2777
            if ($restricted ? $this->restrictedKeyword($key) : $this->keyword($key)) {
2778
                $parts[] = $key;
2779
                continue;
2780
            }
2781
2782
            if ($this->interpolation($inter)) {
2783
                $parts[] = $inter;
2784
                continue;
2785
            }
2786
2787
            break;
2788
        }
2789
2790
        $this->eatWhiteDefault = $oldWhite;
2791
2792
        if (! $parts) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts 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...
introduced by
$parts is an empty array, thus ! $parts is always true.
Loading history...
2793
            return false;
2794
        }
2795
2796
        if ($this->eatWhiteDefault) {
2797
            $this->whitespace();
2798
        }
2799
2800
        $out = $parts;
2801
2802
        return true;
2803
    }
2804
2805
    /**
2806
     * Parse an unbounded string stopped by $end
2807
     *
2808
     * @param string  $end
2809
     * @param array   $out
2810
     * @param string  $nestingOpen
2811
     * @param string  $nestingClose
2812
     * @param boolean $trimEnd
2813
     *
2814
     * @return boolean
2815
     */
2816
    protected function openString($end, &$out, $nestingOpen = null, $nestingClose = null, $trimEnd = true)
2817
    {
2818
        $oldWhite = $this->eatWhiteDefault;
2819
        $this->eatWhiteDefault = false;
2820
2821
        if ($nestingOpen && ! $nestingClose) {
2822
            $nestingClose = $end;
2823
        }
2824
2825
        $patt = '(.*?)([\'"]|#\{|'
2826
            . $this->pregQuote($end) . '|'
2827
            . (($nestingClose && $nestingClose !== $end) ? $this->pregQuote($nestingClose) . '|' : '')
2828
            . static::$commentPattern . ')';
2829
2830
        $nestingLevel = 0;
2831
2832
        $content = [];
2833
2834
        while ($this->match($patt, $m, false)) {
2835
            if (isset($m[1]) && $m[1] !== '') {
2836
                $content[] = $m[1];
2837
2838
                if ($nestingOpen) {
2839
                    $nestingLevel += substr_count($m[1], $nestingOpen);
2840
                }
2841
            }
2842
2843
            $tok = $m[2];
2844
2845
            $this->count -= \strlen($tok);
2846
2847
            if ($tok === $end && ! $nestingLevel) {
2848
                break;
2849
            }
2850
2851
            if ($tok === $nestingClose) {
2852
                $nestingLevel--;
2853
            }
2854
2855
            if (($tok === "'" || $tok === '"') && $this->string($str)) {
2856
                $content[] = $str;
2857
                continue;
2858
            }
2859
2860
            if ($tok === '#{' && $this->interpolation($inter)) {
2861
                $content[] = $inter;
2862
                continue;
2863
            }
2864
2865
            $content[] = $tok;
2866
            $this->count += \strlen($tok);
2867
        }
2868
2869
        $this->eatWhiteDefault = $oldWhite;
2870
2871
        if (! $content || $tok !== $end) {
0 ignored issues
show
Comprehensibility Best Practice introduced by
The variable $tok does not seem to be defined for all execution paths leading up to this point.
Loading history...
2872
            return false;
2873
        }
2874
2875
        // trim the end
2876
        if ($trimEnd && \is_string(end($content))) {
2877
            $content[\count($content) - 1] = rtrim(end($content));
2878
        }
2879
2880
        $out = [Type::T_STRING, '', $content];
2881
2882
        return true;
2883
    }
2884
2885
    /**
2886
     * Parser interpolation
2887
     *
2888
     * @param string|array $out
2889
     * @param boolean      $lookWhite save information about whitespace before and after
2890
     *
2891
     * @return boolean
2892
     */
2893
    protected function interpolation(&$out, $lookWhite = true)
2894
    {
2895
        $oldWhite = $this->eatWhiteDefault;
2896
        $allowVars = $this->allowVars;
2897
        $this->allowVars = true;
2898
        $this->eatWhiteDefault = true;
2899
2900
        $s = $this->count;
2901
2902
        if (
2903
            $this->literal('#{', 2) &&
2904
            $this->valueList($value) &&
2905
            $this->matchChar('}', false)
2906
        ) {
2907
            if ($value === [Type::T_SELF]) {
2908
                $out = $value;
2909
            } else {
2910
                if ($lookWhite) {
2911
                    $left = ($s > 0 && preg_match('/\s/', $this->buffer[$s - 1])) ? ' ' : '';
2912
                    $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ' : '';
2913
                } else {
2914
                    $left = $right = false;
2915
                }
2916
2917
                $out = [Type::T_INTERPOLATE, $value, $left, $right];
2918
            }
2919
2920
            $this->eatWhiteDefault = $oldWhite;
2921
            $this->allowVars = $allowVars;
2922
2923
            if ($this->eatWhiteDefault) {
2924
                $this->whitespace();
2925
            }
2926
2927
            return true;
2928
        }
2929
2930
        $this->seek($s);
2931
2932
        $this->eatWhiteDefault = $oldWhite;
2933
        $this->allowVars = $allowVars;
2934
2935
        return false;
2936
    }
2937
2938
    /**
2939
     * Parse property name (as an array of parts or a string)
2940
     *
2941
     * @param array $out
2942
     *
2943
     * @return boolean
2944
     */
2945
    protected function propertyName(&$out)
2946
    {
2947
        $parts = [];
2948
2949
        $oldWhite = $this->eatWhiteDefault;
2950
        $this->eatWhiteDefault = false;
2951
2952
        for (;;) {
2953
            if ($this->interpolation($inter)) {
2954
                $parts[] = $inter;
2955
                continue;
2956
            }
2957
2958
            if ($this->keyword($text)) {
2959
                $parts[] = $text;
2960
                continue;
2961
            }
2962
2963
            if (! $parts && $this->match('[:.#]', $m, false)) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $parts 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...
2964
                // css hacks
2965
                $parts[] = $m[0];
2966
                continue;
2967
            }
2968
2969
            break;
2970
        }
2971
2972
        $this->eatWhiteDefault = $oldWhite;
2973
2974
        if (! $parts) {
2975
            return false;
2976
        }
2977
2978
        // match comment hack
2979
        if (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
2980
            if (! empty($m[0])) {
2981
                $parts[] = $m[0];
2982
                $this->count += \strlen($m[0]);
2983
            }
2984
        }
2985
2986
        $this->whitespace(); // get any extra whitespace
2987
2988
        $out = [Type::T_STRING, '', $parts];
2989
2990
        return true;
2991
    }
2992
2993
    /**
2994
     * Parse custom property name (as an array of parts or a string)
2995
     *
2996
     * @param array $out
2997
     *
2998
     * @return boolean
2999
     */
3000
    protected function customProperty(&$out)
3001
    {
3002
        $s = $this->count;
3003
3004
        if (! $this->literal('--', 2, false)) {
3005
            return false;
3006
        }
3007
3008
        $parts = ['--'];
3009
3010
        $oldWhite = $this->eatWhiteDefault;
3011
        $this->eatWhiteDefault = false;
3012
3013
        for (;;) {
3014
            if ($this->interpolation($inter)) {
3015
                $parts[] = $inter;
3016
                continue;
3017
            }
3018
3019
            if ($this->matchChar('&', false)) {
3020
                $parts[] = [Type::T_SELF];
3021
                continue;
3022
            }
3023
3024
            if ($this->variable($var)) {
3025
                $parts[] = $var;
3026
                continue;
3027
            }
3028
3029
            if ($this->keyword($text)) {
3030
                $parts[] = $text;
3031
                continue;
3032
            }
3033
3034
            break;
3035
        }
3036
3037
        $this->eatWhiteDefault = $oldWhite;
3038
3039
        if (\count($parts) == 1) {
3040
            $this->seek($s);
3041
3042
            return false;
3043
        }
3044
3045
        $this->whitespace(); // get any extra whitespace
3046
3047
        $out = [Type::T_STRING, '', $parts];
3048
3049
        return true;
3050
    }
3051
3052
    /**
3053
     * Parse comma separated selector list
3054
     *
3055
     * @param array   $out
3056
     * @param boolean $subSelector
3057
     *
3058
     * @return boolean
3059
     */
3060
    protected function selectors(&$out, $subSelector = false)
3061
    {
3062
        $s = $this->count;
3063
        $selectors = [];
3064
3065
        while ($this->selector($sel, $subSelector)) {
3066
            $selectors[] = $sel;
3067
3068
            if (! $this->matchChar(',', true)) {
3069
                break;
3070
            }
3071
3072
            while ($this->matchChar(',', true)) {
3073
                ; // ignore extra
3074
            }
3075
        }
3076
3077
        if (! $selectors) {
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...
introduced by
$selectors is an empty array, thus ! $selectors is always true.
Loading history...
3078
            $this->seek($s);
3079
3080
            return false;
3081
        }
3082
3083
        $out = $selectors;
3084
3085
        return true;
3086
    }
3087
3088
    /**
3089
     * Parse whitespace separated selector list
3090
     *
3091
     * @param array   $out
3092
     * @param boolean $subSelector
3093
     *
3094
     * @return boolean
3095
     */
3096
    protected function selector(&$out, $subSelector = false)
3097
    {
3098
        $selector = [];
3099
3100
        for (;;) {
3101
            $s = $this->count;
3102
3103
            if ($this->match('[>+~]+', $m, true)) {
3104
                if (
3105
                    $subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0 &&
3106
                    $m[0] === '+' && $this->match("(\d+|n\b)", $counter)
3107
                ) {
3108
                    $this->seek($s);
3109
                } else {
3110
                    $selector[] = [$m[0]];
3111
                    continue;
3112
                }
3113
            }
3114
3115
            if ($this->selectorSingle($part, $subSelector)) {
0 ignored issues
show
Bug introduced by
It seems like $subSelector can also be of type string; however, parameter $subSelector of ScssPhp\ScssPhp\Parser::selectorSingle() does only seem to accept boolean, 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

3115
            if ($this->selectorSingle($part, /** @scrutinizer ignore-type */ $subSelector)) {
Loading history...
3116
                $selector[] = $part;
3117
                $this->match('\s+', $m);
3118
                continue;
3119
            }
3120
3121
            if ($this->match('\/[^\/]+\/', $m, true)) {
3122
                $selector[] = [$m[0]];
3123
                continue;
3124
            }
3125
3126
            break;
3127
        }
3128
3129
        if (! $selector) {
3130
            return false;
3131
        }
3132
3133
        $out = $selector;
3134
3135
        return true;
3136
    }
3137
3138
    /**
3139
     * Parse the parts that make up a selector
3140
     *
3141
     * {@internal
3142
     *     div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
3143
     * }}
3144
     *
3145
     * @param array   $out
3146
     * @param boolean $subSelector
3147
     *
3148
     * @return boolean
3149
     */
3150
    protected function selectorSingle(&$out, $subSelector = false)
3151
    {
3152
        $oldWhite = $this->eatWhiteDefault;
3153
        $this->eatWhiteDefault = false;
3154
3155
        $parts = [];
3156
3157
        if ($this->matchChar('*', false)) {
3158
            $parts[] = '*';
3159
        }
3160
3161
        for (;;) {
3162
            if (! isset($this->buffer[$this->count])) {
3163
                break;
3164
            }
3165
3166
            $s = $this->count;
3167
            $char = $this->buffer[$this->count];
3168
3169
            // see if we can stop early
3170
            if ($char === '{' || $char === ',' || $char === ';' || $char === '}' || $char === '@') {
3171
                break;
3172
            }
3173
3174
            // parsing a sub selector in () stop with the closing )
3175
            if ($subSelector && $char === ')') {
3176
                break;
3177
            }
3178
3179
            //self
3180
            switch ($char) {
3181
                case '&':
3182
                    $parts[] = Compiler::$selfSelector;
3183
                    $this->count++;
3184
                    continue 2;
3185
3186
                case '.':
3187
                    $parts[] = '.';
3188
                    $this->count++;
3189
                    continue 2;
3190
3191
                case '|':
3192
                    $parts[] = '|';
3193
                    $this->count++;
3194
                    continue 2;
3195
            }
3196
3197
            if ($char === '\\' && $this->match('\\\\\S', $m)) {
3198
                $parts[] = $m[0];
3199
                continue;
3200
            }
3201
3202
            if ($char === '%') {
3203
                $this->count++;
3204
3205
                if ($this->placeholder($placeholder)) {
3206
                    $parts[] = '%';
3207
                    $parts[] = $placeholder;
3208
                    continue;
3209
                }
3210
3211
                break;
3212
            }
3213
3214
            if ($char === '#') {
3215
                if ($this->interpolation($inter)) {
3216
                    $parts[] = $inter;
3217
                    continue;
3218
                }
3219
3220
                $parts[] = '#';
3221
                $this->count++;
3222
                continue;
3223
            }
3224
3225
            // a pseudo selector
3226
            if ($char === ':') {
3227
                if ($this->buffer[$this->count + 1] === ':') {
3228
                    $this->count += 2;
3229
                    $part = '::';
3230
                } else {
3231
                    $this->count++;
3232
                    $part = ':';
3233
                }
3234
3235
                if ($this->mixedKeyword($nameParts, true)) {
3236
                    $parts[] = $part;
3237
3238
                    foreach ($nameParts as $sub) {
3239
                        $parts[] = $sub;
3240
                    }
3241
3242
                    $ss = $this->count;
3243
3244
                    if (
3245
                        $nameParts === ['not'] ||
3246
                        $nameParts === ['is'] ||
3247
                        $nameParts === ['has'] ||
3248
                        $nameParts === ['where'] ||
3249
                        $nameParts === ['slotted'] ||
3250
                        $nameParts === ['nth-child'] ||
3251
                        $nameParts === ['nth-last-child'] ||
3252
                        $nameParts === ['nth-of-type'] ||
3253
                        $nameParts === ['nth-last-of-type']
3254
                    ) {
3255
                        if (
3256
                            $this->matchChar('(', true) &&
3257
                            ($this->selectors($subs, reset($nameParts)) || true) &&
3258
                            $this->matchChar(')')
3259
                        ) {
3260
                            $parts[] = '(';
3261
3262
                            while ($sub = array_shift($subs)) {
3263
                                while ($ps = array_shift($sub)) {
3264
                                    foreach ($ps as &$p) {
3265
                                        $parts[] = $p;
3266
                                    }
3267
3268
                                    if (\count($sub) && reset($sub)) {
3269
                                        $parts[] = ' ';
3270
                                    }
3271
                                }
3272
3273
                                if (\count($subs) && reset($subs)) {
3274
                                    $parts[] = ', ';
3275
                                }
3276
                            }
3277
3278
                            $parts[] = ')';
3279
                        } else {
3280
                            $this->seek($ss);
3281
                        }
3282
                    } elseif (
3283
                        $this->matchChar('(') &&
3284
                        ($this->openString(')', $str, '(') || true) &&
3285
                        $this->matchChar(')')
3286
                    ) {
3287
                        $parts[] = '(';
3288
3289
                        if (! empty($str)) {
3290
                            $parts[] = $str;
3291
                        }
3292
3293
                        $parts[] = ')';
3294
                    } else {
3295
                        $this->seek($ss);
3296
                    }
3297
3298
                    continue;
3299
                }
3300
            }
3301
3302
            $this->seek($s);
3303
3304
            // 2n+1
3305
            if ($subSelector && \is_string($subSelector) && strpos($subSelector, 'nth-') === 0) {
3306
                if ($this->match("(\s*(\+\s*|\-\s*)?(\d+|n|\d+n))+", $counter)) {
3307
                    $parts[] = $counter[0];
3308
                    //$parts[] = str_replace(' ', '', $counter[0]);
3309
                    continue;
3310
                }
3311
            }
3312
3313
            $this->seek($s);
3314
3315
            // attribute selector
3316
            if (
3317
                $char === '[' &&
3318
                $this->matchChar('[') &&
3319
                ($this->openString(']', $str, '[') || true) &&
3320
                $this->matchChar(']')
3321
            ) {
3322
                $parts[] = '[';
3323
3324
                if (! empty($str)) {
3325
                    $parts[] = $str;
3326
                }
3327
3328
                $parts[] = ']';
3329
                continue;
3330
            }
3331
3332
            $this->seek($s);
3333
3334
            // for keyframes
3335
            if ($this->unit($unit)) {
3336
                $parts[] = $unit;
3337
                continue;
3338
            }
3339
3340
            if ($this->restrictedKeyword($name)) {
3341
                $parts[] = $name;
3342
                continue;
3343
            }
3344
3345
            break;
3346
        }
3347
3348
        $this->eatWhiteDefault = $oldWhite;
3349
3350
        if (! $parts) {
3351
            return false;
3352
        }
3353
3354
        $out = $parts;
3355
3356
        return true;
3357
    }
3358
3359
    /**
3360
     * Parse a variable
3361
     *
3362
     * @param array $out
3363
     *
3364
     * @return boolean
3365
     */
3366
    protected function variable(&$out)
3367
    {
3368
        $s = $this->count;
3369
3370
        if (
3371
            $this->matchChar('$', false) &&
3372
            $this->keyword($name)
3373
        ) {
3374
            if ($this->allowVars) {
3375
                $out = [Type::T_VARIABLE, $name];
3376
            } else {
3377
                $out = [Type::T_KEYWORD, '$' . $name];
3378
            }
3379
3380
            return true;
3381
        }
3382
3383
        $this->seek($s);
3384
3385
        return false;
3386
    }
3387
3388
    /**
3389
     * Parse a keyword
3390
     *
3391
     * @param string  $word
3392
     * @param boolean $eatWhitespace
3393
     *
3394
     * @return boolean
3395
     */
3396
    protected function keyword(&$word, $eatWhitespace = null)
3397
    {
3398
        $match = $this->match(
3399
            $this->utf8
3400
                ? '(([\pL\w\x{00A0}-\x{10FFFF}_\-\*!"\']|[\\\\].)([\pL\w\x{00A0}-\x{10FFFF}\-_"\']|[\\\\].)*)'
3401
                : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
3402
            $m,
3403
            $eatWhitespace
3404
        );
3405
3406
        if ($match) {
3407
            $word = $m[1];
3408
3409
            return true;
3410
        }
3411
3412
        return false;
3413
    }
3414
3415
    /**
3416
     * Parse a keyword that should not start with a number
3417
     *
3418
     * @param string  $word
3419
     * @param boolean $eatWhitespace
3420
     *
3421
     * @return boolean
3422
     */
3423
    protected function restrictedKeyword(&$word, $eatWhitespace = null)
3424
    {
3425
        $s = $this->count;
3426
3427
        if ($this->keyword($word, $eatWhitespace) && (\ord($word[0]) > 57 || \ord($word[0]) < 48)) {
3428
            return true;
3429
        }
3430
3431
        $this->seek($s);
3432
3433
        return false;
3434
    }
3435
3436
    /**
3437
     * Parse a placeholder
3438
     *
3439
     * @param string|array $placeholder
3440
     *
3441
     * @return boolean
3442
     */
3443
    protected function placeholder(&$placeholder)
3444
    {
3445
        $match = $this->match(
3446
            $this->utf8
3447
                ? '([\pL\w\-_]+)'
3448
                : '([\w\-_]+)',
3449
            $m
3450
        );
3451
3452
        if ($match) {
3453
            $placeholder = $m[1];
3454
3455
            return true;
3456
        }
3457
3458
        if ($this->interpolation($placeholder)) {
3459
            return true;
3460
        }
3461
3462
        return false;
3463
    }
3464
3465
    /**
3466
     * Parse a url
3467
     *
3468
     * @param array $out
3469
     *
3470
     * @return boolean
3471
     */
3472
    protected function url(&$out)
3473
    {
3474
        //if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
3475
        if (
3476
            $this->literal('url(', 4) &&
3477
            ($this->string($out) || $this->openString(')', $out)) &&
3478
            $this->matchChar(')')
3479
        ) {
3480
            $out = [Type::T_STRING, '', ['url(', $out, ')']];
3481
3482
            return true;
3483
        }
3484
3485
        return false;
3486
    }
3487
3488
    /**
3489
     * Consume an end of statement delimiter
3490
     *
3491
     * @return boolean
3492
     */
3493
    protected function end()
3494
    {
3495
        if ($this->matchChar(';')) {
3496
            return true;
3497
        }
3498
3499
        if ($this->count === \strlen($this->buffer) || $this->buffer[$this->count] === '}') {
3500
            // if there is end of file or a closing block next then we don't need a ;
3501
            return true;
3502
        }
3503
3504
        return false;
3505
    }
3506
3507
    /**
3508
     * Strip assignment flag from the list
3509
     *
3510
     * @param array $value
3511
     *
3512
     * @return array
3513
     */
3514
    protected function stripAssignmentFlags(&$value)
3515
    {
3516
        $flags = [];
3517
3518
        for ($token = &$value; $token[0] === Type::T_LIST && ($s = \count($token[2])); $token = &$lastNode) {
3519
            $lastNode = &$token[2][$s - 1];
3520
3521
            while ($lastNode[0] === Type::T_KEYWORD && \in_array($lastNode[1], ['!default', '!global'])) {
3522
                array_pop($token[2]);
3523
3524
                $node     = end($token[2]);
3525
                $token    = $this->flattenList($token);
3526
                $flags[]  = $lastNode[1];
3527
                $lastNode = $node;
3528
            }
3529
        }
3530
3531
        return $flags;
3532
    }
3533
3534
    /**
3535
     * Strip optional flag from selector list
3536
     *
3537
     * @param array $selectors
3538
     *
3539
     * @return string
3540
     */
3541
    protected function stripOptionalFlag(&$selectors)
3542
    {
3543
        $optional = false;
3544
        $selector = end($selectors);
3545
        $part     = end($selector);
3546
3547
        if ($part === ['!optional']) {
3548
            array_pop($selectors[\count($selectors) - 1]);
3549
3550
            $optional = true;
3551
        }
3552
3553
        return $optional;
0 ignored issues
show
Bug Best Practice introduced by
The expression return $optional returns the type boolean which is incompatible with the documented return type string.
Loading history...
3554
    }
3555
3556
    /**
3557
     * Turn list of length 1 into value type
3558
     *
3559
     * @param array $value
3560
     *
3561
     * @return array
3562
     */
3563
    protected function flattenList($value)
3564
    {
3565
        if ($value[0] === Type::T_LIST && \count($value[2]) === 1) {
3566
            return $this->flattenList($value[2][0]);
3567
        }
3568
3569
        return $value;
3570
    }
3571
3572
    /**
3573
     * Quote regular expression
3574
     *
3575
     * @param string $what
3576
     *
3577
     * @return string
3578
     */
3579
    private function pregQuote($what)
3580
    {
3581
        return preg_quote($what, '/');
3582
    }
3583
3584
    /**
3585
     * Extract line numbers from buffer
3586
     *
3587
     * @param string $buffer
3588
     */
3589
    private function extractLineNumbers($buffer)
3590
    {
3591
        $this->sourcePositions = [0 => 0];
3592
        $prev = 0;
3593
3594
        while (($pos = strpos($buffer, "\n", $prev)) !== false) {
3595
            $this->sourcePositions[] = $pos;
3596
            $prev = $pos + 1;
3597
        }
3598
3599
        $this->sourcePositions[] = \strlen($buffer);
3600
3601
        if (substr($buffer, -1) !== "\n") {
3602
            $this->sourcePositions[] = \strlen($buffer) + 1;
3603
        }
3604
    }
3605
3606
    /**
3607
     * Get source line number and column (given character position in the buffer)
3608
     *
3609
     * @param integer $pos
3610
     *
3611
     * @return array
3612
     */
3613
    private function getSourcePosition($pos)
3614
    {
3615
        $low = 0;
3616
        $high = \count($this->sourcePositions);
3617
3618
        while ($low < $high) {
3619
            $mid = (int) (($high + $low) / 2);
3620
3621
            if ($pos < $this->sourcePositions[$mid]) {
3622
                $high = $mid - 1;
3623
                continue;
3624
            }
3625
3626
            if ($pos >= $this->sourcePositions[$mid + 1]) {
3627
                $low = $mid + 1;
3628
                continue;
3629
            }
3630
3631
            return [$mid + 1, $pos - $this->sourcePositions[$mid]];
3632
        }
3633
3634
        return [$low + 1, $pos - $this->sourcePositions[$low]];
3635
    }
3636
3637
    /**
3638
     * Save internal encoding
3639
     */
3640
    private function saveEncoding()
3641
    {
3642
        if (\extension_loaded('mbstring')) {
3643
            $this->encoding = mb_internal_encoding();
3644
3645
            mb_internal_encoding('iso-8859-1');
3646
        }
3647
    }
3648
3649
    /**
3650
     * Restore internal encoding
3651
     */
3652
    private function restoreEncoding()
3653
    {
3654
        if (\extension_loaded('mbstring') && $this->encoding) {
3655
            mb_internal_encoding($this->encoding);
3656
        }
3657
    }
3658
}
3659