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

Parser::selectorSingle()   F

Complexity

Conditions 53
Paths 32

Size

Total Lines 185
Code Lines 107

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 53
eloc 107
nc 32
nop 2
dl 0
loc 185
rs 3.3333
c 0
b 0
f 0

How to fix   Long Method    Complexity   

Long Method

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

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

Commonly applied refactorings include:

1
<?php
2
/**
3
 * SCSSPHP
4
 *
5
 * @copyright 2012-2018 Leaf Corcoran
6
 *
7
 * @license http://opensource.org/licenses/MIT MIT
8
 *
9
 * @link http://leafo.github.io/scssphp
10
 */
11
12
namespace Leafo\ScssPhp;
13
14
use Leafo\ScssPhp\Block;
15
use Leafo\ScssPhp\Compiler;
16
use Leafo\ScssPhp\Exception\ParserException;
17
use Leafo\ScssPhp\Node;
18
use Leafo\ScssPhp\Type;
19
20
/**
21
 * Parser
22
 *
23
 * @author Leaf Corcoran <[email protected]>
24
 */
25
class Parser
26
{
27
    const SOURCE_INDEX  = -1;
28
    const SOURCE_LINE   = -2;
29
    const SOURCE_COLUMN = -3;
30
31
    /**
32
     * @var array
33
     */
34
    protected static $precedence = [
35
        '='   => 0,
36
        'or'  => 1,
37
        'and' => 2,
38
        '=='  => 3,
39
        '!='  => 3,
40
        '<=>' => 3,
41
        '<='  => 4,
42
        '>='  => 4,
43
        '<'   => 4,
44
        '>'   => 4,
45
        '+'   => 5,
46
        '-'   => 5,
47
        '*'   => 6,
48
        '/'   => 6,
49
        '%'   => 6,
50
    ];
51
52
    protected static $commentPattern;
53
    protected static $operatorPattern;
54
    protected static $whitePattern;
55
56
    private $sourceName;
57
    private $sourceIndex;
58
    private $sourcePositions;
59
    private $charset;
60
    private $count;
61
    private $env;
62
    private $inParens;
63
    private $eatWhiteDefault;
64
    private $buffer;
65
    private $utf8;
66
    private $encoding;
67
    private $patternModifiers;
68
69
    /**
70
     * Constructor
71
     *
72
     * @api
73
     *
74
     * @param string  $sourceName
75
     * @param integer $sourceIndex
76
     * @param string  $encoding
77
     */
78
    public function __construct($sourceName, $sourceIndex = 0, $encoding = 'utf-8')
79
    {
80
        $this->sourceName       = $sourceName ?: '(stdin)';
81
        $this->sourceIndex      = $sourceIndex;
82
        $this->charset          = null;
83
        $this->utf8             = ! $encoding || strtolower($encoding) === 'utf-8';
84
        $this->patternModifiers = $this->utf8 ? 'Aisu' : 'Ais';
85
86
        if (empty(static::$operatorPattern)) {
87
            static::$operatorPattern = '([*\/%+-]|[!=]\=|\>\=?|\<\=\>|\<\=?|and|or)';
88
89
            $commentSingle      = '\/\/';
90
            $commentMultiLeft   = '\/\*';
91
            $commentMultiRight  = '\*\/';
92
93
            static::$commentPattern = $commentMultiLeft . '.*?' . $commentMultiRight;
94
            static::$whitePattern = $this->utf8
95
                ? '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisuS'
96
                : '/' . $commentSingle . '[^\n]*\s*|(' . static::$commentPattern . ')\s*|\s+/AisS';
97
        }
98
    }
99
100
    /**
101
     * Get source file name
102
     *
103
     * @api
104
     *
105
     * @return string
106
     */
107
    public function getSourceName()
108
    {
109
        return $this->sourceName;
110
    }
111
112
    /**
113
     * Throw parser error
114
     *
115
     * @api
116
     *
117
     * @param string $msg
118
     *
119
     * @throws \Leafo\ScssPhp\Exception\ParserException
120
     */
121
    public function throwParseError($msg = 'parse error')
122
    {
123
        list($line, /* $column */) = $this->getSourcePosition($this->count);
124
125
        $loc = empty($this->sourceName) ? "line: $line" : "$this->sourceName on line $line";
126
127
        if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
128
            throw new ParserException("$msg: failed at `$m[1]` $loc");
129
        }
130
131
        throw new ParserException("$msg: $loc");
132
    }
133
134
    /**
135
     * Parser buffer
136
     *
137
     * @api
138
     *
139
     * @param string $buffer
140
     *
141
     * @return \Leafo\ScssPhp\Block
142
     */
143
    public function parse($buffer)
144
    {
145
        // strip BOM (byte order marker)
146
        if (substr($buffer, 0, 3) === "\xef\xbb\xbf") {
147
            $buffer = substr($buffer, 3);
148
        }
149
150
        $this->buffer          = rtrim($buffer, "\x00..\x1f");
151
        $this->count           = 0;
152
        $this->env             = null;
153
        $this->inParens        = false;
154
        $this->eatWhiteDefault = true;
155
156
        $this->saveEncoding();
157
        $this->extractLineNumbers($buffer);
158
159
        $this->pushBlock(null); // root block
160
        $this->whitespace();
161
        $this->pushBlock(null);
162
        $this->popBlock();
163
164
        while ($this->parseChunk()) {
165
            ;
166
        }
167
168
        if ($this->count !== strlen($this->buffer)) {
169
            $this->throwParseError();
170
        }
171
172
        if (! empty($this->env->parent)) {
173
            $this->throwParseError('unclosed block');
174
        }
175
176
        if ($this->charset) {
177
            array_unshift($this->env->children, $this->charset);
178
        }
179
180
        $this->env->isRoot    = true;
181
182
        $this->restoreEncoding();
183
184
        return $this->env;
185
    }
186
187
    /**
188
     * Parse a value or value list
189
     *
190
     * @api
191
     *
192
     * @param string $buffer
193
     * @param string $out
194
     *
195
     * @return boolean
196
     */
197
    public function parseValue($buffer, &$out)
198
    {
199
        $this->count           = 0;
200
        $this->env             = null;
201
        $this->inParens        = false;
202
        $this->eatWhiteDefault = true;
203
        $this->buffer          = (string) $buffer;
204
205
        $this->saveEncoding();
206
207
        $list = $this->valueList($out);
208
209
        $this->restoreEncoding();
210
211
        return $list;
212
    }
213
214
    /**
215
     * Parse a selector or selector list
216
     *
217
     * @api
218
     *
219
     * @param string $buffer
220
     * @param string $out
221
     *
222
     * @return boolean
223
     */
224
    public function parseSelector($buffer, &$out)
225
    {
226
        $this->count           = 0;
227
        $this->env             = null;
228
        $this->inParens        = false;
229
        $this->eatWhiteDefault = true;
230
        $this->buffer          = (string) $buffer;
231
232
        $this->saveEncoding();
233
234
        $selector = $this->selectors($out);
0 ignored issues
show
Bug introduced by
$out of type string is incompatible with the type array expected by parameter $out of Leafo\ScssPhp\Parser::valueList(). ( Ignorable by Annotation )

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

234
        $selector = $this->select/** @scrutinizer ignore-type */ ors($out);
Loading history...
235
236
        $this->restoreEncoding();
237
238
        return $selector;
239
    }
240
241
    /**
242
     * Parse a single chunk off the head of the buffer and append it to the
243
     * current parse environment.
244
     *
245
     * Returns false when the buffer is empty, or when there is an error.
246
     *
247
     * This function is called repeatedly until the entire document is
248
     * parsed.
249
     *
250
     * This parser is most similar to a recursive descent parser. Single
251
     * functions represent discrete grammatical rules for the language, and
252
     * they are able to capture the text that represents those rules.
253
     *
254
     * Consider the function Compiler::keyword(). (All parse functions are
255
     * structured the same.)
256
     *
257
     * The function takes a single reference argument. When calling the
258
     * function it will attempt to match a keyword on the head of the buffer.
259
     * If it is successful, it will place the keyword in the referenced
260
     * argument, advance the position in the buffer, and return true. If it
261
     * fails then it won't advance the buffer and it will return false.
0 ignored issues
show
Bug introduced by
$out of type string is incompatible with the type array expected by parameter $out of Leafo\ScssPhp\Parser::selectors(). ( Ignorable by Annotation )

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

261
     * fails then it won't advance th/** @scrutinizer ignore-type */ e buffer and it will return false.
Loading history...
262
     *
263
     * All of these parse functions are powered by Compiler::match(), which behaves
264
     * the same way, but takes a literal regular expression. Sometimes it is
265
     * more convenient to use match instead of creating a new function.
266
     *
267
     * Because of the format of the functions, to parse an entire string of
268
     * grammatical rules, you can chain them together using &&.
269
     *
270
     * But, if some of the rules in the chain succeed before one fails, then
271
     * the buffer position will be left at an invalid state. In order to
272
     * avoid this, Compiler::seek() is used to remember and set buffer positions.
273
     *
274
     * Before parsing a chain, use $s = $this->seek() to remember the current
275
     * position into $s. Then if a chain fails, use $this->seek($s) to
276
     * go back where we started.
277
     *
278
     * @return boolean
279
     */
280
    protected function parseChunk()
281
    {
282
        $s = $this->seek();
283
284
        // the directives
285
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
286
            if ($this->literal('@at-root') &&
287
                ($this->selectors($selector) || true) &&
288
                ($this->map($with) || true) &&
289
                $this->literal('{')
290
            ) {
291
                $atRoot = $this->pushSpecialBlock(Type::T_AT_ROOT, $s);
292
                $atRoot->selector = $selector;
293
                $atRoot->with = $with;
294
295
                return true;
296
            }
297
298
            $this->seek($s);
299
300
            if ($this->literal('@media') && $this->mediaQueryList($mediaQueryList) && $this->literal('{')) {
301
                $media = $this->pushSpecialBlock(Type::T_MEDIA, $s);
302
                $media->queryList = $mediaQueryList[2];
303
304
                return true;
305
            }
306
307
            $this->seek($s);
308
309
            if ($this->literal('@mixin') &&
310
                $this->keyword($mixinName) &&
311
                ($this->argumentDef($args) || true) &&
312
                $this->literal('{')
313
            ) {
314
                $mixin = $this->pushSpecialBlock(Type::T_MIXIN, $s);
315
                $mixin->name = $mixinName;
316
                $mixin->args = $args;
317
318
                return true;
319
            }
0 ignored issues
show
Bug introduced by
The property selector does not exist on Leafo\ScssPhp\Block. Did you mean selectors?
Loading history...
320
0 ignored issues
show
Bug introduced by
The property with does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
321
            $this->seek($s);
322
323
            if ($this->literal('@include') &&
324
                $this->keyword($mixinName) &&
325
                ($this->literal('(') &&
326
                    ($this->argValues($argValues) || true) &&
327
                    $this->literal(')') || true) &&
328
                ($this->end() ||
329
                    $this->literal('{') && $hasBlock = true)
0 ignored issues
show
Bug introduced by
The property queryList does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
330
            ) {
331
                $child = [Type::T_INCLUDE, $mixinName, isset($argValues) ? $argValues : null, null];
332
333
                if (! empty($hasBlock)) {
334
                    $include = $this->pushSpecialBlock(Type::T_INCLUDE, $s);
335
                    $include->child = $child;
336
                } else {
337
                    $this->append($child, $s);
338
                }
339
340
                return true;
341
            }
342
0 ignored issues
show
Bug introduced by
The property name does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
343
            $this->seek($s);
0 ignored issues
show
Bug introduced by
The property args does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
344
345
            if ($this->literal('@scssphp-import-once') &&
346
                $this->valueList($importPath) &&
347
                $this->end()
348
            ) {
349
                $this->append([Type::T_SCSSPHP_IMPORT_ONCE, $importPath], $s);
350
351
                return true;
352
            }
353
354
            $this->seek($s);
355
356
            if ($this->literal('@import') &&
357
                $this->valueList($importPath) &&
358
                $this->end()
359
            ) {
360
                $this->append([Type::T_IMPORT, $importPath], $s);
361
362
                return true;
0 ignored issues
show
Bug introduced by
The property child does not exist on Leafo\ScssPhp\Block. Did you mean children?
Loading history...
363
            }
364
365
            $this->seek($s);
366
367
            if ($this->literal('@import') &&
368
                $this->url($importPath) &&
369
                $this->end()
370
            ) {
371
                $this->append([Type::T_IMPORT, $importPath], $s);
372
373
                return true;
374
            }
375
376
            $this->seek($s);
377
378
            if ($this->literal('@extend') &&
379
                $this->selectors($selectors) &&
380
                $this->end()
381
            ) {
382
                // check for '!flag'
383
                $optional = $this->stripOptionalFlag($selectors);
384
                $this->append([Type::T_EXTEND, $selectors, $optional], $s);
385
386
                return true;
387
            }
388
389
            $this->seek($s);
390
391
            if ($this->literal('@function') &&
392
                $this->keyword($fnName) &&
393
                $this->argumentDef($args) &&
394
                $this->literal('{')
395
            ) {
396
                $func = $this->pushSpecialBlock(Type::T_FUNCTION, $s);
397
                $func->name = $fnName;
398
                $func->args = $args;
399
400
                return true;
401
            }
402
403
            $this->seek($s);
404
405
            if ($this->literal('@break') && $this->end()) {
406
                $this->append([Type::T_BREAK], $s);
407
408
                return true;
409
            }
410
411
            $this->seek($s);
412
413
            if ($this->literal('@continue') && $this->end()) {
414
                $this->append([Type::T_CONTINUE], $s);
415
416
                return true;
417
            }
418
419
            $this->seek($s);
420
421
422
            if ($this->literal('@return') && ($this->valueList($retVal) || true) && $this->end()) {
423
                $this->append([Type::T_RETURN, isset($retVal) ? $retVal : [Type::T_NULL]], $s);
424
425
                return true;
426
            }
427
428
            $this->seek($s);
429
430
            if ($this->literal('@each') &&
431
                $this->genericList($varNames, 'variable', ',', false) &&
432
                $this->literal('in') &&
433
                $this->valueList($list) &&
434
                $this->literal('{')
435
            ) {
436
                $each = $this->pushSpecialBlock(Type::T_EACH, $s);
437
438
                foreach ($varNames[2] as $varName) {
439
                    $each->vars[] = $varName[1];
440
                }
441
442
                $each->list = $list;
443
444
                return true;
445
            }
446
447
            $this->seek($s);
448
449
            if ($this->literal('@while') &&
450
                $this->expression($cond) &&
451
                $this->literal('{')
452
            ) {
453
                $while = $this->pushSpecialBlock(Type::T_WHILE, $s);
454
                $while->cond = $cond;
455
456
                return true;
457
            }
458
459
            $this->seek($s);
460
461
            if ($this->literal('@for') &&
462
                $this->variable($varName) &&
463
                $this->literal('from') &&
464
                $this->expression($start) &&
465
                ($this->literal('through') ||
0 ignored issues
show
Bug introduced by
The property vars does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
466
                    ($forUntil = true && $this->literal('to'))) &&
467
                $this->expression($end) &&
468
                $this->literal('{')
0 ignored issues
show
Bug introduced by
The property list does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
469
            ) {
470
                $for = $this->pushSpecialBlock(Type::T_FOR, $s);
471
                $for->var = $varName[1];
472
                $for->start = $start;
473
                $for->end = $end;
474
                $for->until = isset($forUntil);
475
476
                return true;
477
            }
478
479
            $this->seek($s);
480
0 ignored issues
show
Bug introduced by
The property cond does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
481
            if ($this->literal('@if') && $this->valueList($cond) && $this->literal('{')) {
482
                $if = $this->pushSpecialBlock(Type::T_IF, $s);
483
                $if->cond = $cond;
484
                $if->cases = [];
485
486
                return true;
487
            }
488
489
            $this->seek($s);
490
491
            if ($this->literal('@debug') &&
492
                $this->valueList($value) &&
493
                $this->end()
494
            ) {
495
                $this->append([Type::T_DEBUG, $value], $s);
496
497
                return true;
0 ignored issues
show
Bug introduced by
The property var does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
498
            }
0 ignored issues
show
Bug introduced by
The property start does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
499
0 ignored issues
show
Bug introduced by
The property end does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
500
            $this->seek($s);
0 ignored issues
show
Bug introduced by
The property until does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
501
502
            if ($this->literal('@warn') &&
503
                $this->valueList($value) &&
504
                $this->end()
505
            ) {
506
                $this->append([Type::T_WARN, $value], $s);
507
508
                return true;
509
            }
510
0 ignored issues
show
Bug introduced by
The property cases does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
511
            $this->seek($s);
512
513
            if ($this->literal('@error') &&
514
                $this->valueList($value) &&
515
                $this->end()
516
            ) {
517
                $this->append([Type::T_ERROR, $value], $s);
518
519
                return true;
520
            }
521
522
            $this->seek($s);
523
524
            if ($this->literal('@content') && $this->end()) {
525
                $this->append([Type::T_MIXIN_CONTENT], $s);
526
527
                return true;
528
            }
529
530
            $this->seek($s);
531
532
            $last = $this->last();
533
534
            if (isset($last) && $last[0] === Type::T_IF) {
535
                list(, $if) = $last;
536
537
                if ($this->literal('@else')) {
538
                    if ($this->literal('{')) {
539
                        $else = $this->pushSpecialBlock(Type::T_ELSE, $s);
540
                    } elseif ($this->literal('if') && $this->valueList($cond) && $this->literal('{')) {
541
                        $else = $this->pushSpecialBlock(Type::T_ELSEIF, $s);
542
                        $else->cond = $cond;
543
                    }
544
545
                    if (isset($else)) {
546
                        $else->dontAppend = true;
547
                        $if->cases[] = $else;
548
549
                        return true;
550
                    }
551
                }
552
553
                $this->seek($s);
554
            }
555
556
            // only retain the first @charset directive encountered
557
            if ($this->literal('@charset') &&
558
                $this->valueList($charset) &&
559
                $this->end()
560
            ) {
561
                if (! isset($this->charset)) {
562
                    $statement = [Type::T_CHARSET, $charset];
563
564
                    list($line, $column) = $this->getSourcePosition($s);
565
566
                    $statement[static::SOURCE_LINE]   = $line;
567
                    $statement[static::SOURCE_COLUMN] = $column;
568
                    $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
569
570
                    $this->charset = $statement;
571
                }
572
573
                return true;
574
            }
575
576
            $this->seek($s);
577
578
            // doesn't match built in directive, do generic one
579
            if ($this->literal('@', false) &&
580
                $this->keyword($dirName) &&
581
                ($this->variable($dirValue) || $this->openString('{', $dirValue) || true) &&
582
                $this->literal('{')
583
            ) {
584
                if ($dirName === 'media') {
585
                    $directive = $this->pushSpecialBlock(Type::T_MEDIA, $s);
586
                } else {
587
                    $directive = $this->pushSpecialBlock(Type::T_DIRECTIVE, $s);
588
                    $directive->name = $dirName;
589
                }
590
591
                if (isset($dirValue)) {
592
                    $directive->value = $dirValue;
593
                }
594
595
                return true;
596
            }
597
598
            $this->seek($s);
599
600
            return false;
601
        }
602
603
        // property shortcut
604
        // captures most properties before having to parse a selector
605
        if ($this->keyword($name, false) &&
606
            $this->literal(': ') &&
607
            $this->valueList($value) &&
608
            $this->end()
609
        ) {
610
            $name = [Type::T_STRING, '', [$name]];
611
            $this->append([Type::T_ASSIGN, $name, $value], $s);
612
613
            return true;
614
        }
615
616
        $this->seek($s);
617
618
        // variable assigns
0 ignored issues
show
Bug introduced by
The property value does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
619
        if ($this->variable($name) &&
620
            $this->literal(':') &&
621
            $this->valueList($value) &&
622
            $this->end()
623
        ) {
624
            // check for '!flag'
625
            $assignmentFlags = $this->stripAssignmentFlags($value);
626
            $this->append([Type::T_ASSIGN, $name, $value, $assignmentFlags], $s);
627
628
            return true;
629
        }
630
631
        $this->seek($s);
632
633
        // misc
634
        if ($this->literal('-->')) {
635
            return true;
636
        }
637
638
        // opening css block
639
        if ($this->selectors($selectors) && $this->literal('{')) {
640
            $this->pushBlock($selectors, $s);
641
642
            return true;
643
        }
644
645
        $this->seek($s);
646
647
        // property assign, or nested assign
648
        if ($this->propertyName($name) && $this->literal(':')) {
649
            $foundSomething = false;
650
651
            if ($this->valueList($value)) {
652
                $this->append([Type::T_ASSIGN, $name, $value], $s);
653
                $foundSomething = true;
654
            }
655
656
            if ($this->literal('{')) {
657
                $propBlock = $this->pushSpecialBlock(Type::T_NESTED_PROPERTY, $s);
658
                $propBlock->prefix = $name;
659
                $foundSomething = true;
660
            } elseif ($foundSomething) {
661
                $foundSomething = $this->end();
662
            }
663
664
            if ($foundSomething) {
665
                return true;
666
            }
667
        }
668
669
        $this->seek($s);
670
671
        // closing a block
672
        if ($this->literal('}')) {
673
            $block = $this->popBlock();
674
675
            if (isset($block->type) && $block->type === Type::T_INCLUDE) {
676
                $include = $block->child;
677
                unset($block->child);
678
                $include[3] = $block;
679
                $this->append($include, $s);
680
            } elseif (empty($block->dontAppend)) {
681
                $type = isset($block->type) ? $block->type : Type::T_BLOCK;
682
                $this->append([$type, $block], $s);
683
            }
684
685
            return true;
686
        }
687
688
        // extra stuff
689
        if ($this->literal(';') ||
690
            $this->literal('<!--')
691
        ) {
692
            return true;
693
        }
0 ignored issues
show
Bug introduced by
The property prefix does not seem to exist on Leafo\ScssPhp\Block.
Loading history...
694
695
        return false;
696
    }
697
698
    /**
699
     * Push block onto parse tree
700
     *
701
     * @param array   $selectors
702
     * @param integer $pos
703
     *
704
     * @return \Leafo\ScssPhp\Block
705
     */
706
    protected function pushBlock($selectors, $pos = 0)
707
    {
708
        list($line, $column) = $this->getSourcePosition($pos);
709
710
        $b = new Block;
711
        $b->sourceName   = $this->sourceName;
712
        $b->sourceLine   = $line;
713
        $b->sourceColumn = $column;
714
        $b->sourceIndex  = $this->sourceIndex;
715
        $b->selectors    = $selectors;
716
        $b->comments     = [];
717
        $b->parent       = $this->env;
718
719
        if (! $this->env) {
720
            $b->children = [];
721
        } elseif (empty($this->env->children)) {
722
            $this->env->children = $this->env->comments;
723
            $b->children = [];
724
            $this->env->comments = [];
725
        } else {
726
            $b->children = $this->env->comments;
727
            $this->env->comments = [];
728
        }
729
730
        $this->env = $b;
731
732
        return $b;
733
    }
734
735
    /**
736
     * Push special (named) block onto parse tree
737
     *
738
     * @param string  $type
739
     * @param integer $pos
740
     *
741
     * @return \Leafo\ScssPhp\Block
742
     */
743
    protected function pushSpecialBlock($type, $pos)
744
    {
745
        $block = $this->pushBlock(null, $pos);
746
        $block->type = $type;
747
748
        return $block;
749
    }
750
751
    /**
752
     * Pop scope and return last block
753
     *
754
     * @return \Leafo\ScssPhp\Block
755
     *
756
     * @throws \Exception
757
     */
758
    protected function popBlock()
759
    {
760
        $block = $this->env;
761
762
        if (empty($block->parent)) {
763
            $this->throwParseError('unexpected }');
764
        }
765
766
        $this->env = $block->parent;
767
        unset($block->parent);
768
769
        $comments = $block->comments;
770
        if (count($comments)) {
771
            $this->env->comments = $comments;
772
            unset($block->comments);
773
        }
774
775
        return $block;
776
    }
777
778
    /**
779
     * Peek input stream
780
     *
781
     * @param string  $regex
782
     * @param array   $out
783
     * @param integer $from
784
     *
785
     * @return integer
786
     */
787
    protected function peek($regex, &$out, $from = null)
788
    {
789
        if (! isset($from)) {
790
            $from = $this->count;
791
        }
792
793
        $r = '/' . $regex . '/' . $this->patternModifiers;
794
        $result = preg_match($r, $this->buffer, $out, null, $from);
795
796
        return $result;
797
    }
798
799
    /**
800
     * Seek to position in input stream (or return current position in input stream)
801
     *
802
     * @param integer $where
803
     *
804
     * @return integer
805
     */
806
    protected function seek($where = null)
807
    {
808
        if ($where === null) {
809
            return $this->count;
810
        }
811
812
        $this->count = $where;
813
814
        return true;
815
    }
816
817
    /**
818
     * Match string looking for either ending delim, escape, or string interpolation
819
     *
820
     * {@internal This is a workaround for preg_match's 250K string match limit. }}
821
     *
822
     * @param array  $m     Matches (passed by reference)
823
     * @param string $delim Delimeter
824
     *
825
     * @return boolean True if match; false otherwise
826
     */
827
    protected function matchString(&$m, $delim)
828
    {
829
        $token = null;
830
831
        $end = strlen($this->buffer);
832
833
        // look for either ending delim, escape, or string interpolation
834
        foreach (['#{', '\\', $delim] as $lookahead) {
835
            $pos = strpos($this->buffer, $lookahead, $this->count);
836
837
            if ($pos !== false && $pos < $end) {
838
                $end = $pos;
839
                $token = $lookahead;
840
            }
841
        }
842
843
        if (! isset($token)) {
844
            return false;
845
        }
846
847
        $match = substr($this->buffer, $this->count, $end - $this->count);
848
        $m = [
849
            $match . $token,
850
            $match,
851
            $token
852
        ];
853
        $this->count = $end + strlen($token);
854
855
        return true;
856
    }
857
858
    /**
859
     * Try to match something on head of buffer
860
     *
861
     * @param string  $regex
862
     * @param array   $out
863
     * @param boolean $eatWhitespace
864
     *
865
     * @return boolean
866
     */
867
    protected function match($regex, &$out, $eatWhitespace = null)
868
    {
869
        if (! isset($eatWhitespace)) {
870
            $eatWhitespace = $this->eatWhiteDefault;
871
        }
872
873
        $r = '/' . $regex . '/' . $this->patternModifiers;
874
875
        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
876
            $this->count += strlen($out[0]);
877
878
            if ($eatWhitespace) {
879
                $this->whitespace();
880
            }
881
882
            return true;
883
        }
884
885
        return false;
886
    }
887
888
    /**
889
     * Match literal string
890
     *
891
     * @param string  $what
892
     * @param boolean $eatWhitespace
893
     *
894
     * @return boolean
895
     */
896
    protected function literal($what, $eatWhitespace = null)
897
    {
898
        if (! isset($eatWhitespace)) {
899
            $eatWhitespace = $this->eatWhiteDefault;
900
        }
901
902
        $len = strlen($what);
903
904
        if (strcasecmp(substr($this->buffer, $this->count, $len), $what) === 0) {
905
            $this->count += $len;
906
907
            if ($eatWhitespace) {
908
                $this->whitespace();
909
            }
910
911
            return true;
912
        }
913
914
        return false;
915
    }
916
917
    /**
918
     * Match some whitespace
919
     *
920
     * @return boolean
921
     */
922
    protected function whitespace()
923
    {
924
        $gotWhite = false;
925
926
        while (preg_match(static::$whitePattern, $this->buffer, $m, null, $this->count)) {
927
            if (isset($m[1]) && empty($this->commentsSeen[$this->count])) {
928
                $this->appendComment([Type::T_COMMENT, $m[1]]);
929
930
                $this->commentsSeen[$this->count] = true;
931
            }
932
933
            $this->count += strlen($m[0]);
934
            $gotWhite = true;
935
        }
936
937
        return $gotWhite;
938
    }
939
940
    /**
941
     * Append comment to current block
942
     *
943
     * @param array $comment
944
     */
945
    protected function appendComment($comment)
946
    {
947
        $comment[1] = substr(preg_replace(['/^\s+/m', '/^(.)/m'], ['', ' \1'], $comment[1]), 1);
948
949
        $this->env->comments[] = $comment;
950
    }
951
952
    /**
953
     * Append statement to current block
954
     *
955
     * @param array   $statement
956
     * @param integer $pos
957
     */
958
    protected function append($statement, $pos = null)
959
    {
960
        if ($pos !== null) {
961
            list($line, $column) = $this->getSourcePosition($pos);
962
963
            $statement[static::SOURCE_LINE]   = $line;
964
            $statement[static::SOURCE_COLUMN] = $column;
965
            $statement[static::SOURCE_INDEX]  = $this->sourceIndex;
966
        }
967
968
        $this->env->children[] = $statement;
969
970
        $comments = $this->env->comments;
971
972
        if (count($comments)) {
973
            $this->env->children = array_merge($this->env->children, $comments);
974
            $this->env->comments = [];
975
        }
976
    }
977
978
    /**
979
     * Returns last child was appended
980
     *
981
     * @return array|null
982
     */
983
    protected function last()
984
    {
985
        $i = count($this->env->children) - 1;
986
987
        if (isset($this->env->children[$i])) {
988
            return $this->env->children[$i];
989
        }
990
    }
991
992
    /**
993
     * Parse media query list
994
     *
995
     * @param array $out
996
     *
997
     * @return boolean
998
     */
999
    protected function mediaQueryList(&$out)
1000
    {
1001
        return $this->genericList($out, 'mediaQuery', ',', false);
1002
    }
1003
1004
    /**
1005
     * Parse media query
1006
     *
1007
     * @param array $out
1008
     *
1009
     * @return boolean
1010
     */
1011
    protected function mediaQuery(&$out)
1012
    {
1013
        $expressions = null;
1014
        $parts = [];
1015
1016
        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
1017
            $this->mixedKeyword($mediaType)
1018
        ) {
1019
            $prop = [Type::T_MEDIA_TYPE];
1020
1021
            if (isset($only)) {
1022
                $prop[] = [Type::T_KEYWORD, 'only'];
1023
            }
1024
1025
            if (isset($not)) {
1026
                $prop[] = [Type::T_KEYWORD, 'not'];
1027
            }
1028
1029
            $media = [Type::T_LIST, '', []];
1030
1031
            foreach ((array) $mediaType as $type) {
1032
                if (is_array($type)) {
1033
                    $media[2][] = $type;
1034
                } else {
1035
                    $media[2][] = [Type::T_KEYWORD, $type];
1036
                }
1037
            }
1038
1039
            $prop[]  = $media;
1040
            $parts[] = $prop;
1041
        }
1042
1043
        if (empty($parts) || $this->literal('and')) {
1044
            $this->genericList($expressions, 'mediaExpression', 'and', false);
1045
1046
            if (is_array($expressions)) {
1047
                $parts = array_merge($parts, $expressions[2]);
1048
            }
1049
        }
1050
1051
        $out = $parts;
1052
1053
        return true;
1054
    }
1055
1056
    /**
1057
     * Parse media expression
1058
     *
1059
     * @param array $out
1060
     *
1061
     * @return boolean
1062
     */
1063
    protected function mediaExpression(&$out)
1064
    {
1065
        $s = $this->seek();
1066
        $value = null;
1067
0 ignored issues
show
introduced by
The condition is_null($statement) is always false.
Loading history...
1068
        if ($this->literal('(') &&
1069
            $this->expression($feature) &&
1070
            ($this->literal(':') && $this->expression($value) || true) &&
1071
            $this->literal(')')
1072
        ) {
1073
            $out = [Type::T_MEDIA_EXPRESSION, $feature];
1074
1075
            if ($value) {
1076
                $out[] = $value;
1077
            }
1078
1079
            return true;
1080
        }
1081
1082
        $this->seek($s);
1083
1084
        return false;
1085
    }
1086
1087
    /**
1088
     * Parse argument values
1089
     *
1090
     * @param array $out
1091
     *
1092
     * @return boolean
1093
     */
1094
    protected function argValues(&$out)
1095
    {
1096
        if ($this->genericList($list, 'argValue', ',', false)) {
1097
            $out = $list[2];
1098
1099
            return true;
1100
        }
1101
1102
        return false;
1103
    }
1104
1105
    /**
1106
     * Parse argument value
1107
     *
1108
     * @param array $out
1109
     *
1110
     * @return boolean
1111
     */
1112
    protected function argValue(&$out)
1113
    {
1114
        $s = $this->seek();
1115
1116
        $keyword = null;
1117
1118
        if (! $this->variable($keyword) || ! $this->literal(':')) {
1119
            $this->seek($s);
1120
            $keyword = null;
1121
        }
1122
1123
        if ($this->genericList($value, 'expression')) {
1124
            $out = [$keyword, $value, false];
1125
            $s = $this->seek();
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...
1126
1127
            if ($this->literal('...')) {
1128
                $out[2] = true;
1129
            } else {
1130
                $this->seek($s);
1131
            }
1132
1133
            return true;
1134
        }
1135
1136
        return false;
1137
    }
1138
1139
    /**
1140
     * Parse comma separated value list
1141
     *
1142
     * @param string $out
1143
     *
1144
     * @return boolean
1145
     */
1146
    protected function valueList(&$out)
1147
    {
1148
        return $this->genericList($out, 'spaceList', ',');
1149
    }
1150
1151
    /**
1152
     * Parse space separated value list
1153
     *
1154
     * @param array $out
1155
     *
1156
     * @return boolean
1157
     */
1158
    protected function spaceList(&$out)
1159
    {
1160
        return $this->genericList($out, 'expression');
1161
    }
1162
1163
    /**
1164
     * Parse generic list
1165
     *
1166
     * @param array    $out
1167
     * @param callable $parseItem
1168
     * @param string   $delim
1169
     * @param boolean  $flatten
1170
     *
1171
     * @return boolean
1172
     */
1173
    protected function genericList(&$out, $parseItem, $delim = '', $flatten = true)
1174
    {
1175
        $s = $this->seek();
1176
        $items = [];
1177
1178
        while ($this->$parseItem($value)) {
1179
            $items[] = $value;
1180
1181
            if ($delim) {
1182
                if (! $this->literal($delim)) {
1183
                    break;
1184
                }
1185
            }
1186
        }
1187
1188
        if (count($items) === 0) {
1189
            $this->seek($s);
1190
1191
            return false;
1192
        }
1193
1194
        if ($flatten && count($items) === 1) {
1195
            $out = $items[0];
1196
        } else {
1197
            $out = [Type::T_LIST, $delim, $items];
1198
        }
1199
1200
        return true;
1201
    }
1202
1203
    /**
1204
     * Parse expression
1205
     *
1206
     * @param array $out
1207
     *
1208
     * @return boolean
1209
     */
1210
    protected function expression(&$out)
1211
    {
1212
        $s = $this->seek();
1213
1214
        if ($this->literal('(')) {
1215
            if ($this->literal(')')) {
1216
                $out = [Type::T_LIST, '', []];
1217
1218
                return true;
1219
            }
1220
1221
            if ($this->valueList($out) && $this->literal(')') && $out[0] === Type::T_LIST) {
1222
                return true;
1223
            }
1224
1225
            $this->seek($s);
1226
1227
            if ($this->map($out)) {
1228
                return true;
1229
            }
1230
1231
            $this->seek($s);
1232
        }
1233
1234
        if ($this->value($lhs)) {
1235
            $out = $this->expHelper($lhs, 0);
1236
1237
            return true;
1238
        }
1239
1240
        return false;
1241
    }
1242
1243
    /**
1244
     * Parse left-hand side of subexpression
1245
     *
1246
     * @param array   $lhs
1247
     * @param integer $minP
1248
     *
1249
     * @return array
1250
     */
1251
    protected function expHelper($lhs, $minP)
1252
    {
1253
        $operators = static::$operatorPattern;
1254
1255
        $ss = $this->seek();
1256
        $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1257
            ctype_space($this->buffer[$this->count - 1]);
1258
1259
        while ($this->match($operators, $m, false) && static::$precedence[$m[1]] >= $minP) {
1260
            $whiteAfter = isset($this->buffer[$this->count]) &&
1261
                ctype_space($this->buffer[$this->count]);
1262
            $varAfter = isset($this->buffer[$this->count]) &&
1263
                $this->buffer[$this->count] === '$';
1264
1265
            $this->whitespace();
1266
1267
            $op = $m[1];
1268
1269
            // don't turn negative numbers into expressions
1270
            if ($op === '-' && $whiteBefore && ! $whiteAfter && ! $varAfter) {
1271
                break;
1272
            }
1273
1274
            if (! $this->value($rhs)) {
1275
                break;
1276
            }
1277
1278
            // peek and see if rhs belongs to next operator
1279
            if ($this->peek($operators, $next) && static::$precedence[$next[1]] > static::$precedence[$op]) {
1280
                $rhs = $this->expHelper($rhs, static::$precedence[$next[1]]);
1281
            }
1282
1283
            $lhs = [Type::T_EXPRESSION, $op, $lhs, $rhs, $this->inParens, $whiteBefore, $whiteAfter];
1284
            $ss = $this->seek();
1285
            $whiteBefore = isset($this->buffer[$this->count - 1]) &&
1286
                ctype_space($this->buffer[$this->count - 1]);
1287
        }
1288
1289
        $this->seek($ss);
1290
1291
        return $lhs;
1292
    }
1293
1294
    /**
1295
     * Parse value
1296
     *
1297
     * @param array $out
1298
     *
1299
     * @return boolean
1300
     */
1301
    protected function value(&$out)
1302
    {
1303
        $s = $this->seek();
1304
1305
        if ($this->literal('not', false) && $this->whitespace() && $this->value($inner)) {
1306
            $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1307
1308
            return true;
1309
        }
1310
1311
        $this->seek($s);
1312
1313
        if ($this->literal('not', false) && $this->parenValue($inner)) {
1314
            $out = [Type::T_UNARY, 'not', $inner, $this->inParens];
1315
1316
            return true;
1317
        }
1318
1319
        $this->seek($s);
1320
1321
        if ($this->literal('+') && $this->value($inner)) {
1322
            $out = [Type::T_UNARY, '+', $inner, $this->inParens];
1323
1324
            return true;
1325
        }
1326
1327
        $this->seek($s);
1328
1329
        // negation
1330
        if ($this->literal('-', false) &&
1331
            ($this->variable($inner) ||
1332
            $this->unit($inner) ||
1333
            $this->parenValue($inner))
1334
        ) {
1335
            $out = [Type::T_UNARY, '-', $inner, $this->inParens];
1336
1337
            return true;
1338
        }
1339
1340
        $this->seek($s);
1341
1342
        if ($this->parenValue($out) ||
1343
            $this->interpolation($out) ||
1344
            $this->variable($out) ||
1345
            $this->color($out) ||
1346
            $this->unit($out) ||
1347
            $this->string($out) ||
1348
            $this->func($out) ||
1349
            $this->progid($out)
1350
        ) {
1351
            return true;
1352
        }
1353
1354
        if ($this->keyword($keyword)) {
1355
            if ($keyword === 'null') {
1356
                $out = [Type::T_NULL];
1357
            } else {
1358
                $out = [Type::T_KEYWORD, $keyword];
1359
            }
1360
1361
            return true;
1362
        }
1363
1364
        return false;
1365
    }
1366
1367
    /**
1368
     * Parse parenthesized value
1369
     *
1370
     * @param array $out
1371
     *
1372
     * @return boolean
1373
     */
1374
    protected function parenValue(&$out)
1375
    {
1376
        $s = $this->seek();
1377
1378
        $inParens = $this->inParens;
1379
1380
        if ($this->literal('(')) {
1381
            if ($this->literal(')')) {
1382
                $out = [Type::T_LIST, '', []];
1383
1384
                return true;
1385
            }
1386
1387
            $this->inParens = true;
1388
1389
            if ($this->expression($exp) && $this->literal(')')) {
1390
                $out = $exp;
1391
                $this->inParens = $inParens;
1392
1393
                return true;
1394
            }
1395
        }
1396
1397
        $this->inParens = $inParens;
1398
        $this->seek($s);
1399
1400
        return false;
1401
    }
1402
1403
    /**
1404
     * Parse "progid:"
1405
     *
1406
     * @param array $out
1407
     *
1408
     * @return boolean
1409
     */
1410
    protected function progid(&$out)
1411
    {
1412
        $s = $this->seek();
1413
1414
        if ($this->literal('progid:', false) &&
1415
            $this->openString('(', $fn) &&
1416
            $this->literal('(')
1417
        ) {
1418
            $this->openString(')', $args, '(');
1419
1420
            if ($this->literal(')')) {
1421
                $out = [Type::T_STRING, '', [
1422
                    'progid:', $fn, '(', $args, ')'
1423
                ]];
1424
1425
                return true;
1426
            }
1427
        }
1428
1429
        $this->seek($s);
1430
1431
        return false;
1432
    }
1433
1434
    /**
1435
     * Parse function call
1436
     *
1437
     * @param array $out
1438
     *
1439
     * @return boolean
1440
     */
1441
    protected function func(&$func)
1442
    {
1443
        $s = $this->seek();
1444
1445
        if ($this->keyword($name, false) &&
1446
            $this->literal('(')
1447
        ) {
1448
            if ($name === 'alpha' && $this->argumentList($args)) {
1449
                $func = [Type::T_FUNCTION, $name, [Type::T_STRING, '', $args]];
1450
1451
                return true;
1452
            }
1453
1454
            if ($name !== 'expression' && ! preg_match('/^(-[a-z]+-)?calc$/', $name)) {
1455
                $ss = $this->seek();
1456
1457
                if ($this->argValues($args) && $this->literal(')')) {
1458
                    $func = [Type::T_FUNCTION_CALL, $name, $args];
1459
1460
                    return true;
1461
                }
1462
1463
                $this->seek($ss);
1464
            }
1465
1466
            if (($this->openString(')', $str, '(') || true) &&
1467
                $this->literal(')')
1468
            ) {
1469
                $args = [];
1470
1471
                if (! empty($str)) {
1472
                    $args[] = [null, [Type::T_STRING, '', [$str]]];
1473
                }
1474
1475
                $func = [Type::T_FUNCTION_CALL, $name, $args];
1476
1477
                return true;
1478
            }
1479
        }
1480
1481
        $this->seek($s);
1482
1483
        return false;
1484
    }
1485
1486
    /**
1487
     * Parse function call argument list
1488
     *
1489
     * @param array $out
1490
     *
1491
     * @return boolean
1492
     */
1493
    protected function argumentList(&$out)
1494
    {
1495
        $s = $this->seek();
1496
        $this->literal('(');
1497
1498
        $args = [];
1499
1500
        while ($this->keyword($var)) {
1501
            if ($this->literal('=') && $this->expression($exp)) {
1502
                $args[] = [Type::T_STRING, '', [$var . '=']];
1503
                $arg = $exp;
1504
            } else {
1505
                break;
1506
            }
1507
1508
            $args[] = $arg;
1509
1510
            if (! $this->literal(',')) {
1511
                break;
1512
            }
1513
1514
            $args[] = [Type::T_STRING, '', [', ']];
1515
        }
1516
1517
        if (! $this->literal(')') || ! count($args)) {
1518
            $this->seek($s);
1519
1520
            return false;
1521
        }
1522
1523
        $out = $args;
1524
1525
        return true;
1526
    }
1527
1528
    /**
1529
     * Parse mixin/function definition  argument list
1530
     *
1531
     * @param array $out
1532
     *
1533
     * @return boolean
1534
     */
1535
    protected function argumentDef(&$out)
1536
    {
1537
        $s = $this->seek();
1538
        $this->literal('(');
1539
1540
        $args = [];
1541
1542
        while ($this->variable($var)) {
1543
            $arg = [$var[1], null, false];
1544
1545
            $ss = $this->seek();
1546
1547
            if ($this->literal(':') && $this->genericList($defaultVal, 'expression')) {
1548
                $arg[1] = $defaultVal;
1549
            } else {
1550
                $this->seek($ss);
1551
            }
1552
1553
            $ss = $this->seek();
1554
1555
            if ($this->literal('...')) {
1556
                $sss = $this->seek();
1557
1558
                if (! $this->literal(')')) {
1559
                    $this->throwParseError('... has to be after the final argument');
1560
                }
1561
1562
                $arg[2] = true;
1563
                $this->seek($sss);
1564
            } else {
1565
                $this->seek($ss);
1566
            }
1567
1568
            $args[] = $arg;
1569
1570
            if (! $this->literal(',')) {
1571
                break;
1572
            }
1573
        }
1574
1575
        if (! $this->literal(')')) {
1576
            $this->seek($s);
1577
1578
            return false;
1579
        }
1580
1581
        $out = $args;
1582
1583
        return true;
1584
    }
1585
1586
    /**
1587
     * Parse map
1588
     *
1589
     * @param array $out
1590
     *
1591
     * @return boolean
1592
     */
1593
    protected function map(&$out)
1594
    {
1595
        $s = $this->seek();
1596
1597
        if (! $this->literal('(')) {
1598
            return false;
1599
        }
1600
1601
        $keys = [];
1602
        $values = [];
1603
1604
        while ($this->genericList($key, 'expression') && $this->literal(':') &&
1605
            $this->genericList($value, 'expression')
1606
        ) {
1607
            $keys[] = $key;
1608
            $values[] = $value;
1609
1610
            if (! $this->literal(',')) {
1611
                break;
1612
            }
1613
        }
1614
1615
        if (! count($keys) || ! $this->literal(')')) {
1616
            $this->seek($s);
1617
1618
            return false;
1619
        }
1620
1621
        $out = [Type::T_MAP, $keys, $values];
1622
1623
        return true;
1624
    }
1625
1626
    /**
1627
     * Parse color
1628
     *
1629
     * @param array $out
1630
     *
1631
     * @return boolean
1632
     */
1633
    protected function color(&$out)
1634
    {
1635
        $color = [Type::T_COLOR];
1636
1637
        if ($this->match('(#([0-9a-f]{6})|#([0-9a-f]{3}))', $m)) {
1638
            if (isset($m[3])) {
1639
                $num = hexdec($m[3]);
1640
1641
                foreach ([3, 2, 1] as $i) {
1642
                    $t = $num & 0xf;
1643
                    $color[$i] = $t << 4 | $t;
1644
                    $num >>= 4;
1645
                }
1646
            } else {
1647
                $num = hexdec($m[2]);
1648
1649
                foreach ([3, 2, 1] as $i) {
1650
                    $color[$i] = $num & 0xff;
1651
                    $num >>= 8;
1652
                }
1653
            }
1654
1655
            $out = $color;
1656
1657
            return true;
1658
        }
1659
1660
        return false;
1661
    }
1662
1663
    /**
1664
     * Parse number with unit
1665
     *
1666
     * @param array $out
1667
     *
1668
     * @return boolean
1669
     */
1670
    protected function unit(&$unit)
1671
    {
1672
        if ($this->match('([0-9]*(\.)?[0-9]+)([%a-zA-Z]+)?', $m)) {
1673
            $unit = new Node\Number($m[1], empty($m[3]) ? '' : $m[3]);
1674
1675
            return true;
1676
        }
1677
1678
        return false;
1679
    }
1680
1681
    /**
1682
     * Parse string
1683
     *
1684
     * @param array $out
1685
     *
1686
     * @return boolean
1687
     */
1688
    protected function string(&$out)
1689
    {
1690
        $s = $this->seek();
1691
1692
        if ($this->literal('"', false)) {
1693
            $delim = '"';
1694
        } elseif ($this->literal("'", false)) {
1695
            $delim = "'";
1696
        } else {
1697
            return false;
1698
        }
1699
1700
        $content = [];
1701
        $oldWhite = $this->eatWhiteDefault;
1702
        $this->eatWhiteDefault = false;
1703
        $hasInterpolation = false;
1704
1705
        while ($this->matchString($m, $delim)) {
1706
            if ($m[1] !== '') {
1707
                $content[] = $m[1];
1708
            }
1709
1710
            if ($m[2] === '#{') {
1711
                $this->count -= strlen($m[2]);
1712
1713
                if ($this->interpolation($inter, false)) {
1714
                    $content[] = $inter;
1715
                    $hasInterpolation = true;
1716
                } else {
1717
                    $this->count += strlen($m[2]);
1718
                    $content[] = '#{'; // ignore it
1719
                }
1720
            } elseif ($m[2] === '\\') {
1721
                if ($this->literal('"', false)) {
1722
                    $content[] = $m[2] . '"';
1723
                } elseif ($this->literal("'", false)) {
1724
                    $content[] = $m[2] . "'";
1725
                } else {
1726
                    $content[] = $m[2];
1727
                }
1728
            } else {
1729
                $this->count -= strlen($delim);
1730
                break; // delim
1731
            }
1732
        }
1733
1734
        $this->eatWhiteDefault = $oldWhite;
1735
1736
        if ($this->literal($delim)) {
1737
            if ($hasInterpolation) {
1738
                $delim = '"';
1739
1740
                foreach ($content as &$string) {
1741
                    if ($string === "\\'") {
1742
                        $string = "'";
1743
                    } elseif ($string === '\\"') {
1744
                        $string = '"';
1745
                    }
1746
                }
1747
            }
1748
1749
            $out = [Type::T_STRING, $delim, $content];
1750
1751
            return true;
1752
        }
1753
1754
        $this->seek($s);
1755
1756
        return false;
1757
    }
1758
1759
    /**
1760
     * Parse keyword or interpolation
1761
     *
1762
     * @param array $out
1763
     *
1764
     * @return boolean
1765
     */
1766
    protected function mixedKeyword(&$out)
1767
    {
1768
        $parts = [];
1769
1770
        $oldWhite = $this->eatWhiteDefault;
1771
        $this->eatWhiteDefault = false;
1772
1773
        for (;;) {
1774
            if ($this->keyword($key)) {
1775
                $parts[] = $key;
1776
                continue;
1777
            }
1778
1779
            if ($this->interpolation($inter)) {
1780
                $parts[] = $inter;
1781
                continue;
1782
            }
1783
1784
            break;
1785
        }
1786
1787
        $this->eatWhiteDefault = $oldWhite;
1788
1789
        if (count($parts) === 0) {
1790
            return false;
1791
        }
1792
1793
        if ($this->eatWhiteDefault) {
1794
            $this->whitespace();
1795
        }
1796
1797
        $out = $parts;
1798
1799
        return true;
1800
    }
1801
1802
    /**
1803
     * Parse an unbounded string stopped by $end
1804
     *
1805
     * @param string $end
1806
     * @param array  $out
1807
     * @param string $nestingOpen
1808
     *
1809
     * @return boolean
1810
     */
1811
    protected function openString($end, &$out, $nestingOpen = null)
1812
    {
1813
        $oldWhite = $this->eatWhiteDefault;
1814
        $this->eatWhiteDefault = false;
1815
1816
        $patt = '(.*?)([\'"]|#\{|' . $this->pregQuote($end) . '|' . static::$commentPattern . ')';
1817
1818
        $nestingLevel = 0;
1819
1820
        $content = [];
1821
1822
        while ($this->match($patt, $m, false)) {
1823
            if (isset($m[1]) && $m[1] !== '') {
1824
                $content[] = $m[1];
1825
1826
                if ($nestingOpen) {
1827
                    $nestingLevel += substr_count($m[1], $nestingOpen);
1828
                }
1829
            }
1830
1831
            $tok = $m[2];
1832
1833
            $this->count-= strlen($tok);
1834
1835
            if ($tok === $end && ! $nestingLevel--) {
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...
1836
                break;
1837
            }
1838
1839
            if (($tok === "'" || $tok === '"') && $this->string($str)) {
1840
                $content[] = $str;
1841
                continue;
1842
            }
1843
1844
            if ($tok === '#{' && $this->interpolation($inter)) {
1845
                $content[] = $inter;
1846
                continue;
1847
            }
1848
1849
            $content[] = $tok;
1850
            $this->count+= strlen($tok);
1851
        }
1852
1853
        $this->eatWhiteDefault = $oldWhite;
1854
1855
        if (count($content) === 0) {
1856
            return false;
1857
        }
1858
1859
        // trim the end
1860
        if (is_string(end($content))) {
1861
            $content[count($content) - 1] = rtrim(end($content));
1862
        }
1863
1864
        $out = [Type::T_STRING, '', $content];
1865
1866
        return true;
1867
    }
1868
1869
    /**
1870
     * Parser interpolation
1871
     *
1872
     * @param array   $out
1873
     * @param boolean $lookWhite save information about whitespace before and after
1874
     *
1875
     * @return boolean
1876
     */
1877
    protected function interpolation(&$out, $lookWhite = true)
1878
    {
1879
        $oldWhite = $this->eatWhiteDefault;
1880
        $this->eatWhiteDefault = true;
1881
1882
        $s = $this->seek();
1883
1884
        if ($this->literal('#{') && $this->valueList($value) && $this->literal('}', false)) {
1885
            if ($lookWhite) {
1886
                $left = preg_match('/\s/', $this->buffer[$s - 1]) ? ' ' : '';
1887
                $right = preg_match('/\s/', $this->buffer[$this->count]) ? ' ': '';
1888
            } else {
1889
                $left = $right = false;
1890
            }
1891
1892
            $out = [Type::T_INTERPOLATE, $value, $left, $right];
1893
            $this->eatWhiteDefault = $oldWhite;
1894
0 ignored issues
show
introduced by
The condition $color[4] === 255 is always false.
Loading history...
1895
            if ($this->eatWhiteDefault) {
1896
                $this->whitespace();
1897
            }
1898
1899
            return true;
1900
        }
1901
1902
        $this->seek($s);
1903
        $this->eatWhiteDefault = $oldWhite;
1904
1905
        return false;
1906
    }
1907
1908
    /**
1909
     * Parse property name (as an array of parts or a string)
1910
     *
1911
     * @param array $out
1912
     *
1913
     * @return boolean
1914
     */
1915
    protected function propertyName(&$out)
1916
    {
1917
        $parts = [];
1918
1919
        $oldWhite = $this->eatWhiteDefault;
1920
        $this->eatWhiteDefault = false;
1921
1922
        for (;;) {
1923
            if ($this->interpolation($inter)) {
1924
                $parts[] = $inter;
1925
                continue;
1926
            }
1927
1928
            if ($this->keyword($text)) {
1929
                $parts[] = $text;
1930
                continue;
1931
            }
1932
1933
            if (count($parts) === 0 && $this->match('[:.#]', $m, false)) {
1934
                // css hacks
1935
                $parts[] = $m[0];
1936
                continue;
1937
            }
1938
1939
            break;
1940
        }
1941
1942
        $this->eatWhiteDefault = $oldWhite;
1943
1944
        if (count($parts) === 0) {
1945
            return false;
1946
        }
1947
1948
        // match comment hack
1949
        if (preg_match(
1950
            static::$whitePattern,
1951
            $this->buffer,
1952
            $m,
1953
            null,
1954
            $this->count
1955
        )) {
1956
            if (! empty($m[0])) {
1957
                $parts[] = $m[0];
1958
                $this->count += strlen($m[0]);
1959
            }
1960
        }
1961
1962
        $this->whitespace(); // get any extra whitespace
1963
1964
        $out = [Type::T_STRING, '', $parts];
1965
1966
        return true;
1967
    }
1968
1969
    /**
1970
     * Parse comma separated selector list
1971
     *
1972
     * @param array $out
1973
     *
1974
     * @return boolean
1975
     */
1976
    protected function selectors(&$out)
1977
    {
1978
        $s = $this->seek();
1979
        $selectors = [];
1980
1981
        while ($this->selector($sel)) {
1982
            $selectors[] = $sel;
1983
1984
            if (! $this->literal(',')) {
1985
                break;
1986
            }
1987
1988
            while ($this->literal(',')) {
1989
                ; // ignore extra
1990
            }
1991
        }
1992
1993
        if (count($selectors) === 0) {
1994
            $this->seek($s);
1995
1996
            return false;
1997
        }
1998
1999
        $out = $selectors;
2000
2001
        return true;
2002
    }
2003
2004
    /**
2005
     * Parse whitespace separated selector list
2006
     *
2007
     * @param array $out
2008
     *
2009
     * @return boolean
2010
     */
2011
    protected function selector(&$out)
2012
    {
2013
        $selector = [];
2014
2015
        for (;;) {
2016
            if ($this->match('[>+~]+', $m)) {
2017
                $selector[] = [$m[0]];
2018
                continue;
2019
            }
2020
2021
            if ($this->selectorSingle($part)) {
2022
                $selector[] = $part;
2023
                $this->match('\s+', $m);
2024
                continue;
2025
            }
2026
2027
            if ($this->match('\/[^\/]+\/', $m)) {
2028
                $selector[] = [$m[0]];
2029
                continue;
2030
            }
2031
2032
            break;
2033
        }
2034
2035
        if (count($selector) === 0) {
2036
            return false;
2037
        }
2038
2039
        $out = $selector;
2040
        return true;
2041
    }
2042
2043
    /**
2044
     * Parse the parts that make up a selector
2045
     *
2046
     * {@internal
2047
     *     div[yes=no]#something.hello.world:nth-child(-2n+1)%placeholder
2048
     * }}
2049
     *
2050
     * @param array $out
2051
     *
2052
     * @return boolean
0 ignored issues
show
introduced by
$parts is an empty array, thus ! $parts is always true.
Loading history...
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...
2053
     */
2054
    protected function selectorSingle(&$out)
2055
    {
2056
        $oldWhite = $this->eatWhiteDefault;
2057
        $this->eatWhiteDefault = false;
2058
2059
        $parts = [];
2060
2061
        if ($this->literal('*', false)) {
2062
            $parts[] = '*';
2063
        }
2064
2065
        for (;;) {
2066
            // see if we can stop early
2067
            if ($this->match('\s*[{,]', $m)) {
2068
                $this->count--;
2069
                break;
2070
            }
2071
2072
            $s = $this->seek();
2073
2074
            // self
2075
            if ($this->literal('&', false)) {
2076
                $parts[] = Compiler::$selfSelector;
2077
                continue;
2078
            }
2079
2080
            if ($this->literal('.', false)) {
2081
                $parts[] = '.';
2082
                continue;
2083
            }
2084
2085
            if ($this->literal('|', false)) {
2086
                $parts[] = '|';
2087
                continue;
2088
            }
2089
2090
            if ($this->match('\\\\\S', $m)) {
2091
                $parts[] = $m[0];
2092
                continue;
2093
            }
2094
2095
            // for keyframes
2096
            if ($this->unit($unit)) {
2097
                $parts[] = $unit;
2098
                continue;
2099
            }
2100
2101
            if ($this->keyword($name)) {
2102
                $parts[] = $name;
2103
                continue;
2104
            }
2105
2106
            if ($this->interpolation($inter)) {
2107
                $parts[] = $inter;
2108
                continue;
2109
            }
2110
2111
            if ($this->literal('%', false) && $this->placeholder($placeholder)) {
2112
                $parts[] = '%';
2113
                $parts[] = $placeholder;
2114
                continue;
2115
            }
2116
2117
            if ($this->literal('#', false)) {
2118
                $parts[] = '#';
2119
                continue;
2120
            }
2121
2122
            // a pseudo selector
2123
            if ($this->match('::?', $m) && $this->mixedKeyword($nameParts)) {
2124
                $parts[] = $m[0];
2125
2126
                foreach ($nameParts as $sub) {
2127
                    $parts[] = $sub;
2128
                }
2129
2130
                $ss = $this->seek();
2131
2132
                if ($this->literal('(') &&
2133
                    ($this->openString(')', $str, '(') || true) &&
2134
                    $this->literal(')')
2135
                ) {
2136
                    $parts[] = '(';
2137
2138
                    if (! empty($str)) {
2139
                        $parts[] = $str;
2140
                    }
2141
2142
                    $parts[] = ')';
2143
                } else {
2144
                    $this->seek($ss);
2145
                }
2146
2147
                continue;
2148
            }
2149
2150
            $this->seek($s);
2151
2152
            // attribute selector
2153
            if ($this->literal('[') &&
2154
               ($this->openString(']', $str, '[') || true) &&
2155
               $this->literal(']')
2156
            ) {
2157
                $parts[] = '[';
2158
2159
                if (! empty($str)) {
2160
                    $parts[] = $str;
2161
                }
2162
2163
                $parts[] = ']';
2164
2165
                continue;
2166
            }
2167
2168
            $this->seek($s);
2169
2170
            break;
2171
        }
2172
2173
        $this->eatWhiteDefault = $oldWhite;
2174
2175
        if (count($parts) === 0) {
2176
            return false;
2177
        }
2178
2179
        $out = $parts;
2180
2181
        return true;
2182
    }
2183
2184
    /**
2185
     * Parse a variable
2186
     *
2187
     * @param array $out
2188
     *
2189
     * @return boolean
2190
     */
2191
    protected function variable(&$out)
2192
    {
2193
        $s = $this->seek();
2194
2195
        if ($this->literal('$', false) && $this->keyword($name)) {
2196
            $out = [Type::T_VARIABLE, $name];
2197
2198
            return true;
2199
        }
2200
2201
        $this->seek($s);
2202
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...
2203
        return false;
2204
    }
2205
2206
    /**
2207
     * Parse a keyword
2208
     *
2209
     * @param string  $word
2210
     * @param boolean $eatWhitespace
2211
     *
2212
     * @return boolean
2213
     */
2214
    protected function keyword(&$word, $eatWhitespace = null)
2215
    {
2216
        if ($this->match(
2217
            $this->utf8
2218
                ? '(([\pL\w_\-\*!"\']|[\\\\].)([\pL\w\-_"\']|[\\\\].)*)'
2219
                : '(([\w_\-\*!"\']|[\\\\].)([\w\-_"\']|[\\\\].)*)',
2220
            $m,
2221
            $eatWhitespace
2222
        )) {
2223
            $word = $m[1];
2224
2225
            return true;
2226
        }
2227
2228
        return false;
2229
    }
2230
2231
    /**
2232
     * Parse a placeholder
2233
     *
2234
     * @param string $placeholder
2235
     *
2236
     * @return boolean
2237
     */
2238
    protected function placeholder(&$placeholder)
2239
    {
2240
        if ($this->match(
2241
            $this->utf8
2242
                ? '([\pL\w\-_]+|#[{][$][\pL\w\-_]+[}])'
2243
                : '([\w\-_]+|#[{][$][\w\-_]+[}])',
2244
            $m
2245
        )) {
2246
            $placeholder = $m[1];
2247
2248
            return true;
2249
        }
2250
2251
        return false;
2252
    }
2253
2254
    /**
2255
     * Parse a url
2256
     *
2257
     * @param array $out
2258
     *
2259
     * @return boolean
2260
     */
2261
    protected function url(&$out)
2262
    {
0 ignored issues
show
introduced by
$selectors is an empty array, thus ! $selectors is always true.
Loading history...
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...
2263
        if ($this->match('(url\(\s*(["\']?)([^)]+)\2\s*\))', $m)) {
2264
            $out = [Type::T_STRING, '', ['url(' . $m[2] . $m[3] . $m[2] . ')']];
2265
2266
            return true;
2267
        }
2268
2269
        return false;
2270
    }
2271
2272
    /**
2273
     * Consume an end of statement delimiter
2274
     *
2275
     * @return boolean
2276
     */
2277
    protected function end()
2278
    {
2279
        if ($this->literal(';')) {
2280
            return true;
2281
        }
2282
2283
        if ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
2284
            // if there is end of file or a closing block next then we don't need a ;
2285
            return true;
2286
        }
2287
2288
        return false;
2289
    }
2290
2291
    /**
2292
     * Strip assignment flag from the list
2293
     *
2294
     * @param array $value
2295
     *
2296
     * @return array
2297
     */
2298
    protected function stripAssignmentFlags(&$value)
2299
    {
2300
        $flags = [];
2301
2302
        for ($token = &$value; $token[0] === Type::T_LIST && ($s = count($token[2])); $token = &$lastNode) {
2303
            $lastNode = &$token[2][$s - 1];
2304
2305
            while ($lastNode[0] === Type::T_KEYWORD && in_array($lastNode[1], ['!default', '!global'])) {
2306
                array_pop($token[2]);
2307
2308
                $node = end($token[2]);
2309
2310
                $token = $this->flattenList($token);
2311
2312
                $flags[] = $lastNode[1];
2313
2314
                $lastNode = $node;
2315
            }
2316
        }
2317
2318
        return $flags;
2319
    }
2320
2321
    /**
2322
     * Strip optional flag from selector list
2323
     *
2324
     * @param array $selectors
2325
     *
2326
     * @return string
2327
     */
2328
    protected function stripOptionalFlag(&$selectors)
2329
    {
2330
        $optional = false;
2331
2332
        $selector = end($selectors);
2333
        $part = end($selector);
2334
2335
        if ($part === ['!optional']) {
2336
            array_pop($selectors[count($selectors) - 1]);
2337
2338
            $optional = true;
2339
        }
2340
2341
        return $optional;
2342
    }
2343
2344
    /**
2345
     * Turn list of length 1 into value type
2346
     *
2347
     * @param array $value
2348
     *
2349
     * @return array
2350
     */
2351
    protected function flattenList($value)
2352
    {
2353
        if ($value[0] === Type::T_LIST && count($value[2]) === 1) {
2354
            return $this->flattenList($value[2][0]);
2355
        }
2356
2357
        return $value;
2358
    }
2359
2360
    /**
2361
     * @deprecated
2362
     *
2363
     * {@internal
2364
     *     advance counter to next occurrence of $what
2365
     *     $until - don't include $what in advance
2366
     *     $allowNewline, if string, will be used as valid char set
2367
     * }}
2368
     */
2369
    protected function to($what, &$out, $until = false, $allowNewline = false)
2370
    {
2371
        if (is_string($allowNewline)) {
2372
            $validChars = $allowNewline;
2373
        } else {
2374
            $validChars = $allowNewline ? '.' : "[^\n]";
2375
        }
2376
2377
        if (! $this->match('(' . $validChars . '*?)' . $this->pregQuote($what), $m, ! $until)) {
2378
            return false;
2379
        }
2380
2381
        if ($until) {
2382
            $this->count -= strlen($what); // give back $what
2383
        }
2384
2385
        $out = $m[1];
2386
2387
        return true;
2388
    }
2389
2390
    /**
2391
     * @deprecated
2392
     */
2393
    protected function show()
2394
    {
2395
        if ($this->peek("(.*?)(\n|$)", $m, $this->count)) {
2396
            return $m[1];
2397
        }
2398
2399
        return '';
2400
    }
2401
2402
    /**
2403
     * Quote regular expression
2404
     *
2405
     * @param string $what
2406
     *
2407
     * @return string
2408
     */
2409
    private function pregQuote($what)
2410
    {
2411
        return preg_quote($what, '/');
2412
    }
2413
2414
    /**
2415
     * Extract line numbers from buffer
2416
     *
2417
     * @param string $buffer
2418
     */
2419
    private function extractLineNumbers($buffer)
2420
    {
2421
        $this->sourcePositions = [0 => 0];
2422
        $prev = 0;
2423
2424
        while (($pos = strpos($buffer, "\n", $prev)) !== false) {
2425
            $this->sourcePositions[] = $pos;
2426
            $prev = $pos + 1;
2427
        }
2428
2429
        $this->sourcePositions[] = strlen($buffer);
2430
2431
        if (substr($buffer, -1) !== "\n") {
2432
            $this->sourcePositions[] = strlen($buffer) + 1;
2433
        }
2434
    }
2435
2436
    /**
2437
     * Get source line number and column (given character position in the buffer)
2438
     *
2439
     * @param integer $pos
2440
     *
2441
     * @return integer
2442
     */
2443
    private function getSourcePosition($pos)
2444
    {
2445
        $low = 0;
2446
        $high = count($this->sourcePositions);
2447
2448
        while ($low < $high) {
2449
            $mid = (int) (($high + $low) / 2);
2450
2451
            if ($pos < $this->sourcePositions[$mid]) {
2452
                $high = $mid - 1;
2453
                continue;
2454
            }
2455
2456
            if ($pos >= $this->sourcePositions[$mid + 1]) {
2457
                $low = $mid + 1;
2458
                continue;
2459
            }
2460
2461
            return [$mid + 1, $pos - $this->sourcePositions[$mid]];
2462
        }
2463
2464
        return [$low + 1, $pos - $this->sourcePositions[$low]];
2465
    }
2466
2467
    /**
2468
     * Save internal encoding
2469
     */
2470
    private function saveEncoding()
2471
    {
2472
        if (version_compare(PHP_VERSION, '7.2.0') >= 0) {
2473
            return;
2474
        }
2475
2476
        $iniDirective = 'mbstring' . '.func_overload'; // deprecated in PHP 7.2
2477
2478
        if (ini_get($iniDirective) & 2) {
2479
            $this->encoding = mb_internal_encoding();
2480
2481
            mb_internal_encoding('iso-8859-1');
2482
        }
2483
    }
2484
2485
    /**
2486
     * Restore internal encoding
2487
     */
2488
    private function restoreEncoding()
2489
    {
2490
        if ($this->encoding) {
2491
            mb_internal_encoding($this->encoding);
2492
        }
2493
    }
2494
}
2495