Completed
Pull Request — master (#7)
by Marcus
04:20 queued 02:09
created

Parser::stringValue()   C

Complexity

Conditions 9
Paths 17

Size

Total Lines 53
Code Lines 35

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 36
CRAP Score 9

Importance

Changes 0
Metric Value
dl 0
loc 53
ccs 36
cts 36
cp 1
rs 6.8963
c 0
b 0
f 0
cc 9
eloc 35
nc 17
nop 1
crap 9

How to fix   Long Method   

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
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
    protected static $nextBlockId = 0; // used to uniquely identify blocks
23
24
    protected static $precedence = [
25
        '=<' => 0,
26
        '>=' => 0,
27
        '=' => 0,
28
        '<' => 0,
29
        '>' => 0,
30
31
        '+' => 1,
32
        '-' => 1,
33
        '*' => 2,
34
        '/' => 2,
35
        '%' => 2,
36
    ];
37
38
    protected static $whitePattern;
39
    protected static $commentMulti;
40
41
    protected static $commentSingle = "//";
42
    protected static $commentMultiLeft = "/*";
43
    protected static $commentMultiRight = "*/";
44
45
    // regex string to match any of the operators
46
    protected static $operatorString;
47
48
    // these properties will supress division unless it's inside parenthases
49
    protected static $supressDivisionProps =
50
        ['/border-radius$/i', '/^font$/i'];
51
52
    private $blockDirectives = [
53
        'font-face',
54
        'keyframes',
55
        'page',
56
        '-moz-document',
57
        'viewport',
58
        '-moz-viewport',
59
        '-o-viewport',
60
        '-ms-viewport',
61
    ];
62
    private $lineDirectives = ['charset'];
63
64
    /**
65
     * if we are in parens we can be more liberal with whitespace around
66
     * operators because it must evaluate to a single value and thus is less
67
     * ambiguous.
68
     *
69
     * Consider:
70
     *     property1: 10 -5; // is two numbers, 10 and -5
71
     *     property2: (10 -5); // should evaluate to 5
72
     */
73
    protected $inParens = false;
74
75
    // caches preg escaped literals
76
    protected static $literalCache = [];
77
    /** @var int */
78
    public $count;
79
    /** @var int */
80
    private $line;
81
    /** @var array */
82
    private $seenComments;
83
    /** @var string */
84
    public $buffer;
85
86
    private $env;
87
    /** @var bool */
88
    private $inExp;
89
    /** @var string */
90
    private $currentProperty;
91
92
    /**
93
     * @var bool
94
     */
95
    private $writeComments = false;
96
97
    /**
98
     * Parser constructor.
99
     *
100
     * @param \LesserPhp\Compiler $lessc
101
     * @param null                $sourceName
102
     */
103 49
    public function __construct(Compiler $lessc, $sourceName = null)
104
    {
105 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...
106
        // reference to less needed for vPrefix, mPrefix, and parentSelector
107 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...
108
109 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...
110
111 49
        if (!self::$operatorString) {
112 1
            self::$operatorString =
113 1
                '(' . implode('|', array_map([Compiler::class, 'pregQuote'], array_keys(self::$precedence))) . ')';
114
115 1
            $commentSingle = Compiler::pregQuote(self::$commentSingle);
116 1
            $commentMultiLeft = Compiler::pregQuote(self::$commentMultiLeft);
117 1
            $commentMultiRight = Compiler::pregQuote(self::$commentMultiRight);
118
119 1
            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
120 1
            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
121 1
        }
122 49
    }
123
124
    /**
125
     * @param $buffer
126
     *
127
     * @return mixed
128
     * @throws \LesserPhp\Exception\GeneralException
129
     */
130 49
    public function parse($buffer)
131
    {
132 49
        $this->count = 0;
133 49
        $this->line = 1;
134
135 49
        $this->env = null; // block stack
136 49
        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
137 49
        $this->pushSpecialBlock('root');
138 49
        $this->eatWhiteDefault = true;
139 49
        $this->seenComments = [];
140
141 49
        $this->whitespace();
142
143
        // parse the entire file
144 49
        while (false !== $this->parseChunk()) {
145
            ;
146 49
        }
147
148 49
        if ($this->count !== strlen($this->buffer)) {
149
            //            var_dump($this->count);
150
//            var_dump($this->buffer);
151
            $this->throwError();
152
        }
153
154
        // TODO report where the block was opened
155 49
        if (!property_exists($this->env, 'parent') || $this->env->parent !== null) {
156
            throw new GeneralException('parse error: unclosed block');
157
        }
158
159 49
        return $this->env;
160
    }
161
162
    /**
163
     * Parse a single chunk off the head of the buffer and append it to the
164
     * current parse environment.
165
     * Returns false when the buffer is empty, or when there is an error.
166
     *
167
     * This function is called repeatedly until the entire document is
168
     * parsed.
169
     *
170
     * This parser is most similar to a recursive descent parser. Single
171
     * functions represent discrete grammatical rules for the language, and
172
     * they are able to capture the text that represents those rules.
173
     *
174
     * Consider the function \LesserPhp\Compiler::keyword(). (all parse functions are
175
     * structured the same)
176
     *
177
     * The function takes a single reference argument. When calling the
178
     * function it will attempt to match a keyword on the head of the buffer.
179
     * If it is successful, it will place the keyword in the referenced
180
     * argument, advance the position in the buffer, and return true. If it
181
     * fails then it won't advance the buffer and it will return false.
182
     *
183
     * All of these parse functions are powered by \LesserPhp\Compiler::match(), which behaves
184
     * the same way, but takes a literal regular expression. Sometimes it is
185
     * more convenient to use match instead of creating a new function.
186
     *
187
     * Because of the format of the functions, to parse an entire string of
188
     * grammatical rules, you can chain them together using &&.
189
     *
190
     * But, if some of the rules in the chain succeed before one fails, then
191
     * the buffer position will be left at an invalid state. In order to
192
     * avoid this, \LesserPhp\Compiler::seek() is used to remember and set buffer positions.
193
     *
194
     * Before parsing a chain, use $s = $this->seek() to remember the current
195
     * position into $s. Then if a chain fails, use $this->seek($s) to
196
     * go back where we started.
197
     * @throws \LesserPhp\Exception\GeneralException
198
     */
199 49
    protected function parseChunk()
200
    {
201 49
        if (empty($this->buffer)) {
202
            return false;
203
        }
204 49
        $s = $this->seek();
205
206 49
        if ($this->whitespace()) {
207 46
            return true;
208
        }
209
210
        // setting a property
211 49
        if ($this->keyword($key) && $this->assign() && $this->propertyValue($value, $key) && $this->end()) {
212 47
            $this->append(['assign', $key, $value], $s);
213
214 47
            return true;
215
        } else {
216 49
            $this->seek($s);
217
        }
218
219
        // look for special css blocks
220 49
        if ($this->literal('@', false)) {
221 26
            $this->count--;
222
223
            // media
224 26
            if ($this->literal('@media')) {
225 3
                return $this->handleLiteralMedia($s);
226
            }
227
228 26
            if ($this->literal('@', false) && $this->keyword($directiveName)) {
229 26
                if ($this->isDirective($directiveName, $this->blockDirectives)) {
230 4
                    if ($this->handleDirectiveBlock($directiveName) === true) {
231 4
                        return true;
232
                    }
233 25
                } elseif ($this->isDirective($directiveName, $this->lineDirectives)) {
234 1
                    if ($this->handleDirectiveLine($directiveName) === true) {
235 1
                        return true;
236
                    }
237 25
                } elseif ($this->literal(':', true)) {
238 24
                    if ($this->handleRulesetDefinition($directiveName) === true) {
239 1
                        return true;
240
                    }
241 23
                }
242 25
            }
243
244 25
            $this->seek($s);
245 25
        }
246
247 49
        if ($this->literal('&', false)) {
248 4
            $this->count--;
249 4
            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...
250
                // hierauf folgt was in runden klammern, und zwar das element, das erweitert werden soll
251
                // heißt also, das was in klammern steht wird um die aktuellen klassen erweitert
252
                /*
253
Aus
254
255
nav ul {
256
  &:extend(.inline);
257
  background: blue;
258
}
259
.inline {
260
  color: red;
261
}
262
263
264
Wird:
265
266
nav ul {
267
  background: blue;
268
}
269
.inline,
270
nav ul {
271
  color: red;
272
}
273
274
                 */
275
//                echo "Here we go";
276
            }
277 4
        }
278
279
280
        // setting a variable
281 49
        if ($this->variable($var) && $this->assign() &&
282 49
            $this->propertyValue($value) && $this->end()
283 49
        ) {
284 23
            $this->append(['assign', $var, $value], $s);
285
286 23
            return true;
287
        } else {
288 49
            $this->seek($s);
289
        }
290
291 49
        if ($this->import($importValue)) {
292 3
            $this->append($importValue, $s);
293
294 3
            return true;
295
        }
296
297
        // opening parametric mixin
298 49
        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
299 49
            ($this->guards($guards) || true) &&
300 19
            $this->literal('{')
301 49
        ) {
302 18
            $block = $this->pushBlock($this->fixTags([$tag]));
303 18
            $block->args = $args;
304 18
            $block->isVararg = $isVararg;
305 18
            if (!empty($guards)) {
306 5
                $block->guards = $guards;
307 5
            }
308
309 18
            return true;
310
        } else {
311 49
            $this->seek($s);
312
        }
313
314
        // opening a simple block
315 49
        if ($this->tags($tags) && $this->literal('{', false)) {
316 46
            $tags = $this->fixTags($tags);
317 46
            $this->pushBlock($tags);
318
319 46
            return true;
320
        } else {
321 49
            $this->seek($s);
322
        }
323
324
        // closing a block
325 49
        if ($this->literal('}', false)) {
326
            try {
327 46
                $block = $this->pop();
328 46
            } catch (\Exception $e) {
329
                $this->seek($s);
330
                $this->throwError($e->getMessage());
331
            }
332
333 46
            $hidden = false;
334 46
            if ($block->type === null) {
335 46
                $hidden = true;
336 46
                if (!isset($block->args)) {
337 46
                    foreach ($block->tags as $tag) {
338 46
                        if (!is_string($tag) || $tag[0] !== $this->lessc->getMPrefix()) {
339 46
                            $hidden = false;
340 46
                            break;
341
                        }
342 46
                    }
343 46
                }
344
345 46
                foreach ($block->tags as $tag) {
346 46
                    if (is_string($tag)) {
347 46
                        $this->env->children[$tag][] = $block;
348 46
                    }
349 46
                }
350 46
            }
351
352 46
            if (!$hidden) {
353 46
                $this->append(['block', $block], $s);
354 46
            }
355
356
            // this is done here so comments aren't bundled into he block that
357
            // was just closed
358 46
            $this->whitespace();
359
360 46
            return true;
361
        }
362
363
        // mixin
364 49
        if ($this->mixinTags($tags) &&
365 49
            ($this->argumentDef($argv, $isVararg) || true) &&
366 49
            ($this->keyword($suffix) || true) && $this->end()
367 49
        ) {
368 22
            $tags = $this->fixTags($tags);
369 22
            $this->append(['mixin', $tags, $argv, $suffix], $s);
370
371 22
            return true;
372
        } else {
373 49
            $this->seek($s);
374
        }
375
376
        // spare ;
377 49
        if ($this->literal(';')) {
378 4
            return true;
379
        }
380
381 49
        return false; // got nothing, throw error
382
    }
383
384
    /**
385
     * @param string $directiveName
386
     * @param array $directives
387
     *
388
     * @return bool
389
     */
390 26
    protected function isDirective($directiveName, array $directives)
391
    {
392
        // TODO: cache pattern in parser
393 26
        $pattern = implode('|', array_map([Compiler::class, 'pregQuote'], $directives));
394 26
        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
395
396 26
        return (preg_match($pattern, $directiveName) === 1);
397
    }
398
399
    /**
400
     * @param array $tags
401
     *
402
     * @return mixed
403
     */
404 46
    protected function fixTags(array $tags)
405
    {
406
        // move @ tags out of variable namespace
407 46
        foreach ($tags as &$tag) {
408 46
            if ($tag[0] === $this->lessc->getVPrefix()) {
409 8
                $tag[0] = $this->lessc->getMPrefix();
410 8
            }
411 46
        }
412
413 46
        return $tags;
414
    }
415
416
    /**
417
     * a list of expressions
418
     *
419
     * @param $exps
420
     *
421
     * @return bool
422
     */
423 48
    protected function expressionList(&$exps)
424
    {
425 48
        $values = [];
426
427 48
        while ($this->expression($exp)) {
428 48
            $values[] = $exp;
429 48
        }
430
431 48
        if (count($values) === 0) {
432 26
            return false;
433
        }
434
435 48
        $exps = Compiler::compressList($values, ' ');
436
437 48
        return true;
438
    }
439
440
    /**
441
     * Attempt to consume an expression.
442
     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
443
     *
444
     * @param $out
445
     *
446
     * @return bool
447
     */
448 48
    protected function expression(&$out)
449
    {
450 48
        if ($this->value($lhs)) {
451 48
            $out = $this->expHelper($lhs, 0);
452
453
            // look for / shorthand
454 48
            if (!empty($this->env->supressedDivision)) {
455 2
                unset($this->env->supressedDivision);
456 2
                $s = $this->seek();
457 2
                if ($this->literal("/") && $this->value($rhs)) {
458
                    $out = [
459 2
                        "list",
460 2
                        "",
461 2
                        [$out, ["keyword", "/"], $rhs],
462 2
                    ];
463 2
                } else {
464
                    $this->seek($s);
465
                }
466 2
            }
467
468 48
            return true;
469
        }
470
471 48
        return false;
472
    }
473
474
    /**
475
     * recursively parse infix equation with $lhs at precedence $minP
476
     *
477
     * @param $lhs
478
     * @param $minP
479
     *
480
     * @return array
481
     */
482 48
    protected function expHelper($lhs, $minP)
483
    {
484 48
        $this->inExp = true;
485 48
        $ss = $this->seek();
486
487 48
        while (true) {
488 48
            $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
489
490
            // If there is whitespace before the operator, then we require
491
            // whitespace after the operator for it to be an expression
492 48
            $needWhite = $whiteBefore && !$this->inParens;
493
494 48
            if ($this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) &&
495 20
                self::$precedence[$m[1]] >= $minP
496 48
            ) {
497 20
                if (!$this->inParens &&
498 20
                    isset($this->env->currentProperty) &&
499 20
                    $m[1] === '/' &&
500
                    empty($this->env->supressedDivision)
501 20
                ) {
502 5
                    foreach (self::$supressDivisionProps as $pattern) {
503 5
                        if (preg_match($pattern, $this->env->currentProperty)) {
504 2
                            $this->env->supressedDivision = true;
505 2
                            break 2;
506
                        }
507 5
                    }
508 4
                }
509
510 20
                $whiteAfter = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
511
512 20
                if (!$this->value($rhs)) {
513
                    break;
514
                }
515
516
                // peek for next operator to see what to do with rhs
517 20
                if ($this->peek(self::$operatorString, $next) &&
518 3
                    self::$precedence[$next[1]] > self::$precedence[$m[1]]
519 20
                ) {
520 1
                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
521 1
                }
522
523 20
                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
524 20
                $ss = $this->seek();
525
526 20
                continue;
527
            }
528
529 48
            break;
530
        }
531
532 48
        $this->seek($ss);
533
534 48
        return $lhs;
535
    }
536
537
    /**
538
     * consume a list of values for a property
539
     *
540
     * @param      $value
541
     * @param string $keyName
542
     *
543
     * @return bool
544
     */
545 48
    public function propertyValue(&$value, $keyName = null)
546
    {
547 48
        $values = [];
548
549 48
        if ($keyName !== null) {
550 47
            $this->env->currentProperty = $keyName;
551 47
        }
552
553 48
        $s = null;
554 48
        while ($this->expressionList($v)) {
555 48
            $values[] = $v;
556 48
            $s = $this->seek();
557 48
            if (!$this->literal(',')) {
558 48
                break;
559
            }
560 7
        }
561
562 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...
563 48
            $this->seek($s);
564 48
        }
565
566 48
        if ($keyName !== null) {
567 47
            unset($this->env->currentProperty);
568 47
        }
569
570 48
        if (count($values) === 0) {
571 3
            return false;
572
        }
573
574 48
        $value = Compiler::compressList($values, ', ');
575
576 48
        return true;
577
    }
578
579
    /**
580
     * @param $out
581
     *
582
     * @return bool
583
     */
584 48
    protected function parenValue(&$out)
585
    {
586 48
        $s = $this->seek();
587
588
        // speed shortcut
589 48
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== "(") {
590 48
            return false;
591
        }
592
593 5
        $inParens = $this->inParens;
594 5
        if ($this->literal("(") &&
595 5
            ($this->inParens = true) && $this->expression($exp) &&
596 3
            $this->literal(")")
597 5
        ) {
598 3
            $out = $exp;
599 3
            $this->inParens = $inParens;
600
601 3
            return true;
602
        } else {
603 2
            $this->inParens = $inParens;
604 2
            $this->seek($s);
605
        }
606
607 2
        return false;
608
    }
609
610
    /**
611
     * a single value
612
     *
613
     * @param array $value
614
     *
615
     * @return bool
616
     */
617 48
    protected function value(&$value)
618
    {
619 48
        $s = $this->seek();
620
621
        // speed shortcut
622 48
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === "-") {
623
            // negation
624 7
            if ($this->literal("-", false) &&
625 7
                (($this->variable($inner) && $inner = ["variable", $inner]) ||
626 6
                    $this->unit($inner) ||
627 3
                    $this->parenValue($inner))
628 7
            ) {
629 6
                $value = ["unary", "-", $inner];
630
631 6
                return true;
632
            } else {
633 2
                $this->seek($s);
634
            }
635 2
        }
636
637 48
        if ($this->parenValue($value)) {
638 3
            return true;
639
        }
640 48
        if ($this->unit($value)) {
641 38
            return true;
642
        }
643 48
        if ($this->color($value)) {
644 13
            return true;
645
        }
646 48
        if ($this->func($value)) {
647 25
            return true;
648
        }
649 48
        if ($this->stringValue($value)) {
650 21
            return true;
651
        }
652
653 48
        if ($this->keyword($word)) {
654 35
            $value = ['keyword', $word];
655
656 35
            return true;
657
        }
658
659
        // try a variable
660 48
        if ($this->variable($var)) {
661 29
            $value = ['variable', $var];
662
663 29
            return true;
664
        }
665
666
        // unquote string (should this work on any type?
667 48
        if ($this->literal("~") && $this->stringValue($str)) {
668 4
            $value = ["escape", $str];
669
670 4
            return true;
671
        } else {
672 48
            $this->seek($s);
673
        }
674
675
        // css hack: \0
676 48
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
677 1
            $value = ['keyword', '\\' . $m[1]];
678
679 1
            return true;
680
        } else {
681 48
            $this->seek($s);
682
        }
683
684 48
        return false;
685
    }
686
687
    /**
688
     * an import statement
689
     *
690
     * @param array $out
691
     *
692
     * @return bool|null
693
     */
694 49
    protected function import(&$out)
695
    {
696 49
        if (!$this->literal('@import')) {
697 49
            return false;
698
        }
699
700
        // @import "something.css" media;
701
        // @import url("something.css") media;
702
        // @import url(something.css) media;
703
704 3
        if ($this->propertyValue($value)) {
705 3
            $out = ["import", $value];
706
707 3
            return true;
708
        }
709
710
        return false;
711
    }
712
713
    /**
714
     * @param $out
715
     *
716
     * @return bool
717
     */
718 3
    protected function mediaQueryList(&$out)
719
    {
720 3
        if ($this->genericList($list, "mediaQuery", ",", false)) {
721 3
            $out = $list[2];
722
723 3
            return true;
724
        }
725
726
        return false;
727
    }
728
729
    /**
730
     * @param $out
731
     *
732
     * @return bool
733
     */
734 3
    protected function mediaQuery(&$out)
735
    {
736 3
        $s = $this->seek();
737
738 3
        $expressions = null;
739 3
        $parts = [];
740
741 3
        if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) &&
742 3
            $this->keyword($mediaType)
743 3
        ) {
744 3
            $prop = ["mediaType"];
745 3
            if (isset($only)) {
746
                $prop[] = "only";
747
            }
748 3
            if (isset($not)) {
749
                $prop[] = "not";
750
            }
751 3
            $prop[] = $mediaType;
752 3
            $parts[] = $prop;
753 3
        } else {
754 1
            $this->seek($s);
755
        }
756
757
758 3
        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...
759
            // ~
760 3
        } else {
761 1
            $this->genericList($expressions, "mediaExpression", "and", false);
762 1
            if (is_array($expressions)) {
763 1
                $parts = array_merge($parts, $expressions[2]);
764 1
            }
765
        }
766
767 3
        if (count($parts) === 0) {
768
            $this->seek($s);
769
770
            return false;
771
        }
772
773 3
        $out = $parts;
774
775 3
        return true;
776
    }
777
778
    /**
779
     * @param $out
780
     *
781
     * @return bool
782
     */
783 1
    protected function mediaExpression(&$out)
784
    {
785 1
        $s = $this->seek();
786 1
        $value = null;
787 1
        if ($this->literal("(") &&
788 1
            $this->keyword($feature) &&
789 1
            ($this->literal(":") && $this->expression($value) || true) &&
790 1
            $this->literal(")")
791 1
        ) {
792 1
            $out = ["mediaExp", $feature];
793 1
            if ($value) {
794 1
                $out[] = $value;
795 1
            }
796
797 1
            return true;
798 1
        } elseif ($this->variable($variable)) {
799 1
            $out = ['variable', $variable];
800
801 1
            return true;
802
        }
803
804
        $this->seek($s);
805
806
        return false;
807
    }
808
809
    /**
810
     * an unbounded string stopped by $end
811
     *
812
     * @param      $end
813
     * @param      $out
814
     * @param null $nestingOpen
815
     * @param null $rejectStrs
816
     *
817
     * @return bool
818
     */
819 26
    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
820
    {
821 26
        $oldWhite = $this->eatWhiteDefault;
822 26
        $this->eatWhiteDefault = false;
823
824 26
        $stop = ["'", '"', "@{", $end];
825 26
        $stop = array_map([Compiler::class, 'pregQuote'], $stop);
826
        // $stop[] = self::$commentMulti;
827
828 26
        if ($rejectStrs !== null) {
829 25
            $stop = array_merge($stop, $rejectStrs);
830 25
        }
831
832 26
        $patt = '(.*?)(' . implode("|", $stop) . ')';
833
834 26
        $nestingLevel = 0;
835
836 26
        $content = [];
837 26
        while ($this->match($patt, $m, false)) {
838 26
            if (!empty($m[1])) {
839 25
                $content[] = $m[1];
840 25
                if ($nestingOpen) {
841
                    $nestingLevel += substr_count($m[1], $nestingOpen);
842
                }
843 25
            }
844
845 26
            $tok = $m[2];
846
847 26
            $this->count -= strlen($tok);
848 26
            if ($tok == $end) {
849 14
                if ($nestingLevel === 0) {
850 14
                    break;
851
                } else {
852
                    $nestingLevel--;
853
                }
854
            }
855
856 24
            if (($tok === "'" || $tok === '"') && $this->stringValue($str)) {
857 13
                $content[] = $str;
858 13
                continue;
859
            }
860
861 23
            if ($tok === "@{" && $this->interpolation($inter)) {
862 3
                $content[] = $inter;
863 3
                continue;
864
            }
865
866 23
            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
867 23
                break;
868
            }
869
870
            $content[] = $tok;
871
            $this->count += strlen($tok);
872
        }
873
874 26
        $this->eatWhiteDefault = $oldWhite;
875
876 26
        if (count($content) === 0) {
877 4
            return false;
878
        }
879
880
        // trim the end
881 25
        if (is_string(end($content))) {
882 25
            $content[count($content) - 1] = rtrim(end($content));
883 25
        }
884
885 25
        $out = ["string", "", $content];
886
887 25
        return true;
888
    }
889
890
    /**
891
     * @param $out
892
     *
893
     * @return bool
894
     */
895 48
    protected function stringValue(&$out)
896
    {
897 48
        $s = $this->seek();
898 48
        if ($this->literal('"', false)) {
899 22
            $delim = '"';
900 48
        } elseif ($this->literal("'", false)) {
901 12
            $delim = "'";
902 12
        } else {
903 48
            return false;
904
        }
905
906 24
        $content = [];
907
908
        // look for either ending delim , escape, or string interpolation
909
        $patt = '([^\n]*?)(@\{|\\\\|' .
910 24
            Compiler::pregQuote($delim) . ')';
911
912 24
        $oldWhite = $this->eatWhiteDefault;
913 24
        $this->eatWhiteDefault = false;
914
915 24
        while ($this->match($patt, $m, false)) {
916 24
            $content[] = $m[1];
917 24
            if ($m[2] === "@{") {
918 6
                $this->count -= strlen($m[2]);
919 6
                if ($this->interpolation($inter)) {
920 6
                    $content[] = $inter;
921 6
                } else {
922 1
                    $this->count += strlen($m[2]);
923 1
                    $content[] = "@{"; // ignore it
924
                }
925 24
            } elseif ($m[2] === '\\') {
926 2
                $content[] = $m[2];
927 2
                if ($this->literal($delim, false)) {
928 2
                    $content[] = $delim;
929 2
                }
930 2
            } else {
931 24
                $this->count -= strlen($delim);
932 24
                break; // delim
933
            }
934 6
        }
935
936 24
        $this->eatWhiteDefault = $oldWhite;
937
938 24
        if ($this->literal($delim)) {
939 24
            $out = ["string", $delim, $content];
940
941 24
            return true;
942
        }
943
944 1
        $this->seek($s);
945
946 1
        return false;
947
    }
948
949
    /**
950
     * @param $out
951
     *
952
     * @return bool
953
     */
954 19
    protected function interpolation(&$out)
955
    {
956 19
        $oldWhite = $this->eatWhiteDefault;
957 19
        $this->eatWhiteDefault = true;
958
959 19
        $s = $this->seek();
960 19
        if ($this->literal("@{") &&
961 19
            $this->openString("}", $interp, null, ["'", '"', ";"]) &&
962 7
            $this->literal("}", false)
963 19
        ) {
964 7
            $out = ["interpolate", $interp];
965 7
            $this->eatWhiteDefault = $oldWhite;
966 7
            if ($this->eatWhiteDefault) {
967 1
                $this->whitespace();
968 1
            }
969
970 7
            return true;
971
        }
972
973 16
        $this->eatWhiteDefault = $oldWhite;
974 16
        $this->seek($s);
975
976 16
        return false;
977
    }
978
979
    /**
980
     * @param $unit
981
     *
982
     * @return bool
983
     */
984 49
    protected function unit(&$unit)
985
    {
986
        // speed shortcut
987 49
        if (isset($this->buffer[$this->count])) {
988 49
            $char = $this->buffer[$this->count];
989 49
            if (!ctype_digit($char) && $char !== ".") {
990 49
                return false;
991
            }
992 38
        }
993
994 49
        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
995 38
            $unit = ["number", $m[1], empty($m[2]) ? "" : $m[2]];
996
997 38
            return true;
998
        }
999
1000 49
        return false;
1001
    }
1002
1003
1004
    /**
1005
     * a # color
1006
     *
1007
     * @param $out
1008
     *
1009
     * @return bool
1010
     */
1011 48
    protected function color(&$out)
1012
    {
1013 48
        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
1014 13
            if (strlen($m[1]) > 7) {
1015 1
                $out = ["string", "", [$m[1]]];
1016 1
            } else {
1017 13
                $out = ["raw_color", $m[1]];
1018
            }
1019
1020 13
            return true;
1021
        }
1022
1023 48
        return false;
1024
    }
1025
1026
    /**
1027
     * consume an argument definition list surrounded by ()
1028
     * each argument is a variable name with optional value
1029
     * or at the end a ... or a variable named followed by ...
1030
     * arguments are separated by , unless a ; is in the list, then ; is the
1031
     * delimiter.
1032
     *
1033
     * @param $args
1034
     * @param $isVararg
1035
     *
1036
     * @return bool
1037
     * @throws \LesserPhp\Exception\GeneralException
1038
     */
1039 45
    protected function argumentDef(&$args, &$isVararg)
1040
    {
1041 45
        $s = $this->seek();
1042 45
        if (!$this->literal('(')) {
1043 45
            return false;
1044
        }
1045
1046 19
        $values = [];
1047 19
        $delim = ",";
1048 19
        $method = "expressionList";
1049
1050 19
        $isVararg = false;
1051 19
        while (true) {
1052 19
            if ($this->literal("...")) {
1053 2
                $isVararg = true;
1054 2
                break;
1055
            }
1056
1057 19
            if ($this->$method($value)) {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1058 16
                if ($value[0] === "variable") {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1059 16
                    $arg = ["arg", $value[1]];
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1060 16
                    $ss = $this->seek();
1061
1062 16
                    if ($this->assign() && $this->$method($rhs)) {
1063 9
                        $arg[] = $rhs;
0 ignored issues
show
Bug introduced by
The variable $rhs does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1064 9
                    } else {
1065 13
                        $this->seek($ss);
1066 13
                        if ($this->literal("...")) {
1067 2
                            $arg[0] = "rest";
1068 2
                            $isVararg = true;
1069 2
                        }
1070
                    }
1071
1072 16
                    $values[] = $arg;
1073 16
                    if ($isVararg) {
1074 2
                        break;
1075
                    }
1076 16
                    continue;
1077
                } else {
1078 15
                    $values[] = ["lit", $value];
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you mean $values?

This check looks for variables that are accessed but have not been defined. It raises an issue if it finds another variable that has a similar name.

The variable may have been renamed without also renaming all references.

Loading history...
1079
                }
1080 15
            }
1081
1082
1083 19
            if (!$this->literal($delim)) {
1084 19
                if ($delim === "," && $this->literal(";")) {
1085
                    // found new delim, convert existing args
1086 2
                    $delim = ";";
1087 2
                    $method = "propertyValue";
1088 2
                    $newArg = null;
1089
1090
                    // transform arg list
1091 2
                    if (isset($values[1])) { // 2 items
1092 2
                        $newList = [];
1093 2
                        foreach ($values as $i => $arg) {
1094 2
                            switch ($arg[0]) {
1095 2
                                case "arg":
1096 2
                                    if ($i) {
1097
                                        throw new GeneralException("Cannot mix ; and , as delimiter types");
1098
                                    }
1099 2
                                    $newList[] = $arg[2];
1100 2
                                    break;
1101 2
                                case "lit":
1102 2
                                    $newList[] = $arg[1];
1103 2
                                    break;
1104
                                case "rest":
1105
                                    throw new GeneralException("Unexpected rest before semicolon");
1106 2
                            }
1107 2
                        }
1108
1109 2
                        $newList = ["list", ", ", $newList];
1110
1111 2
                        switch ($values[0][0]) {
1112 2
                            case "arg":
1113 2
                                $newArg = ["arg", $values[0][1], $newList];
1114 2
                                break;
1115 1
                            case "lit":
1116 1
                                $newArg = ["lit", $newList];
1117 1
                                break;
1118 2
                        }
1119 2
                    } 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...
1120 2
                        $newArg = $values[0];
1121 2
                    }
1122
1123 2
                    if ($newArg !== null) {
1124 2
                        $values = [$newArg];
1125 2
                    }
1126 2
                } else {
1127 19
                    break;
1128
                }
1129 2
            }
1130 9
        }
1131
1132 19
        if (!$this->literal(')')) {
1133
            $this->seek($s);
1134
1135
            return false;
1136
        }
1137
1138 19
        $args = $values;
1139
1140 19
        return true;
1141
    }
1142
1143
    /**
1144
     * consume a list of tags
1145
     * this accepts a hanging delimiter
1146
     *
1147
     * @param array  $tags
1148
     * @param bool   $simple
1149
     * @param string $delim
1150
     *
1151
     * @return bool
1152
     */
1153 49
    protected function tags(&$tags, $simple = false, $delim = ',')
1154
    {
1155 49
        $tags = [];
1156 49
        while ($this->tag($tt, $simple)) {
1157 46
            $tags[] = $tt;
1158 46
            if (!$this->literal($delim)) {
1159 46
                break;
1160
            }
1161 17
        }
1162
1163 49
        return count($tags) !== 0;
1164
    }
1165
1166
    /**
1167
     * list of tags of specifying mixin path
1168
     * optionally separated by > (lazy, accepts extra >)
1169
     *
1170
     * @param array $tags
1171
     *
1172
     * @return bool
1173
     */
1174 49
    protected function mixinTags(&$tags)
1175
    {
1176 49
        $tags = [];
1177 49
        while ($this->tag($tt, true)) {
1178 22
            $tags[] = $tt;
1179 22
            $this->literal(">");
1180 22
        }
1181
1182 49
        return count($tags) !== 0;
1183
    }
1184
1185
    /**
1186
     * a bracketed value (contained within in a tag definition)
1187
     *
1188
     * @param array $parts
1189
     * @param bool $hasExpression
1190
     *
1191
     * @return bool
1192
     */
1193 49
    protected function tagBracket(&$parts, &$hasExpression)
1194
    {
1195
        // speed shortcut
1196 49
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== "[") {
1197 47
            return false;
1198
        }
1199
1200 49
        $s = $this->seek();
1201
1202 49
        $hasInterpolation = false;
1203
1204 49
        if ($this->literal("[", false)) {
1205 3
            $attrParts = ["["];
1206
            // keyword, string, operator
1207 3
            while (true) {
1208 3
                if ($this->literal("]", false)) {
1209 3
                    $this->count--;
1210 3
                    break; // get out early
1211
                }
1212
1213 3
                if ($this->match('\s+', $m)) {
1214
                    $attrParts[] = " ";
1215
                    continue;
1216
                }
1217 3
                if ($this->stringValue($str)) {
1218
                    // escape parent selector, (yuck)
1219 3
                    foreach ($str[2] as &$chunk) {
1220 3
                        $chunk = str_replace($this->lessc->getParentSelector(), '$&$', $chunk);
1221 3
                    }
1222
1223 3
                    $attrParts[] = $str;
1224 3
                    $hasInterpolation = true;
1225 3
                    continue;
1226
                }
1227
1228 3
                if ($this->keyword($word)) {
1229 3
                    $attrParts[] = $word;
1230 3
                    continue;
1231
                }
1232
1233 3
                if ($this->interpolation($inter)) {
1234 1
                    $attrParts[] = $inter;
1235 1
                    $hasInterpolation = true;
1236 1
                    continue;
1237
                }
1238
1239
                // operator, handles attr namespace too
1240 3
                if ($this->match('[|-~\$\*\^=]+', $m)) {
1241 3
                    $attrParts[] = $m[0];
1242 3
                    continue;
1243
                }
1244
1245
                break;
1246
            }
1247
1248 3
            if ($this->literal("]", false)) {
1249 3
                $attrParts[] = "]";
1250 3
                foreach ($attrParts as $part) {
1251 3
                    $parts[] = $part;
1252 3
                }
1253 3
                $hasExpression = $hasExpression || $hasInterpolation;
1254
1255 3
                return true;
1256
            }
1257
            $this->seek($s);
1258
        }
1259
1260 49
        $this->seek($s);
1261
1262 49
        return false;
1263
    }
1264
1265
    /**
1266
     * a space separated list of selectors
1267
     *
1268
     * @param      $tag
1269
     * @param bool $simple
1270
     *
1271
     * @return bool
1272
     */
1273 49
    protected function tag(&$tag, $simple = false)
1274
    {
1275 49
        if ($simple) {
1276 49
            $chars = '^@,:;{}\][>\(\) "\'';
1277 49
        } else {
1278 49
            $chars = '^@,;{}["\'';
1279
        }
1280
1281 49
        $s = $this->seek();
1282
1283 49
        $hasExpression = false;
1284 49
        $parts = [];
1285 49
        while ($this->tagBracket($parts, $hasExpression)) {
1286
            ;
1287 2
        }
1288
1289 49
        $oldWhite = $this->eatWhiteDefault;
1290 49
        $this->eatWhiteDefault = false;
1291
1292 49
        while (true) {
1293 49
            if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
1294 46
                $parts[] = $m[1];
1295 46
                if ($simple) {
1296 45
                    break;
1297
                }
1298
1299 46
                while ($this->tagBracket($parts, $hasExpression)) {
1300
                    ;
1301 2
                }
1302 46
                continue;
1303
            }
1304
1305 49
            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === "@") {
1306 13
                if ($this->interpolation($interp)) {
1307 2
                    $hasExpression = true;
1308 2
                    $interp[2] = true; // don't unescape
1309 2
                    $parts[] = $interp;
1310 2
                    continue;
1311
                }
1312
1313 12
                if ($this->literal("@")) {
1314 12
                    $parts[] = "@";
1315 12
                    continue;
1316
                }
1317
            }
1318
1319 49
            if ($this->unit($unit)) { // for keyframes
1320 9
                $parts[] = $unit[1];
1321 9
                $parts[] = $unit[2];
1322 9
                continue;
1323
            }
1324
1325 49
            break;
1326
        }
1327
1328 49
        $this->eatWhiteDefault = $oldWhite;
1329 49
        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...
1330 49
            $this->seek($s);
1331
1332 49
            return false;
1333
        }
1334
1335 46
        if ($hasExpression) {
1336 4
            $tag = ["exp", ["string", "", $parts]];
1337 4
        } else {
1338 46
            $tag = trim(implode($parts));
1339
        }
1340
1341 46
        $this->whitespace();
1342
1343 46
        return true;
1344
    }
1345
1346
    /**
1347
     * a css function
1348
     *
1349
     * @param array $func
1350
     *
1351
     * @return bool
1352
     */
1353 48
    protected function func(&$func)
1354
    {
1355 48
        $s = $this->seek();
1356
1357 48
        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
1358 25
            $fname = $m[1];
1359
1360 25
            $sPreArgs = $this->seek();
1361
1362 25
            $args = [];
1363 25
            while (true) {
1364 25
                $ss = $this->seek();
1365
                // this ugly nonsense is for ie filter properties
1366 25
                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
1367 1
                    $args[] = ["string", "", [$name, "=", $value]];
1368 1
                } else {
1369 24
                    $this->seek($ss);
1370 24
                    if ($this->expressionList($value)) {
1371 21
                        $args[] = $value;
1372 21
                    }
1373
                }
1374
1375 25
                if (!$this->literal(',')) {
1376 25
                    break;
1377
                }
1378 12
            }
1379 25
            $args = ['list', ',', $args];
1380
1381 25
            if ($this->literal(')')) {
1382 25
                $func = ['function', $fname, $args];
1383
1384 25
                return true;
1385 7
            } elseif ($fname === 'url') {
1386
                // couldn't parse and in url? treat as string
1387 6
                $this->seek($sPreArgs);
1388 6
                if ($this->openString(")", $string) && $this->literal(")")) {
1389 6
                    $func = ['function', $fname, $string];
1390
1391 6
                    return true;
1392
                }
1393
            }
1394 1
        }
1395
1396 48
        $this->seek($s);
1397
1398 48
        return false;
1399
    }
1400
1401
    /**
1402
     * consume a less variable
1403
     *
1404
     * @param $name
1405
     *
1406
     * @return bool
1407
     */
1408 49
    protected function variable(&$name)
1409
    {
1410 49
        $s = $this->seek();
1411 49
        if ($this->literal($this->lessc->getVPrefix(), false) &&
1412 32
            ($this->variable($sub) || $this->keyword($name))
1413 49
        ) {
1414 32
            if (!empty($sub)) {
1415 1
                $name = ['variable', $sub];
1416 1
            } else {
1417 32
                $name = $this->lessc->getVPrefix() . $name;
1418
            }
1419
1420 32
            return true;
1421
        }
1422
1423 49
        $name = null;
1424 49
        $this->seek($s);
1425
1426 49
        return false;
1427
    }
1428
1429
    /**
1430
     * Consume an assignment operator
1431
     * Can optionally take a name that will be set to the current property name
1432
     *
1433
     * @param string $name
1434
     *
1435
     * @return bool
1436
     */
1437 48
    protected function assign($name = null)
1438
    {
1439 48
        if ($name !== null) {
1440
            $this->currentProperty = $name;
1441
        }
1442
1443 48
        return $this->literal(':') || $this->literal('=');
1444
    }
1445
1446
    /**
1447
     * consume a keyword
1448
     *
1449
     * @param $word
1450
     *
1451
     * @return bool
1452
     */
1453 49
    protected function keyword(&$word)
1454
    {
1455 49
        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
1456 48
            $word = $m[1];
1457
1458 48
            return true;
1459
        }
1460
1461 49
        return false;
1462
    }
1463
1464
    /**
1465
     * consume an end of statement delimiter
1466
     *
1467
     * @return bool
1468
     */
1469 48
    protected function end()
1470
    {
1471 48
        if ($this->literal(';', false)) {
1472 48
            return true;
1473 12
        } elseif ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
1474
            // if there is end of file or a closing block next then we don't need a ;
1475 10
            return true;
1476
        }
1477
1478 2
        return false;
1479
    }
1480
1481
    /**
1482
     * @param $guards
1483
     *
1484
     * @return bool
1485
     */
1486 19
    protected function guards(&$guards)
1487
    {
1488 19
        $s = $this->seek();
1489
1490 19
        if (!$this->literal("when")) {
1491 19
            $this->seek($s);
1492
1493 19
            return false;
1494
        }
1495
1496 5
        $guards = [];
1497
1498 5
        while ($this->guardGroup($g)) {
1499 5
            $guards[] = $g;
1500 5
            if (!$this->literal(",")) {
1501 5
                break;
1502
            }
1503 1
        }
1504
1505 5
        if (count($guards) === 0) {
1506
            $guards = null;
1507
            $this->seek($s);
1508
1509
            return false;
1510
        }
1511
1512 5
        return true;
1513
    }
1514
1515
    /**
1516
     * a bunch of guards that are and'd together
1517
     *
1518
     * @param $guardGroup
1519
     *
1520
     * @return bool
1521
     */
1522 5
    protected function guardGroup(&$guardGroup)
1523
    {
1524 5
        $s = $this->seek();
1525 5
        $guardGroup = [];
1526 5
        while ($this->guard($guard)) {
1527 5
            $guardGroup[] = $guard;
1528 5
            if (!$this->literal("and")) {
1529 5
                break;
1530
            }
1531 1
        }
1532
1533 5
        if (count($guardGroup) === 0) {
1534
            $guardGroup = null;
1535
            $this->seek($s);
1536
1537
            return false;
1538
        }
1539
1540 5
        return true;
1541
    }
1542
1543
    /**
1544
     * @param $guard
1545
     *
1546
     * @return bool
1547
     */
1548 5
    protected function guard(&$guard)
1549
    {
1550 5
        $s = $this->seek();
1551 5
        $negate = $this->literal("not");
1552
1553 5
        if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
1554 5
            $guard = $exp;
1555 5
            if ($negate) {
1556 1
                $guard = ["negate", $guard];
1557 1
            }
1558
1559 5
            return true;
1560
        }
1561
1562
        $this->seek($s);
1563
1564
        return false;
1565
    }
1566
1567
    /* raw parsing functions */
1568
1569
    /**
1570
     * @param string $what
1571
     * @param bool $eatWhitespace
1572
     *
1573
     * @return bool
1574
     */
1575 49
    protected function literal($what, $eatWhitespace = null)
1576
    {
1577 49
        if ($eatWhitespace === null) {
1578 49
            $eatWhitespace = $this->eatWhiteDefault;
1579 49
        }
1580
1581
        // shortcut on single letter
1582 49
        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
1583 49
            if ($this->buffer[$this->count] === $what) {
1584 49
                if (!$eatWhitespace) {
1585 49
                    $this->count++;
1586
1587 49
                    return true;
1588
                }
1589
                // goes below...
1590 48
            } else {
1591 49
                return false;
1592
            }
1593 48
        }
1594
1595 49
        if (!isset(self::$literalCache[$what])) {
1596 11
            self::$literalCache[$what] = Compiler::pregQuote($what);
1597 11
        }
1598
1599 49
        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
1600
    }
1601
1602
    /**
1603
     * @param        $out
1604
     * @param string $parseItem
1605
     * @param string $delim
1606
     * @param bool   $flatten
1607
     *
1608
     * @return bool
1609
     */
1610 3
    protected function genericList(&$out, $parseItem, $delim = "", $flatten = true)
1611
    {
1612
        // $parseItem is one of mediaQuery, mediaExpression
1613 3
        $s = $this->seek();
1614 3
        $items = [];
1615 3
        while ($this->$parseItem($value)) {
0 ignored issues
show
Bug introduced by
The variable $value does not exist. Did you forget to declare it?

This check marks access to variables or properties that have not been declared yet. While PHP has no explicit notion of declaring a variable, accessing it before a value is assigned to it is most likely a bug.

Loading history...
1616 3
            $items[] = $value;
1617 3
            if ($delim) {
1618 3
                if (!$this->literal($delim)) {
1619 3
                    break;
1620
                }
1621 1
            }
1622 1
        }
1623
1624 3
        if (count($items) === 0) {
1625
            $this->seek($s);
1626
1627
            return false;
1628
        }
1629
1630 3
        if ($flatten && count($items) === 1) {
1631
            $out = $items[0];
1632
        } else {
1633 3
            $out = ["list", $delim, $items];
1634
        }
1635
1636 3
        return true;
1637
    }
1638
1639
    /**
1640
     * advance counter to next occurrence of $what
1641
     * $until - don't include $what in advance
1642
     * $allowNewline, if string, will be used as valid char set
1643
     *
1644
     * @param      $what
1645
     * @param      $out
1646
     * @param bool $until
1647
     * @param bool $allowNewline
1648
     *
1649
     * @return bool
1650
     */
1651
    protected function to($what, &$out, $until = false, $allowNewline = false)
0 ignored issues
show
Unused Code introduced by
The parameter $what is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $out is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $until is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
Unused Code introduced by
The parameter $allowNewline is not used and could be removed.

This check looks from parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
1652
    {
1653
        die('this seems not to be used, tests dont break');
0 ignored issues
show
Coding Style Compatibility introduced by
The method to() contains an exit expression.

An exit expression should only be used in rare cases. For example, if you write a short command line script.

In most cases however, using an exit expression makes the code untestable and often causes incompatibilities with other libraries. Thus, unless you are absolutely sure it is required here, we recommend to refactor your code to avoid its usage.

Loading history...
1654
        if (is_string($allowNewline)) {
0 ignored issues
show
Unused Code introduced by
if (is_string($allowNewl...wline ? '.' : '[^ ]'; } does not seem to be reachable.

This check looks for unreachable code. It uses sophisticated control flow analysis techniques to find statements which will never be executed.

Unreachable code is most often the result of return, die or exit statements that have been added for debug purposes.

function fx() {
    try {
        doSomething();
        return true;
    }
    catch (\Exception $e) {
        return false;
    }

    return false;
}

In the above example, the last return false will never be executed, because a return statement has already been met in every possible execution path.

Loading history...
1655
            $validChars = $allowNewline;
1656
        } else {
1657
            $validChars = $allowNewline ? "." : "[^\n]";
1658
        }
1659
        if (!$this->match('(' . $validChars . '*?)' . Compiler::pregQuote($what), $m, !$until)) {
1660
            return false;
1661
        }
1662
        if ($until) {
1663
            $this->count -= strlen($what);
1664
        } // give back $what
1665
        $out = $m[1];
1666
1667
        return true;
1668
    }
1669
1670
    /**
1671
     * try to match something on head of buffer
1672
     *
1673
     * @param string $regex
1674
     * @param      $out
1675
     * @param bool $eatWhitespace
1676
     *
1677
     * @return bool
1678
     */
1679 49
    protected function match($regex, &$out, $eatWhitespace = null)
1680
    {
1681 49
        if ($eatWhitespace === null) {
1682 49
            $eatWhitespace = $this->eatWhiteDefault;
1683 49
        }
1684
1685 49
        $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais';
1686 49
        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
1687 49
            $this->count += strlen($out[0]);
1688 49
            if ($eatWhitespace && $this->writeComments) {
1689 1
                $this->whitespace();
1690 1
            }
1691
1692 49
            return true;
1693
        }
1694
1695 49
        return false;
1696
    }
1697
1698
    /**
1699
     * match some whitespace
1700
     *
1701
     * @return bool
1702
     */
1703 49
    protected function whitespace()
1704
    {
1705 49
        if ($this->writeComments) {
1706 1
            $gotWhite = false;
1707 1
            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
1708 1
                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
1709 1
                    $this->append(["comment", $m[1]]);
1710 1
                    $this->seenComments[$this->count] = true;
1711 1
                }
1712 1
                $this->count += strlen($m[0]);
1713 1
                $gotWhite = true;
1714 1
            }
1715
1716 1
            return $gotWhite;
1717
        }
1718
1719 49
        $this->match("", $m);
1720 49
        return strlen($m[0]) > 0;
1721
    }
1722
1723
    /**
1724
     * match something without consuming it
1725
     *
1726
     * @param string $regex
1727
     * @param array $out
1728
     * @param int $from
1729
     *
1730
     * @return int
1731
     */
1732 24
    protected function peek($regex, &$out = null, $from = null)
1733
    {
1734 24
        if ($from === null) {
1735 20
            $from = $this->count;
1736 20
        }
1737 24
        $r = '/' . $regex . '/Ais';
1738
1739 24
        return preg_match($r, $this->buffer, $out, null, $from);
1740
    }
1741
1742
    /**
1743
     * seek to a spot in the buffer or return where we are on no argument
1744
     *
1745
     * @param int $where
1746
     *
1747
     * @return int
1748
     */
1749 49
    protected function seek($where = null)
1750
    {
1751 49
        if ($where !== null) {
1752 49
            $this->count = $where;
1753 49
        }
1754
1755 49
        return $this->count;
1756
    }
1757
1758
    /* misc functions */
1759
1760
    /**
1761
     * @param string $msg
1762
     * @param int $count
1763
     *
1764
     * @throws \LesserPhp\Exception\GeneralException
1765
     */
1766 5
    public function throwError($msg = 'parse error', $count = null)
1767
    {
1768 5
        $count = $count === null ? $this->count : $count;
1769
1770 5
        $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n");
1771
1772 5
        if (!empty($this->sourceName)) {
1773
            $loc = "$this->sourceName on line $line";
1774
        } else {
1775 5
            $loc = "line: $line";
1776
        }
1777
1778
        // TODO this depends on $this->count
1779 5
        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
1780 5
            throw new GeneralException("$msg: failed at `$m[1]` $loc");
1781
        } else {
1782
            throw new GeneralException("$msg: $loc");
1783
        }
1784
    }
1785
1786
    /**
1787
     * @param null $selectors
1788
     * @param null $type
1789
     *
1790
     * @return \stdClass
1791
     */
1792 49
    protected function pushBlock($selectors = null, $type = null)
1793
    {
1794 49
        $b = new \stdClass();
1795 49
        $b->parent = $this->env;
1796
1797 49
        $b->type = $type;
1798 49
        $b->id = self::$nextBlockId++;
1799
1800 49
        $b->isVararg = false; // TODO: kill me from here
1801 49
        $b->tags = $selectors;
1802
1803 49
        $b->props = [];
1804 49
        $b->children = [];
1805
1806
        // add a reference to the parser so
1807
        // we can access the parser to throw errors
1808
        // or retrieve the sourceName of this block.
1809 49
        $b->parser = $this;
1810
1811
        // so we know the position of this block
1812 49
        $b->count = $this->count;
1813
1814 49
        $this->env = $b;
1815
1816 49
        return $b;
1817
    }
1818
1819
    /**
1820
     * push a block that doesn't multiply tags
1821
     *
1822
     * @param $type
1823
     *
1824
     * @return \stdClass
1825
     */
1826 49
    protected function pushSpecialBlock($type)
1827
    {
1828 49
        return $this->pushBlock(null, $type);
1829
    }
1830
1831
    /**
1832
     * append a property to the current block
1833
     *
1834
     * @param      $prop
1835
     * @param  $pos
1836
     */
1837 49
    protected function append($prop, $pos = null)
1838
    {
1839 49
        if ($pos !== null) {
1840 49
            $prop[-1] = $pos;
1841 49
        }
1842 49
        $this->env->props[] = $prop;
1843 49
    }
1844
1845
    /**
1846
     * pop something off the stack
1847
     *
1848
     * @return mixed
1849
     */
1850 46
    protected function pop()
1851
    {
1852 46
        $old = $this->env;
1853 46
        $this->env = $this->env->parent;
1854
1855 46
        return $old;
1856
    }
1857
1858
    /**
1859
     * remove comments from $text
1860
     * todo: make it work for all functions, not just url
1861
     *
1862
     * @param string $text
1863
     *
1864
     * @return string
1865
     */
1866 49
    protected function removeComments($text)
1867
    {
1868
        $look = [
1869 49
            'url(',
1870 49
            '//',
1871 49
            '/*',
1872 49
            '"',
1873 49
            "'",
1874 49
        ];
1875
1876 49
        $out = '';
1877 49
        $min = null;
1878 49
        while (true) {
1879
            // find the next item
1880 49
            foreach ($look as $token) {
1881 49
                $pos = mb_strpos($text, $token);
1882 49
                if ($pos !== false) {
1883 27
                    if ($min === null || $pos < $min[1]) {
1884 27
                        $min = [$token, $pos];
1885 27
                    }
1886 27
                }
1887 49
            }
1888
1889 49
            if ($min === null) {
1890 49
                break;
1891
            }
1892
1893 27
            $count = $min[1];
1894 27
            $skip = 0;
1895 27
            $newlines = 0;
1896 27
            switch ($min[0]) {
1897 27
                case 'url(':
1898 6
                    if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) {
1899 6
                        $count += mb_strlen($m[0]) - mb_strlen($min[0]);
1900 6
                    }
1901 6
                    break;
1902 27
                case '"':
1903 27
                case "'":
1904 24
                    if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count)) {
1905 24
                        $count += mb_strlen($m[0]) - 1;
1906 24
                    }
1907 24
                    break;
1908 18
                case '//':
1909 18
                    $skip = mb_strpos($text, "\n", $count);
1910 18
                    if ($skip === false) {
1911
                        $skip = mb_strlen($text) - $count;
1912
                    } else {
1913 18
                        $skip -= $count;
1914
                    }
1915 18
                    break;
1916 4
                case '/*':
1917 4
                    if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
1918 4
                        $skip = mb_strlen($m[0]);
1919 4
                        $newlines = mb_substr_count($m[0], "\n");
1920 4
                    }
1921 4
                    break;
1922 27
            }
1923
1924 27
            if ($skip === 0) {
1925 24
                $count += mb_strlen($min[0]);
1926 24
            }
1927
1928 27
            $out .= mb_substr($text, 0, $count) . str_repeat("\n", $newlines);
1929 27
            $text = mb_substr($text, $count + $skip);
1930
1931 27
            $min = null;
1932 27
        }
1933
1934 49
        return $out . $text;
1935
    }
1936
1937
    /**
1938
     * @param bool $writeComments
1939
     */
1940 49
    public function setWriteComments($writeComments)
1941
    {
1942 49
        $this->writeComments = $writeComments;
1943 49
    }
1944
1945
    /**
1946
     * @param $s
1947
     *
1948
     * @return bool
1949
     */
1950 3
    protected function handleLiteralMedia($s)
1951
    {
1952
        // seriously, this || true is required for this statement to work!?
1953 3
        if (($this->mediaQueryList($mediaQueries) || true) && $this->literal('{')) {
1954 3
            $media = $this->pushSpecialBlock('media');
1955 3
            $media->queries = $mediaQueries === null ? [] : $mediaQueries;
1956
1957 3
            return true;
1958
        } else {
1959
            $this->seek($s);
1960
        }
1961
1962
        return false;
1963
    }
1964
1965
    /**
1966
     * @param string $directiveName
1967
     *
1968
     * @return bool
1969
     */
1970 4
    protected function handleDirectiveBlock($directiveName)
1971
    {
1972
        // seriously, this || true is required for this statement to work!?
1973 4
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1974 4
            $dir = $this->pushSpecialBlock('directive');
1975 4
            $dir->name = $directiveName;
1976 4
            if ($directiveValue !== null) {
1977 2
                $dir->value = $directiveValue;
1978 2
            }
1979
1980 4
            return true;
1981
        }
1982
1983 1
        return false;
1984
    }
1985
1986
    /**
1987
     * @param string $directiveName
1988
     *
1989
     * @return bool
1990
     */
1991 1
    protected function handleDirectiveLine($directiveName)
1992
    {
1993 1
        if ($this->propertyValue($directiveValue) && $this->end()) {
1994 1
            $this->append(['directive', $directiveName, $directiveValue]);
1995
1996 1
            return true;
1997
        }
1998
1999
        return false;
2000
    }
2001
2002
    /**
2003
     * @param string $directiveName
2004
     *
2005
     * @return bool
2006
     */
2007 24
    protected function handleRulesetDefinition($directiveName)
2008
    {
2009
        //Ruleset Definition
2010
        // seriously, this || true is required for this statement to work!?
2011 24
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
2012 1
            $dir = $this->pushBlock($this->fixTags(['@' . $directiveName]));
2013 1
            $dir->name = $directiveName;
2014 1
            if ($directiveValue !== null) {
2015
                $dir->value = $directiveValue;
2016
            }
2017
2018 1
            return true;
2019
        }
2020
2021 23
        return false;
2022
    }
2023
}
2024