Completed
Push — master ( 283f0c...b38731 )
by Marcus
11s
created

Parser::clearBlockStack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 0
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace LesserPhp;
4
5
use LesserPhp\Exception\GeneralException;
6
7
/**
8
 * lesserphp
9
 * https://www.maswaba.de/lesserphp
10
 *
11
 * LESS CSS compiler, adapted from http://lesscss.org
12
 *
13
 * Copyright 2013, Leaf Corcoran <[email protected]>
14
 * Copyright 2016, Marcus Schwarz <[email protected]>
15
 * Licensed under MIT or GPLv3, see LICENSE
16
 * @package LesserPhp
17
 * // responsible for taking a string of LESS code and converting it into a
18
 * // syntax tree
19
 */
20
class Parser
21
{
22
23
    protected static $nextBlockId = 0; // used to uniquely identify blocks
24
25
    protected static $precedence = [
26
        '=<' => 0,
27
        '>=' => 0,
28
        '=' => 0,
29
        '<' => 0,
30
        '>' => 0,
31
32
        '+' => 1,
33
        '-' => 1,
34
        '*' => 2,
35
        '/' => 2,
36
        '%' => 2,
37
    ];
38
39
    protected static $whitePattern;
40
    protected static $commentMulti;
41
42
    protected static $commentSingle = '//';
43
    protected static $commentMultiLeft = '/*';
44
    protected static $commentMultiRight = '*/';
45
46
    // regex string to match any of the operators
47
    protected static $operatorString;
48
49
    // these properties will supress division unless it's inside parenthases
50
    protected static $supressDivisionProps =
51
        ['/border-radius$/i', '/^font$/i'];
52
53
    private $blockDirectives = [
54
        'font-face',
55
        'keyframes',
56
        'page',
57
        '-moz-document',
58
        'viewport',
59
        '-moz-viewport',
60
        '-o-viewport',
61
        '-ms-viewport',
62
    ];
63
    private $lineDirectives = ['charset'];
64
65
    /**
66
     * if we are in parens we can be more liberal with whitespace around
67
     * operators because it must evaluate to a single value and thus is less
68
     * ambiguous.
69
     *
70
     * Consider:
71
     *     property1: 10 -5; // is two numbers, 10 and -5
72
     *     property2: (10 -5); // should evaluate to 5
73
     */
74
    protected $inParens = false;
75
76
    // caches preg escaped literals
77
    protected static $literalCache = [];
78
    /** @var int */
79
    public $count;
80
    /** @var int */
81
    private $line;
82
    /** @var array */
83
    private $seenComments;
84
    /** @var string */
85
    public $buffer;
86
87
    /** @var mixed $env Block Stack */
88
    private $env;
89
    /** @var bool */
90
    private $inExp;
91
    /** @var string */
92
    private $currentProperty;
93
94
    /**
95
     * @var bool
96
     */
97
    private $writeComments = false;
98
99
    /**
100
     * Parser constructor.
101
     *
102
     * @param \LesserPhp\Compiler $lessc
103 49
     * @param string              $sourceName
104
     */
105 49
    public function __construct(Compiler $lessc, $sourceName = null)
106
    {
107 49
        $this->eatWhiteDefault = true;
0 ignored issues
show
Bug introduced by
The property eatWhiteDefault does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
108
        // reference to less needed for vPrefix, mPrefix, and parentSelector
109 49
        $this->lessc = $lessc;
0 ignored issues
show
Bug introduced by
The property lessc does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
110
111 49
        $this->sourceName = $sourceName; // name used for error messages
0 ignored issues
show
Bug introduced by
The property sourceName does not exist. Did you maybe forget to declare it?

In PHP it is possible to write to properties without declaring them. For example, the following is perfectly valid PHP code:

class MyClass { }

$x = new MyClass();
$x->foo = true;

Generally, it is a good practice to explictly declare properties to avoid accidental typos and provide IDE auto-completion:

class MyClass {
    public $foo;
}

$x = new MyClass();
$x->foo = true;
Loading history...
112 1
113 1
        if (!self::$operatorString) {
114
            self::$operatorString =
115 1
                '(' . implode('|', array_map([Compiler::class, 'pregQuote'], array_keys(self::$precedence))) . ')';
116 1
117 1
            $commentSingle = Compiler::pregQuote(self::$commentSingle);
118
            $commentMultiLeft = Compiler::pregQuote(self::$commentMultiLeft);
119 1
            $commentMultiRight = Compiler::pregQuote(self::$commentMultiRight);
120 1
121
            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
122 49
            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
123
        }
124
    }
125
126
    /**
127
     * @param string $buffer
128
     *
129
     * @return mixed
130 49
     * @throws \LesserPhp\Exception\GeneralException
131
     */
132 49
    public function parse($buffer)
133 49
    {
134
        $this->count = 0;
135 49
        $this->line = 1;
136 49
137 49
        $this->clearBlockStack();
138 49
        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
139 49
        $this->pushSpecialBlock('root');
140
        $this->eatWhiteDefault = true;
141 49
        $this->seenComments = [];
142
143
        $this->whitespace();
144 49
145
        // parse the entire file
146
        while (false !== $this->parseChunk()) {
147
            ;
148 49
        }
149
150
        if ($this->count !== strlen($this->buffer)) {
151
            //            var_dump($this->count);
152
//            var_dump($this->buffer);
153
            $this->throwError();
154
        }
155 49
156
        // TODO report where the block was opened
157
        if (!property_exists($this->env, 'parent') || $this->env->parent !== null) {
158
            throw new GeneralException('parse error: unclosed block');
159 49
        }
160
161
        return $this->env;
162
    }
163
164
    /**
165
     * Parse a single chunk off the head of the buffer and append it to the
166
     * current parse environment.
167
     * Returns false when the buffer is empty, or when there is an error.
168
     *
169
     * This function is called repeatedly until the entire document is
170
     * parsed.
171
     *
172
     * This parser is most similar to a recursive descent parser. Single
173
     * functions represent discrete grammatical rules for the language, and
174
     * they are able to capture the text that represents those rules.
175
     *
176
     * Consider the function \LesserPhp\Compiler::keyword(). (all parse functions are
177
     * structured the same)
178
     *
179
     * The function takes a single reference argument. When calling the
180
     * function it will attempt to match a keyword on the head of the buffer.
181
     * If it is successful, it will place the keyword in the referenced
182
     * argument, advance the position in the buffer, and return true. If it
183
     * fails then it won't advance the buffer and it will return false.
184
     *
185
     * All of these parse functions are powered by \LesserPhp\Compiler::match(), which behaves
186
     * the same way, but takes a literal regular expression. Sometimes it is
187
     * more convenient to use match instead of creating a new function.
188
     *
189
     * Because of the format of the functions, to parse an entire string of
190
     * grammatical rules, you can chain them together using &&.
191
     *
192
     * But, if some of the rules in the chain succeed before one fails, then
193
     * the buffer position will be left at an invalid state. In order to
194
     * avoid this, \LesserPhp\Compiler::seek() is used to remember and set buffer positions.
195
     *
196
     * Before parsing a chain, use $s = $this->seek() to remember the current
197
     * position into $s. Then if a chain fails, use $this->seek($s) to
198
     * go back where we started.
199 49
     * @throws \LesserPhp\Exception\GeneralException
200
     */
201 49
    protected function parseChunk()
202
    {
203
        if (empty($this->buffer)) {
204 49
            return false;
205
        }
206 49
        $s = $this->seek();
207 46
208
        if ($this->whitespace()) {
209
            return true;
210
        }
211 49
212 47
        // setting a property
213
        if ($this->keyword($key) && $this->assign() && $this->propertyValue($value, $key) && $this->end()) {
214 47
            $this->append(['assign', $key, $value], $s);
215
216 49
            return true;
217
        } else {
218
            $this->seek($s);
219
        }
220 49
221 26
        // look for special css blocks
222
        if ($this->literal('@', false)) {
223
            $this->count--;
224 26
225 3
            // media
226
            if ($this->literal('@media')) {
227
                return $this->handleLiteralMedia($s);
228 26
            }
229 26
230 4
            if ($this->literal('@', false) && $this->keyword($directiveName)) {
231 4
                if ($this->isDirective($directiveName, $this->blockDirectives)) {
232
                    if ($this->handleDirectiveBlock($directiveName) === true) {
233 25
                        return true;
234 1
                    }
235 1
                } elseif ($this->isDirective($directiveName, $this->lineDirectives)) {
236
                    if ($this->handleDirectiveLine($directiveName) === true) {
237 25
                        return true;
238 24
                    }
239 1
                } elseif ($this->literal(':', true)) {
240
                    if ($this->handleRulesetDefinition($directiveName) === true) {
241
                        return true;
242
                    }
243
                }
244 25
            }
245
246
            $this->seek($s);
247 49
        }
248 4
249 4
        if ($this->literal('&', false)) {
250
            $this->count--;
251
            if ($this->literal('&:extend')) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
252
                // hierauf folgt was in runden klammern, und zwar das element, das erweitert werden soll
253
                // heißt also, das was in klammern steht wird um die aktuellen klassen erweitert
254
                /*
255
Aus
256
257
nav ul {
258
  &:extend(.inline);
259
  background: blue;
260
}
261
.inline {
262
  color: red;
263
}
264
265
266
Wird:
267
268
nav ul {
269
  background: blue;
270
}
271
.inline,
272
nav ul {
273
  color: red;
274
}
275
276
                 */
277
//                echo "Here we go";
278
            }
279
        }
280
281 49
282 49
        // setting a variable
283
        if ($this->variable($var) && $this->assign() &&
284 23
            $this->propertyValue($value) && $this->end()
285
        ) {
286 23
            $this->append(['assign', $var, $value], $s);
287
288 49
            return true;
289
        } else {
290
            $this->seek($s);
291 49
        }
292 3
293
        if ($this->import($importValue)) {
294 3
            $this->append($importValue, $s);
295
296
            return true;
297
        }
298 49
299 49
        // opening parametric mixin
300 49
        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
301
            ($this->guards($guards) || true) &&
302 18
            $this->literal('{')
303 18
        ) {
304 18
            $block = $this->pushBlock($this->fixTags([$tag]));
305 18
            $block->args = $args;
306 5
            $block->isVararg = $isVararg;
307
            if (!empty($guards)) {
308
                $block->guards = $guards;
309 18
            }
310
311 49
            return true;
312
        } else {
313
            $this->seek($s);
314
        }
315 49
316 46
        // opening a simple block
317 46
        if ($this->tags($tags) && $this->literal('{', false)) {
318
            $tags = $this->fixTags($tags);
319 46
            $this->pushBlock($tags);
320
321 49
            return true;
322
        } else {
323
            $this->seek($s);
324
        }
325 49
326
        // closing a block
327 46
        if ($this->literal('}', false)) {
328
            try {
329
                $block = $this->pop();
330
            } catch (\Exception $e) {
331
                $this->seek($s);
332
                $this->throwError($e->getMessage());
333 46
334 46
                return false; // will never be reached, but silences the ide for now
335 46
            }
336 46
337 46
            $hidden = false;
338 46
            if ($block->type === null) {
339 46
                $hidden = true;
340 46
                if (!isset($block->args)) {
341
                    foreach ($block->tags as $tag) {
342
                        if (!is_string($tag) || $tag[0] !== $this->lessc->getMPrefix()) {
343
                            $hidden = false;
344
                            break;
345 46
                        }
346 46
                    }
347 46
                }
348
349
                foreach ($block->tags as $tag) {
350
                    if (is_string($tag)) {
351
                        $this->env->children[$tag][] = $block;
352 46
                    }
353 46
                }
354
            }
355
356
            if (!$hidden) {
357
                $this->append(['block', $block], $s);
358 46
            }
359
360 46
            // this is done here so comments aren't bundled into he block that
361
            // was just closed
362
            $this->whitespace();
363
364 49
            return true;
365 49
        }
366 49
367
        // mixin
368 22
        if ($this->mixinTags($tags) &&
369 22
            ($this->argumentDef($argv, $isVararg) || true) &&
370
            ($this->keyword($suffix) || true) && $this->end()
371 22
        ) {
372
            $tags = $this->fixTags($tags);
373 49
            $this->append(['mixin', $tags, $argv, $suffix], $s);
374
375
            return true;
376
        } else {
377 49
            $this->seek($s);
378 4
        }
379
380
        // spare ;
381 49
        if ($this->literal(';')) {
382
            return true;
383
        }
384
385
        return false; // got nothing, throw error
386
    }
387
388
    /**
389
     * @param string $directiveName
390 26
     * @param array  $directives
391
     *
392
     * @return bool
393 26
     */
394 26
    protected function isDirective($directiveName, array $directives)
395
    {
396 26
        // TODO: cache pattern in parser
397
        $pattern = implode('|', array_map([Compiler::class, 'pregQuote'], $directives));
398
        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
399
400
        return (preg_match($pattern, $directiveName) === 1);
401
    }
402
403
    /**
404 46
     * @param array $tags
405
     *
406
     * @return mixed
407 46
     */
408 46
    protected function fixTags(array $tags)
409 46
    {
410
        // move @ tags out of variable namespace
411
        foreach ($tags as &$tag) {
412
            if ($tag[0] === $this->lessc->getVPrefix()) {
413 46
                $tag[0] = $this->lessc->getMPrefix();
414
            }
415
        }
416
417
        return $tags;
418
    }
419
420
    /**
421
     * a list of expressions
422
     *
423 48
     * @param $exps
424
     *
425 48
     * @return bool
426
     */
427 48
    protected function expressionList(&$exps)
428 48
    {
429
        $values = [];
430
431 48
        while ($this->expression($exp)) {
432 26
            $values[] = $exp;
433
        }
434
435 48
        if (count($values) === 0) {
436
            return false;
437 48
        }
438
439
        $exps = Compiler::compressList($values, ' ');
440
441
        return true;
442
    }
443
444
    /**
445
     * Attempt to consume an expression.
446
     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
447
     *
448 48
     * @param $out
449
     *
450 48
     * @return bool
451 48
     */
452
    protected function expression(&$out)
453
    {
454 48
        if ($this->value($lhs)) {
455 2
            $out = $this->expHelper($lhs, 0);
456 2
457 2
            // look for / shorthand
458
            if (!empty($this->env->supressedDivision)) {
459 2
                unset($this->env->supressedDivision);
460 2
                $s = $this->seek();
461 2
                if ($this->literal('/') && $this->value($rhs)) {
462
                    $out = [
463
                        'list',
464
                        '',
465
                        [$out, ['keyword', '/'], $rhs],
466
                    ];
467
                } else {
468 48
                    $this->seek($s);
469
                }
470
            }
471 48
472
            return true;
473
        }
474
475
        return false;
476
    }
477
478
    /**
479
     * recursively parse infix equation with $lhs at precedence $minP
480
     *
481
     * @param     $lhs
482 48
     * @param int $minP
483
     *
484 48
     * @return array
485 48
     */
486
    protected function expHelper($lhs, $minP)
487 48
    {
488 48
        $this->inExp = true;
489
        $ss = $this->seek();
490
491
        while (true) {
492 48
            $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
493
494 48
            // If there is whitespace before the operator, then we require
495 48
            // whitespace after the operator for it to be an expression
496
            $needWhite = $whiteBefore && !$this->inParens;
497 20
498 20
            if ($this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) &&
499 20
                self::$precedence[$m[1]] >= $minP
500 20
            ) {
501
                if (!$this->inParens &&
502 5
                    isset($this->env->currentProperty) &&
503 5
                    $m[1] === '/' &&
504 2
                    empty($this->env->supressedDivision)
505 5
                ) {
506
                    foreach (self::$supressDivisionProps as $pattern) {
507
                        if (preg_match($pattern, $this->env->currentProperty)) {
508
                            $this->env->supressedDivision = true;
509
                            break 2;
510 20
                        }
511
                    }
512 20
                }
513
514
                $whiteAfter = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
515
516
                if (!$this->value($rhs)) {
517 20
                    break;
518 20
                }
519
520 1
                // peek for next operator to see what to do with rhs
521
                if ($this->peek(self::$operatorString, $next) &&
522
                    self::$precedence[$next[1]] > self::$precedence[$m[1]]
523 20
                ) {
524 20
                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
525
                }
526 20
527
                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
528
                $ss = $this->seek();
529 48
530
                continue;
531
            }
532 48
533
            break;
534 48
        }
535
536
        $this->seek($ss);
537
538
        return $lhs;
539
    }
540
541
    /**
542
     * consume a list of values for a property
543
     *
544
     * @param        $value
545 48
     * @param string $keyName
546
     *
547 48
     * @return bool
548
     */
549 48
    public function propertyValue(&$value, $keyName = null)
550 47
    {
551
        $values = [];
552
553 48
        if ($keyName !== null) {
554 48
            $this->env->currentProperty = $keyName;
555 48
        }
556 48
557 48
        $s = null;
558 48
        while ($this->expressionList($v)) {
559
            $values[] = $v;
560
            $s = $this->seek();
561
            if (!$this->literal(',')) {
562 48
                break;
563 48
            }
564
        }
565
566 48
        if ($s) {
0 ignored issues
show
Bug Best Practice introduced by
The expression $s of type null|integer is loosely compared to true; this is ambiguous if the integer can be zero. You might want to explicitly use !== null instead.

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

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

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

// It is often better to use strict comparison
0 === false // false
0 === null  // false
Loading history...
567 47
            $this->seek($s);
568
        }
569
570 48
        if ($keyName !== null) {
571 3
            unset($this->env->currentProperty);
572
        }
573
574 48
        if (count($values) === 0) {
575
            return false;
576 48
        }
577
578
        $value = Compiler::compressList($values, ', ');
579
580
        return true;
581
    }
582
583
    /**
584 48
     * @param $out
585
     *
586 48
     * @return bool
587
     */
588
    protected function parenValue(&$out)
589 48
    {
590 48
        $s = $this->seek();
591
592
        // speed shortcut
593 5
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== '(') {
594 5
            return false;
595 5
        }
596 5
597
        $inParens = $this->inParens;
598 3
        if ($this->literal('(') &&
599 3
            ($this->inParens = true) && $this->expression($exp) &&
600
            $this->literal(')')
601 3
        ) {
602
            $out = $exp;
603 2
            $this->inParens = $inParens;
604 2
605
            return true;
606
        } else {
607 2
            $this->inParens = $inParens;
608
            $this->seek($s);
609
        }
610
611
        return false;
612
    }
613
614
    /**
615
     * a single value
616
     *
617 48
     * @param array $value
618
     *
619 48
     * @return bool
620
     */
621
    protected function value(&$value)
622 48
    {
623
        $s = $this->seek();
624 7
625 7
        // speed shortcut
626 6
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '-') {
627 7
            // negation
628
            if ($this->literal('-', false) &&
629 6
                (($this->variable($inner) && $inner = ['variable', $inner]) ||
630
                    $this->unit($inner) ||
631 6
                    $this->parenValue($inner))
632
            ) {
633 2
                $value = ['unary', '-', $inner];
634
635
                return true;
636
            } else {
637 48
                $this->seek($s);
638 3
            }
639
        }
640 48
641 38
        if ($this->parenValue($value)) {
642
            return true;
643 48
        }
644 13
        if ($this->unit($value)) {
645
            return true;
646 48
        }
647 25
        if ($this->color($value)) {
648
            return true;
649 48
        }
650 21
        if ($this->func($value)) {
651
            return true;
652
        }
653 48
        if ($this->stringValue($value)) {
654 35
            return true;
655
        }
656 35
657
        if ($this->keyword($word)) {
658
            $value = ['keyword', $word];
659
660 48
            return true;
661 29
        }
662
663 29
        // try a variable
664
        if ($this->variable($var)) {
665
            $value = ['variable', $var];
666
667 48
            return true;
668 4
        }
669
670 4
        // unquote string (should this work on any type?
671
        if ($this->literal('~') && $this->stringValue($str)) {
672 48
            $value = ['escape', $str];
673
674
            return true;
675
        } else {
676 48
            $this->seek($s);
677 1
        }
678
679 1
        // css hack: \0
680
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
681 48
            $value = ['keyword', '\\' . $m[1]];
682
683
            return true;
684 48
        } else {
685
            $this->seek($s);
686
        }
687
688
        return false;
689
    }
690
691
    /**
692
     * an import statement
693
     *
694 49
     * @param array $out
695
     *
696 49
     * @return bool|null
697 49
     */
698
    protected function import(&$out)
699
    {
700
        if (!$this->literal('@import')) {
701
            return false;
702
        }
703
704 3
        // @import "something.css" media;
705 3
        // @import url("something.css") media;
706
        // @import url(something.css) media;
707 3
708
        if ($this->propertyValue($value)) {
709
            $out = ['import', $value];
710
711
            return true;
712
        }
713
714
        return false;
715
    }
716
717
    /**
718 3
     * @param $out
719
     *
720 3
     * @return bool
721 3
     */
722
    protected function mediaQueryList(&$out)
723 3
    {
724
        if ($this->genericList($list, 'mediaQuery', ',', false)) {
725
            $out = $list[2];
726
727
            return true;
728
        }
729
730
        return false;
731
    }
732
733
    /**
734 3
     * @param $out
735
     *
736 3
     * @return bool
737
     */
738 3
    protected function mediaQuery(&$out)
739 3
    {
740
        $s = $this->seek();
741 3
742 3
        $expressions = null;
743
        $parts = [];
744 3
745 3
        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
746
            $this->keyword($mediaType)
747
        ) {
748 3
            $prop = ['mediaType'];
749
            if (isset($only)) {
750
                $prop[] = 'only';
751 3
            }
752 3
            if (isset($not)) {
753
                $prop[] = 'not';
754 1
            }
755
            $prop[] = $mediaType;
756
            $parts[] = $prop;
757
        } else {
758 3
            $this->seek($s);
759
        }
760
761 1
762 1
        if (!empty($mediaType) && !$this->literal('and')) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

This check looks for the bodies of if statements that have no statements or where all statements have been commented out. This may be the result of changes for debugging or the code may simply be obsolete.

These if bodies can be removed. If you have an empty if but statements in the else branch, consider inverting the condition.

if (rand(1, 6) > 3) {
//print "Check failed";
} else {
    print "Check succeeded";
}

could be turned into

if (rand(1, 6) <= 3) {
    print "Check succeeded";
}

This is much more concise to read.

Loading history...
763 1
            // ~
764
        } else {
765
            $this->genericList($expressions, 'mediaExpression', 'and', false);
766
            if (is_array($expressions)) {
767 3
                $parts = array_merge($parts, $expressions[2]);
768
            }
769
        }
770
771
        if (count($parts) === 0) {
772
            $this->seek($s);
773 3
774
            return false;
775 3
        }
776
777
        $out = $parts;
778
779
        return true;
780
    }
781
782
    /**
783 1
     * @param $out
784
     *
785 1
     * @return bool
786 1
     */
787 1
    protected function mediaExpression(&$out)
788 1
    {
789 1
        $s = $this->seek();
790 1
        $value = null;
791
        if ($this->literal('(') &&
792 1
            $this->keyword($feature) &&
793 1
            ($this->literal(':') && $this->expression($value) || true) &&
794 1
            $this->literal(')')
795
        ) {
796
            $out = ['mediaExp', $feature];
797 1
            if ($value) {
798 1
                $out[] = $value;
799 1
            }
800
801 1
            return true;
802
        } elseif ($this->variable($variable)) {
803
            $out = ['variable', $variable];
804
805
            return true;
806
        }
807
808
        $this->seek($s);
809
810
        return false;
811
    }
812
813
    /**
814
     * an unbounded string stopped by $end
815
     *
816
     * @param string   $end
817
     * @param          $out
818
     * @param null     $nestingOpen
819 26
     * @param string[] $rejectStrs
820
     *
821 26
     * @return bool
822 26
     */
823
    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
824 26
    {
825 26
        $oldWhite = $this->eatWhiteDefault;
826
        $this->eatWhiteDefault = false;
827
828 26
        $stop = ["'", '"', '@{', $end];
829 25
        $stop = array_map([Compiler::class, 'pregQuote'], $stop);
830
        // $stop[] = self::$commentMulti;
831
832 26
        if ($rejectStrs !== null) {
833
            $stop = array_merge($stop, $rejectStrs);
834 26
        }
835
836 26
        $patt = '(.*?)(' . implode('|', $stop) . ')';
837 26
838 26
        $nestingLevel = 0;
839 25
840 25
        $content = [];
841
        while ($this->match($patt, $m, false)) {
842
            if (!empty($m[1])) {
843
                $content[] = $m[1];
844
                if ($nestingOpen) {
845 26
                    $nestingLevel += substr_count($m[1], $nestingOpen);
846
                }
847 26
            }
848 26
849 14
            $tok = $m[2];
850 14
851
            $this->count -= strlen($tok);
852
            if ($tok == $end) {
853
                if ($nestingLevel === 0) {
854
                    break;
855
                } else {
856 24
                    $nestingLevel--;
857 13
                }
858 13
            }
859
860
            if (($tok === "'" || $tok === '"') && $this->stringValue($str)) {
861 23
                $content[] = $str;
862 3
                continue;
863 3
            }
864
865
            if ($tok === '@{' && $this->interpolation($inter)) {
866 23
                $content[] = $inter;
867 23
                continue;
868
            }
869
870
            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
871
                break;
872
            }
873
874 26
            $content[] = $tok;
875
            $this->count += strlen($tok);
876 26
        }
877 4
878
        $this->eatWhiteDefault = $oldWhite;
879
880
        if (count($content) === 0) {
881 25
            return false;
882 25
        }
883
884
        // trim the end
885 25
        if (is_string(end($content))) {
886
            $content[count($content) - 1] = rtrim(end($content));
887 25
        }
888
889
        $out = ['string', '', $content];
890
891
        return true;
892
    }
893
894
    /**
895 48
     * @param $out
896
     *
897 48
     * @return bool
898 48
     */
899 22
    protected function stringValue(&$out)
900 48
    {
901 12
        $s = $this->seek();
902
        if ($this->literal('"', false)) {
903 48
            $delim = '"';
904
        } elseif ($this->literal("'", false)) {
905
            $delim = "'";
906 24
        } else {
907
            return false;
908
        }
909
910 24
        $content = [];
911
912 24
        // look for either ending delim , escape, or string interpolation
913 24
        $patt = '([^\n]*?)(@\{|\\\\|' .
914
            Compiler::pregQuote($delim) . ')';
915 24
916 24
        $oldWhite = $this->eatWhiteDefault;
917 24
        $this->eatWhiteDefault = false;
918 6
919 6
        while ($this->match($patt, $m, false)) {
920 6
            $content[] = $m[1];
921
            if ($m[2] === '@{') {
922 1
                $this->count -= strlen($m[2]);
923 6
                if ($this->interpolation($inter)) {
924
                    $content[] = $inter;
925 24
                } else {
926 2
                    $this->count += strlen($m[2]);
927 2
                    $content[] = '@{'; // ignore it
928 2
                }
929
            } elseif ($m[2] === '\\') {
930
                $content[] = $m[2];
931 24
                if ($this->literal($delim, false)) {
932 24
                    $content[] = $delim;
933
                }
934
            } else {
935
                $this->count -= strlen($delim);
936 24
                break; // delim
937
            }
938 24
        }
939 24
940
        $this->eatWhiteDefault = $oldWhite;
941 24
942
        if ($this->literal($delim)) {
943
            $out = ['string', $delim, $content];
944 1
945
            return true;
946 1
        }
947
948
        $this->seek($s);
949
950
        return false;
951
    }
952
953
    /**
954 19
     * @param $out
955
     *
956 19
     * @return bool
957 19
     */
958
    protected function interpolation(&$out)
959 19
    {
960 19
        $oldWhite = $this->eatWhiteDefault;
961 19
        $this->eatWhiteDefault = true;
962 19
963
        $s = $this->seek();
964 7
        if ($this->literal('@{') &&
965 7
            $this->openString('}', $interp, null, ["'", '"', ';']) &&
966 7
            $this->literal('}', false)
967 1
        ) {
968
            $out = ['interpolate', $interp];
969
            $this->eatWhiteDefault = $oldWhite;
970 7
            if ($this->eatWhiteDefault) {
971
                $this->whitespace();
972
            }
973 16
974 16
            return true;
975
        }
976 16
977
        $this->eatWhiteDefault = $oldWhite;
978
        $this->seek($s);
979
980
        return false;
981
    }
982
983
    /**
984 49
     * @param $unit
985
     *
986
     * @return bool
987 49
     */
988 49
    protected function unit(&$unit)
989 49
    {
990 49
        // speed shortcut
991
        if (isset($this->buffer[$this->count])) {
992
            $char = $this->buffer[$this->count];
993
            if (!ctype_digit($char) && $char !== '.') {
994 49
                return false;
995 38
            }
996
        }
997 38
998
        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
999
            $unit = ['number', $m[1], empty($m[2]) ? '' : $m[2]];
1000 49
1001
            return true;
1002
        }
1003
1004
        return false;
1005
    }
1006
1007
1008
    /**
1009
     * a # color
1010
     *
1011 48
     * @param $out
1012
     *
1013 48
     * @return bool
1014 13
     */
1015 1
    protected function color(&$out)
1016
    {
1017 13
        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
1018
            if (strlen($m[1]) > 7) {
1019
                $out = ['string', '', [$m[1]]];
1020 13
            } else {
1021
                $out = ['raw_color', $m[1]];
1022
            }
1023 48
1024
            return true;
1025
        }
1026
1027
        return false;
1028
    }
1029
1030
    /**
1031
     * consume an argument definition list surrounded by ()
1032
     * each argument is a variable name with optional value
1033
     * or at the end a ... or a variable named followed by ...
1034
     * arguments are separated by , unless a ; is in the list, then ; is the
1035
     * delimiter.
1036
     *
1037
     * @param $args
1038
     * @param $isVararg
1039 45
     *
1040
     * @return bool
1041 45
     * @throws \LesserPhp\Exception\GeneralException
1042 45
     */
1043 45
    protected function argumentDef(&$args, &$isVararg)
1044
    {
1045
        $s = $this->seek();
1046 19
        if (!$this->literal('(')) {
1047 19
            return false;
1048 19
        }
1049
1050 19
        $values = [];
1051 19
        $delim = ',';
1052 19
        $method = 'expressionList';
1053 2
        $value = [];
1054 2
        $rhs = null;
1055
1056
        $isVararg = false;
1057 19
        while (true) {
1058 16
            if ($this->literal('...')) {
1059 16
                $isVararg = true;
1060 16
                break;
1061
            }
1062 16
1063 9
            if ($this->$method($value)) {
1064
                if ($value[0] === 'variable') {
1065 13
                    $arg = ['arg', $value[1]];
1066 13
                    $ss = $this->seek();
1067 2
1068 2
                    if ($this->assign() && $this->$method($rhs)) {
1069
                        $arg[] = $rhs;
1070
                    } else {
1071
                        $this->seek($ss);
1072 16
                        if ($this->literal('...')) {
1073 16
                            $arg[0] = 'rest';
1074 2
                            $isVararg = true;
1075
                        }
1076 16
                    }
1077
1078 15
                    $values[] = $arg;
1079
                    if ($isVararg) {
1080
                        break;
1081
                    }
1082
                    continue;
1083 19
                } else {
1084 19
                    $values[] = ['lit', $value];
1085
                }
1086 2
            }
1087 2
1088 2
1089
            if (!$this->literal($delim)) {
1090
                if ($delim === ',' && $this->literal(';')) {
1091 2
                    // found new delim, convert existing args
1092 2
                    $delim = ';';
1093 2
                    $method = 'propertyValue';
1094 2
                    $newArg = null;
1095 2
1096 2
                    // transform arg list
1097
                    if (isset($values[1])) { // 2 items
1098
                        $newList = [];
1099 2
                        foreach ($values as $i => $arg) {
1100 2
                            switch ($arg[0]) {
1101 2
                                case 'arg':
1102 2
                                    if ($i) {
1103 2
                                        throw new GeneralException('Cannot mix ; and , as delimiter types');
1104
                                    }
1105 2
                                    $newList[] = $arg[2];
1106
                                    break;
1107
                                case 'lit':
1108
                                    $newList[] = $arg[1];
1109 2
                                    break;
1110
                                case 'rest':
1111 2
                                    throw new GeneralException('Unexpected rest before semicolon');
1112 2
                            }
1113 2
                        }
1114 2
1115 1
                        $newList = ['list', ', ', $newList];
1116 1
1117 2
                        switch ($values[0][0]) {
1118
                            case 'arg':
1119 2
                                $newArg = ['arg', $values[0][1], $newList];
1120 2
                                break;
1121
                            case 'lit':
1122
                                $newArg = ['lit', $newList];
1123 2
                                break;
1124 2
                        }
1125
                    } elseif ($values) { // 1 item
0 ignored issues
show
Bug Best Practice introduced by
The expression $values of type null[] 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...
1126
                        $newArg = $values[0];
1127 19
                    }
1128
1129
                    if ($newArg !== null) {
1130
                        $values = [$newArg];
1131
                    }
1132 19
                } else {
1133
                    break;
1134
                }
1135
            }
1136
        }
1137
1138 19
        if (!$this->literal(')')) {
1139
            $this->seek($s);
1140 19
1141
            return false;
1142
        }
1143
1144
        $args = $values;
1145
1146
        return true;
1147
    }
1148
1149
    /**
1150
     * consume a list of tags
1151
     * this accepts a hanging delimiter
1152
     *
1153 49
     * @param array  $tags
1154
     * @param bool   $simple
1155 49
     * @param string $delim
1156 49
     *
1157 46
     * @return bool
1158 46
     */
1159 46
    protected function tags(&$tags, $simple = false, $delim = ',')
1160
    {
1161
        $tags = [];
1162
        while ($this->tag($tt, $simple)) {
1163 49
            $tags[] = $tt;
1164
            if (!$this->literal($delim)) {
1165
                break;
1166
            }
1167
        }
1168
1169
        return count($tags) !== 0;
1170
    }
1171
1172
    /**
1173
     * list of tags of specifying mixin path
1174 49
     * optionally separated by > (lazy, accepts extra >)
1175
     *
1176 49
     * @param array $tags
1177 49
     *
1178 22
     * @return bool
1179 22
     */
1180
    protected function mixinTags(&$tags)
1181
    {
1182 49
        $tags = [];
1183
        while ($this->tag($tt, true)) {
1184
            $tags[] = $tt;
1185
            $this->literal('>');
1186
        }
1187
1188
        return count($tags) !== 0;
1189
    }
1190
1191
    /**
1192
     * a bracketed value (contained within in a tag definition)
1193 49
     *
1194
     * @param array $parts
1195
     * @param bool  $hasExpression
1196 49
     *
1197 47
     * @return bool
1198
     */
1199
    protected function tagBracket(&$parts, &$hasExpression)
1200 49
    {
1201
        // speed shortcut
1202 49
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== '[') {
1203
            return false;
1204 49
        }
1205 3
1206
        $s = $this->seek();
1207 3
1208 3
        $hasInterpolation = false;
1209 3
1210 3
        if ($this->literal('[', false)) {
1211
            $attrParts = ['['];
1212
            // keyword, string, operator
1213 3
            while (true) {
1214
                if ($this->literal(']', false)) {
1215
                    $this->count--;
1216
                    break; // get out early
1217 3
                }
1218
1219 3
                if ($this->match('\s+', $m)) {
1220 3
                    $attrParts[] = ' ';
1221
                    continue;
1222
                }
1223 3
                if ($this->stringValue($str)) {
1224 3
                    // escape parent selector, (yuck)
1225 3
                    foreach ($str[2] as &$chunk) {
1226
                        $chunk = str_replace($this->lessc->getParentSelector(), '$&$', $chunk);
1227
                    }
1228 3
1229 3
                    $attrParts[] = $str;
1230 3
                    $hasInterpolation = true;
1231
                    continue;
1232
                }
1233 3
1234 1
                if ($this->keyword($word)) {
1235 1
                    $attrParts[] = $word;
1236 1
                    continue;
1237
                }
1238
1239
                if ($this->interpolation($inter)) {
1240 3
                    $attrParts[] = $inter;
1241 3
                    $hasInterpolation = true;
1242 3
                    continue;
1243
                }
1244
1245
                // operator, handles attr namespace too
1246
                if ($this->match('[|-~\$\*\^=]+', $m)) {
1247
                    $attrParts[] = $m[0];
1248 3
                    continue;
1249 3
                }
1250 3
1251 3
                break;
1252
            }
1253 3
1254
            if ($this->literal(']', false)) {
1255 3
                $attrParts[] = ']';
1256
                foreach ($attrParts as $part) {
1257
                    $parts[] = $part;
1258
                }
1259
                $hasExpression = $hasExpression || $hasInterpolation;
1260 49
1261
                return true;
1262 49
            }
1263
            $this->seek($s);
1264
        }
1265
1266
        $this->seek($s);
1267
1268
        return false;
1269
    }
1270
1271
    /**
1272
     * a space separated list of selectors
1273 49
     *
1274
     * @param      $tag
1275 49
     * @param bool $simple
1276 49
     *
1277
     * @return bool
1278 49
     */
1279
    protected function tag(&$tag, $simple = false)
1280
    {
1281 49
        if ($simple) {
1282
            $chars = '^@,:;{}\][>\(\) "\'';
1283 49
        } else {
1284 49
            $chars = '^@,;{}["\'';
1285 49
        }
1286
1287
        $s = $this->seek();
1288
1289 49
        $hasExpression = false;
1290 49
        $parts = [];
1291
        while ($this->tagBracket($parts, $hasExpression)) {
1292 49
            ;
1293 49
        }
1294 46
1295 46
        $oldWhite = $this->eatWhiteDefault;
1296 45
        $this->eatWhiteDefault = false;
1297
1298
        while (true) {
1299 46
            if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
1300
                $parts[] = $m[1];
1301
                if ($simple) {
1302 46
                    break;
1303
                }
1304
1305 49
                while ($this->tagBracket($parts, $hasExpression)) {
1306 13
                    ;
1307 2
                }
1308 2
                continue;
1309 2
            }
1310 2
1311
            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
1312
                if ($this->interpolation($interp)) {
1313 12
                    $hasExpression = true;
1314 12
                    $interp[2] = true; // don't unescape
1315 12
                    $parts[] = $interp;
1316
                    continue;
1317
                }
1318
1319 49
                if ($this->literal('@')) {
1320 9
                    $parts[] = '@';
1321 9
                    continue;
1322 9
                }
1323
            }
1324
1325 49
            if ($this->unit($unit)) { // for keyframes
1326
                $parts[] = $unit[1];
1327
                $parts[] = $unit[2];
1328 49
                continue;
1329 49
            }
1330 49
1331
            break;
1332 49
        }
1333
1334
        $this->eatWhiteDefault = $oldWhite;
1335 46
        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...
1336 4
            $this->seek($s);
1337
1338 46
            return false;
1339
        }
1340
1341 46
        if ($hasExpression) {
1342
            $tag = ['exp', ['string', '', $parts]];
1343 46
        } else {
1344
            $tag = trim(implode($parts));
1345
        }
1346
1347
        $this->whitespace();
1348
1349
        return true;
1350
    }
1351
1352
    /**
1353 48
     * a css function
1354
     *
1355 48
     * @param array $func
1356
     *
1357 48
     * @return bool
1358 25
     */
1359
    protected function func(&$func)
1360 25
    {
1361
        $s = $this->seek();
1362 25
1363 25
        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
1364 25
            $fname = $m[1];
1365
1366 25
            $sPreArgs = $this->seek();
1367 1
1368
            $args = [];
1369 24
            while (true) {
1370 24
                $ss = $this->seek();
1371 21
                // this ugly nonsense is for ie filter properties
1372
                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
1373
                    $args[] = ['string', '', [$name, '=', $value]];
1374
                } else {
1375 25
                    $this->seek($ss);
1376 25
                    if ($this->expressionList($value)) {
1377
                        $args[] = $value;
1378
                    }
1379 25
                }
1380
1381 25
                if (!$this->literal(',')) {
1382 25
                    break;
1383
                }
1384 25
            }
1385 7
            $args = ['list', ',', $args];
1386
1387 6
            if ($this->literal(')')) {
1388 6
                $func = ['function', $fname, $args];
1389 6
1390
                return true;
1391 6
            } elseif ($fname === 'url') {
1392
                // couldn't parse and in url? treat as string
1393
                $this->seek($sPreArgs);
1394
                if ($this->openString(')', $string) && $this->literal(')')) {
1395
                    $func = ['function', $fname, $string];
1396 48
1397
                    return true;
1398 48
                }
1399
            }
1400
        }
1401
1402
        $this->seek($s);
1403
1404
        return false;
1405
    }
1406
1407
    /**
1408 49
     * consume a less variable
1409
     *
1410 49
     * @param $name
1411 49
     *
1412 49
     * @return bool
1413
     */
1414 32
    protected function variable(&$name)
1415 1
    {
1416
        $s = $this->seek();
1417 32
        if ($this->literal($this->lessc->getVPrefix(), false) &&
1418
            ($this->variable($sub) || $this->keyword($name))
1419
        ) {
1420 32
            if (!empty($sub)) {
1421
                $name = ['variable', $sub];
1422
            } else {
1423 49
                $name = $this->lessc->getVPrefix() . $name;
1424 49
            }
1425
1426 49
            return true;
1427
        }
1428
1429
        $name = null;
1430
        $this->seek($s);
1431
1432
        return false;
1433
    }
1434
1435
    /**
1436
     * Consume an assignment operator
1437 48
     * Can optionally take a name that will be set to the current property name
1438
     *
1439 48
     * @param string $name
1440
     *
1441
     * @return bool
1442
     */
1443 48
    protected function assign($name = null)
1444
    {
1445
        if ($name !== null) {
1446
            $this->currentProperty = $name;
1447
        }
1448
1449
        return $this->literal(':') || $this->literal('=');
1450
    }
1451
1452
    /**
1453 49
     * consume a keyword
1454
     *
1455 49
     * @param $word
1456 48
     *
1457
     * @return bool
1458 48
     */
1459
    protected function keyword(&$word)
1460
    {
1461 49
        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
1462
            $word = $m[1];
1463
1464
            return true;
1465
        }
1466
1467
        return false;
1468
    }
1469 48
1470
    /**
1471 48
     * consume an end of statement delimiter
1472 48
     *
1473 12
     * @return bool
1474
     */
1475 10
    protected function end()
1476
    {
1477
        if ($this->literal(';', false)) {
1478 2
            return true;
1479
        } elseif ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
1480
            // if there is end of file or a closing block next then we don't need a ;
1481
            return true;
1482
        }
1483
1484
        return false;
1485
    }
1486 19
1487
    /**
1488 19
     * @param $guards
1489
     *
1490 19
     * @return bool
1491 19
     */
1492
    protected function guards(&$guards)
1493 19
    {
1494
        $s = $this->seek();
1495
1496 5
        if (!$this->literal('when')) {
1497
            $this->seek($s);
1498 5
1499 5
            return false;
1500 5
        }
1501 5
1502
        $guards = [];
1503
1504
        while ($this->guardGroup($g)) {
1505 5
            $guards[] = $g;
1506
            if (!$this->literal(',')) {
1507
                break;
1508
            }
1509
        }
1510
1511
        if (count($guards) === 0) {
1512 5
            $guards = null;
1513
            $this->seek($s);
1514
1515
            return false;
1516
        }
1517
1518
        return true;
1519
    }
1520
1521
    /**
1522 5
     * a bunch of guards that are and'd together
1523
     *
1524 5
     * @param $guardGroup
1525 5
     *
1526 5
     * @return bool
1527 5
     */
1528 5
    protected function guardGroup(&$guardGroup)
1529 5
    {
1530
        $s = $this->seek();
1531
        $guardGroup = [];
1532
        while ($this->guard($guard)) {
1533 5
            $guardGroup[] = $guard;
1534
            if (!$this->literal('and')) {
1535
                break;
1536
            }
1537
        }
1538
1539
        if (count($guardGroup) === 0) {
1540 5
            $guardGroup = null;
1541
            $this->seek($s);
1542
1543
            return false;
1544
        }
1545
1546
        return true;
1547
    }
1548 5
1549
    /**
1550 5
     * @param $guard
1551 5
     *
1552
     * @return bool
1553 5
     */
1554 5
    protected function guard(&$guard)
1555 5
    {
1556 1
        $s = $this->seek();
1557
        $negate = $this->literal('not');
1558
1559 5
        if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
1560
            $guard = $exp;
1561
            if ($negate) {
1562
                $guard = ['negate', $guard];
1563
            }
1564
1565
            return true;
1566
        }
1567
1568
        $this->seek($s);
1569
1570
        return false;
1571
    }
1572
1573
    /* raw parsing functions */
1574
1575 49
    /**
1576
     * @param string $what
1577 49
     * @param bool   $eatWhitespace
1578 49
     *
1579
     * @return bool
1580
     */
1581
    protected function literal($what, $eatWhitespace = null)
1582 49
    {
1583 49
        if ($eatWhitespace === null) {
1584 49
            $eatWhitespace = $this->eatWhiteDefault;
1585 49
        }
1586
1587 49
        // shortcut on single letter
1588
        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
1589
            if ($this->buffer[$this->count] === $what) {
1590
                if (!$eatWhitespace) {
1591 49
                    $this->count++;
1592
1593
                    return true;
1594
                }
1595 49
                // goes below...
1596 11
            } else {
1597
                return false;
1598
            }
1599 49
        }
1600
1601
        if (!isset(self::$literalCache[$what])) {
1602
            self::$literalCache[$what] = Compiler::pregQuote($what);
1603
        }
1604
1605
        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
1606
    }
1607
1608
    /**
1609
     * @param        $out
1610 3
     * @param string $parseItem
1611
     * @param string $delim
1612
     * @param bool   $flatten
1613 3
     *
1614 3
     * @return bool
1615 3
     */
1616 3
    protected function genericList(&$out, $parseItem, $delim = "", $flatten = true)
1617 3
    {
1618 3
        // $parseItem is one of mediaQuery, mediaExpression
1619 3
        $s = $this->seek();
1620
        $items = [];
1621
        $value = null;
1622
        while ($this->$parseItem($value)) {
1623
            $items[] = $value;
1624 3
            if ($delim) {
1625
                if (!$this->literal($delim)) {
1626
                    break;
1627
                }
1628
            }
1629
        }
1630 3
1631
        if (count($items) === 0) {
1632
            $this->seek($s);
1633 3
1634
            return false;
1635
        }
1636 3
1637
        if ($flatten && count($items) === 1) {
1638
            $out = $items[0];
1639
        } else {
1640
            $out = ['list', $delim, $items];
1641
        }
1642
1643
        return true;
1644
    }
1645
1646
    /**
1647
     * try to match something on head of buffer
1648
     *
1649
     * @param string $regex
1650
     * @param        $out
1651
     * @param bool   $eatWhitespace
1652
     *
1653
     * @return bool
1654
     */
1655
    protected function match($regex, &$out, $eatWhitespace = null)
1656
    {
1657
        if ($eatWhitespace === null) {
1658
            $eatWhitespace = $this->eatWhiteDefault;
1659
        }
1660
1661
        $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais';
1662
        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
1663
            $this->count += strlen($out[0]);
1664
            if ($eatWhitespace && $this->writeComments) {
1665
                $this->whitespace();
1666
            }
1667
1668
            return true;
1669
        }
1670
1671
        return false;
1672
    }
1673
1674
    /**
1675
     * match some whitespace
1676
     *
1677
     * @return bool
1678
     */
1679 49
    protected function whitespace()
1680
    {
1681 49
        if ($this->writeComments) {
1682 49
            $gotWhite = false;
1683
            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
1684
                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
1685 49
                    $this->append(['comment', $m[1]]);
1686 49
                    $this->seenComments[$this->count] = true;
1687 49
                }
1688 49
                $this->count += mb_strlen($m[0]);
1689 1
                $gotWhite = true;
1690
            }
1691
1692 49
            return $gotWhite;
1693
        }
1694
1695 49
        $this->match('', $m);
1696
1697
        return mb_strlen($m[0]) > 0;
1698
    }
1699
1700
    /**
1701
     * match something without consuming it
1702
     *
1703 49
     * @param string $regex
1704
     * @param array  $out
1705 49
     * @param int    $from
1706 1
     *
1707 1
     * @return int
1708 1
     */
1709 1
    protected function peek($regex, &$out = null, $from = null)
1710 1
    {
1711
        if ($from === null) {
1712 1
            $from = $this->count;
1713 1
        }
1714
        $r = '/' . $regex . '/Ais';
1715
1716 1
        return preg_match($r, $this->buffer, $out, null, $from);
1717
    }
1718
1719 49
    /**
1720 49
     * seek to a spot in the buffer or return where we are on no argument
1721
     *
1722
     * @param int $where
1723
     *
1724
     * @return int
1725
     */
1726
    protected function seek($where = null)
1727
    {
1728
        if ($where !== null) {
1729
            $this->count = $where;
1730
        }
1731
1732 24
        return $this->count;
1733
    }
1734 24
1735 20
    /* misc functions */
1736
1737 24
    /**
1738
     * @param string $msg
1739 24
     * @param int    $count
1740
     *
1741
     * @throws \LesserPhp\Exception\GeneralException
1742
     */
1743
    public function throwError($msg = 'parse error', $count = null)
1744
    {
1745
        $count = $count === null ? $this->count : $count;
1746
1747
        $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n");
1748
1749 49
        if (!empty($this->sourceName)) {
1750
            $loc = "$this->sourceName on line $line";
1751 49
        } else {
1752 49
            $loc = "line: $line";
1753
        }
1754
1755 49
        // TODO this depends on $this->count
1756
        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
1757
            throw new GeneralException("$msg: failed at `$m[1]` $loc");
1758
        } else {
1759
            throw new GeneralException("$msg: $loc");
1760
        }
1761
    }
1762
1763
    /**
1764
     * @param null $selectors
1765
     * @param null $type
1766 5
     *
1767
     * @return \stdClass
1768 5
     */
1769
    protected function pushBlock($selectors = null, $type = null)
1770 5
    {
1771
        $b = new \stdClass();
1772 5
        $b->parent = $this->env;
1773
1774
        $b->type = $type;
1775 5
        $b->id = self::$nextBlockId++;
1776
1777
        $b->isVararg = false; // TODO: kill me from here
1778
        $b->tags = $selectors;
1779 5
1780 5
        $b->props = [];
1781
        $b->children = [];
1782
1783
        // add a reference to the parser so
1784
        // we can access the parser to throw errors
1785
        // or retrieve the sourceName of this block.
1786
        $b->parser = $this;
1787
1788
        // so we know the position of this block
1789
        $b->count = $this->count;
1790
1791
        $this->env = $b;
1792 49
1793
        return $b;
1794 49
    }
1795 49
1796
    /**
1797 49
     * push a block that doesn't multiply tags
1798 49
     *
1799
     * @param string $type
1800 49
     *
1801 49
     * @return \stdClass
1802
     */
1803 49
    protected function pushSpecialBlock($type)
1804 49
    {
1805
        return $this->pushBlock(null, $type);
1806
    }
1807
1808
    /**
1809 49
     * append a property to the current block
1810
     *
1811
     * @param      $prop
1812 49
     * @param int  $pos
1813
     */
1814 49
    protected function append($prop, $pos = null)
1815
    {
1816 49
        if ($pos !== null) {
1817
            $prop[-1] = $pos;
1818
        }
1819
        $this->env->props[] = $prop;
1820
    }
1821
1822
    /**
1823
     * pop something off the stack
1824
     *
1825
     * @return mixed
1826 49
     */
1827
    protected function pop()
1828 49
    {
1829
        $old = $this->env;
1830
        $this->env = $this->env->parent;
1831
1832
        return $old;
1833
    }
1834
1835
    /**
1836
     * remove comments from $text
1837 49
     * todo: make it work for all functions, not just url
1838
     *
1839 49
     * @param string $text
1840 49
     *
1841
     * @return string
1842 49
     */
1843 49
    protected function removeComments($text)
1844
    {
1845
        $look = [
1846
            'url(',
1847
            '//',
1848
            '/*',
1849
            '"',
1850 46
            "'",
1851
        ];
1852 46
1853 46
        $out = '';
1854
        $min = null;
1855 46
        while (true) {
1856
            // find the next item
1857
            foreach ($look as $token) {
1858
                $pos = mb_strpos($text, $token);
1859
                if ($pos !== false) {
1860
                    if ($min === null || $pos < $min[1]) {
1861
                        $min = [$token, $pos];
1862
                    }
1863
                }
1864
            }
1865
1866 49
            if ($min === null) {
1867
                break;
1868
            }
1869 49
1870
            $count = $min[1];
1871
            $skip = 0;
1872
            $newlines = 0;
1873
            switch ($min[0]) {
1874
                case 'url(':
1875
                    if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) {
1876 49
                        $count += mb_strlen($m[0]) - mb_strlen($min[0]);
1877 49
                    }
1878 49
                    break;
1879
                case '"':
1880 49
                case "'":
1881 49
                    if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count)) {
1882 49
                        $count += mb_strlen($m[0]) - 1;
1883 27
                    }
1884 49
                    break;
1885
                case '//':
1886
                    $skip = mb_strpos($text, "\n", $count);
1887
                    if ($skip === false) {
1888
                        $skip = mb_strlen($text) - $count;
1889 49
                    } else {
1890 49
                        $skip -= $count;
1891
                    }
1892
                    break;
1893 27
                case '/*':
1894 27
                    if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
1895 27
                        $skip = mb_strlen($m[0]);
1896 27
                        $newlines = mb_substr_count($m[0], "\n");
1897 27
                    }
1898 6
                    break;
1899 6
            }
1900
1901 6
            if ($skip === 0) {
1902 27
                $count += mb_strlen($min[0]);
1903 22
            }
1904 24
1905 24
            $out .= mb_substr($text, 0, $count) . str_repeat("\n", $newlines);
1906
            $text = mb_substr($text, $count + $skip);
1907 24
1908 18
            $min = null;
1909 18
        }
1910 18
1911
        return $out . $text;
1912
    }
1913 18
1914
    /**
1915 18
     * @param bool $writeComments
1916 4
     */
1917 4
    public function setWriteComments($writeComments)
1918 4
    {
1919 4
        $this->writeComments = $writeComments;
1920
    }
1921 4
1922
    /**
1923
     * @param int $s
1924 27
     *
1925 24
     * @return bool
1926
     */
1927
    protected function handleLiteralMedia($s)
1928 27
    {
1929 27
        // seriously, this || true is required for this statement to work!?
1930
        if (($this->mediaQueryList($mediaQueries) || true) && $this->literal('{')) {
1931 27
            $media = $this->pushSpecialBlock('media');
1932
            $media->queries = $mediaQueries === null ? [] : $mediaQueries;
1933
1934 49
            return true;
1935
        } else {
1936
            $this->seek($s);
1937
        }
1938
1939
        return false;
1940 49
    }
1941
1942 49
    /**
1943 49
     * @param string $directiveName
1944
     *
1945
     * @return bool
1946
     */
1947
    protected function handleDirectiveBlock($directiveName)
1948
    {
1949
        // seriously, this || true is required for this statement to work!?
1950 3
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1951
            $dir = $this->pushSpecialBlock('directive');
1952
            $dir->name = $directiveName;
1953 3
            if ($directiveValue !== null) {
1954 3
                $dir->value = $directiveValue;
1955 3
            }
1956
1957 3
            return true;
1958
        }
1959
1960
        return false;
1961
    }
1962
1963
    /**
1964
     * @param string $directiveName
1965
     *
1966
     * @return bool
1967
     */
1968
    protected function handleDirectiveLine($directiveName)
1969
    {
1970 4
        if ($this->propertyValue($directiveValue) && $this->end()) {
1971
            $this->append(['directive', $directiveName, $directiveValue]);
1972
1973 4
            return true;
1974 4
        }
1975 4
1976 4
        return false;
1977 2
    }
1978
1979
    /**
1980 4
     * @param string $directiveName
1981
     *
1982
     * @return bool
1983 1
     */
1984
    protected function handleRulesetDefinition($directiveName)
1985
    {
1986
        //Ruleset Definition
1987
        // seriously, this || true is required for this statement to work!?
1988
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1989
            $dir = $this->pushBlock($this->fixTags(['@' . $directiveName]));
1990
            $dir->name = $directiveName;
1991 1
            if ($directiveValue !== null) {
1992
                $dir->value = $directiveValue;
1993 1
            }
1994 1
1995
            return true;
1996 1
        }
1997
1998
        return false;
1999
    }
2000
2001
    private function clearBlockStack()
2002
    {
2003
        $this->env = null;
2004
    }
2005
}
2006