Passed
Push — master ( 9a86cf...630b88 )
by Marcus
02:50
created

Parser::handleRulesetDefinition()   B

Complexity

Conditions 5
Paths 3

Size

Total Lines 16
Code Lines 8

Duplication

Lines 16
Ratio 100 %

Code Coverage

Tests 7
CRAP Score 5.0488

Importance

Changes 0
Metric Value
dl 16
loc 16
ccs 7
cts 8
cp 0.875
rs 8.8571
c 0
b 0
f 0
cc 5
eloc 8
nc 3
nop 1
crap 5.0488
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
    static protected $nextBlockId = 0; // used to uniquely identify blocks
24
25
    static protected $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
    static protected $whitePattern;
40
    static protected $commentMulti;
41
42
    static protected $commentSingle = "//";
43
    static protected $commentMultiLeft = "/*";
44
    static protected $commentMultiRight = "*/";
45
46
    // regex string to match any of the operators
47
    static protected $operatorString;
48
49
    // these properties will supress division unless it's inside parenthases
50
    static protected $supressDivisionProps =
51
        ['/border-radius$/i', '/^font$/i'];
52
53
    protected $blockDirectives = [
54
        'font-face',
55
        'keyframes',
56
        'page',
57
        '-moz-document',
58
        'viewport',
59
        '-moz-viewport',
60
        '-o-viewport',
61
        '-ms-viewport',
62
    ];
63
    protected $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
    static protected $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
    private $env;
88
    /** @var bool */
89
    private $inExp;
90
    /** @var string */
91
    private $currentProperty;
92
93
    /**
94
     * @var bool
95
     */
96
    private $writeComments = false;
97
98
    /**
99
     * Parser constructor.
100
     *
101
     * @param \LesserPhp\Compiler $lessc
102
     * @param null                $sourceName
103
     */
104 49
    public function __construct(Compiler $lessc, $sourceName = null)
105
    {
106 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...
107
        // reference to less needed for vPrefix, mPrefix, and parentSelector
108 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...
109
110 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...
111
112 49
        if (!self::$operatorString) {
113 1
            self::$operatorString =
114 1
                '(' . implode('|', array_map(['\LesserPhp\Compiler', 'pregQuote'],
115 1
                    array_keys(self::$precedence))) . ')';
116
117 1
            $commentSingle = \LesserPhp\Compiler::pregQuote(self::$commentSingle);
118 1
            $commentMultiLeft = \LesserPhp\Compiler::pregQuote(self::$commentMultiLeft);
119 1
            $commentMultiRight = \LesserPhp\Compiler::pregQuote(self::$commentMultiRight);
120
121 1
            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
122 1
            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
123
        }
124 49
    }
125
126 49
    public function parse($buffer)
127
    {
128 49
        $this->count = 0;
129 49
        $this->line = 1;
130
131 49
        $this->env = null; // block stack
132 49
        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
133 49
        $this->pushSpecialBlock('root');
134 49
        $this->eatWhiteDefault = true;
135 49
        $this->seenComments = [];
136
137 49
        $this->whitespace();
138
139
        // parse the entire file
140 49
        while (false !== $this->parseChunk()) {
141
            ;
142
        }
143
144 49
        if ($this->count !== strlen($this->buffer)) {
145
//            var_dump($this->count);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
146
//            var_dump($this->buffer);
147
            $this->throwError();
148
        }
149
150
        // TODO report where the block was opened
151 49
        if (!property_exists($this->env, 'parent') || !is_null($this->env->parent)) {
152
            throw new \Exception('parse error: unclosed block');
153
        }
154
155 49
        return $this->env;
156
    }
157
158
    /**
159
     * Parse a single chunk off the head of the buffer and append it to the
160
     * current parse environment.
161
     * Returns false when the buffer is empty, or when there is an error.
162
     *
163
     * This function is called repeatedly until the entire document is
164
     * parsed.
165
     *
166
     * This parser is most similar to a recursive descent parser. Single
167
     * functions represent discrete grammatical rules for the language, and
168
     * they are able to capture the text that represents those rules.
169
     *
170
     * Consider the function \LesserPhp\Compiler::keyword(). (all parse functions are
171
     * structured the same)
172
     *
173
     * The function takes a single reference argument. When calling the
174
     * function it will attempt to match a keyword on the head of the buffer.
175
     * If it is successful, it will place the keyword in the referenced
176
     * argument, advance the position in the buffer, and return true. If it
177
     * fails then it won't advance the buffer and it will return false.
178
     *
179
     * All of these parse functions are powered by \LesserPhp\Compiler::match(), which behaves
180
     * the same way, but takes a literal regular expression. Sometimes it is
181
     * more convenient to use match instead of creating a new function.
182
     *
183
     * Because of the format of the functions, to parse an entire string of
184
     * grammatical rules, you can chain them together using &&.
185
     *
186
     * But, if some of the rules in the chain succeed before one fails, then
187
     * the buffer position will be left at an invalid state. In order to
188
     * avoid this, \LesserPhp\Compiler::seek() is used to remember and set buffer positions.
189
     *
190
     * Before parsing a chain, use $s = $this->seek() to remember the current
191
     * position into $s. Then if a chain fails, use $this->seek($s) to
192
     * go back where we started.
193
     */
194 49
    protected function parseChunk()
195
    {
196 49
        if (empty($this->buffer)) {
197
            return false;
198
        }
199 49
        $s = $this->seek();
200
201 49
        if ($this->whitespace()) {
202 46
            return true;
203
        }
204
205
        // setting a property
206 49 View Code Duplication
        if ($this->keyword($key) && $this->assign() && $this->propertyValue($value, $key) && $this->end()) {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
207 47
            $this->append(['assign', $key, $value], $s);
208
209 47
            return true;
210
        } else {
211 49
            $this->seek($s);
212
        }
213
214
        // look for special css blocks
215 49
        if ($this->literal('@', false)) {
216 26
            $this->count--;
217
218
            // media
219 26
            if ($this->literal('@media')) {
220 3
                return $this->handleLiteralMedia($s);
221
            }
222
223 26
            if ($this->literal('@', false) && $this->keyword($directiveName)) {
224 26
                if ($this->isDirective($directiveName, $this->blockDirectives)) {
225 4
                    if ($this->handleDirectiveBlock($directiveName) === true) {
226 4
                        return true;
227
                    }
228 25
                } elseif ($this->isDirective($directiveName, $this->lineDirectives)) {
229 1
                    if ($this->handleDirectiveLine($directiveName) === true) {
230 1
                        return true;
231
                    }
232 25
                } elseif ($this->literal(':', true)) {
233 24
                    if ($this->handleRulesetDefinition($directiveName) === true) {
234 1
                        return true;
235
                    }
236
                }
237
            }
238
239 25
            $this->seek($s);
240
        }
241
242 49
        if ($this->literal('&', false)) {
243 4
            $this->count--;
244 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...
245
                // hierauf folgt was in runden klammern, und zwar das element, das erweitert werden soll
246
                // heißt also, das was in klammern steht wird um die aktuellen klassen erweitert
247
                /*
248
Aus
249
250
nav ul {
251
  &:extend(.inline);
252
  background: blue;
253
}
254
.inline {
255
  color: red;
256
}
257
258
259
Wird:
260
261
nav ul {
262
  background: blue;
263
}
264
.inline,
265
nav ul {
266
  color: red;
267
}
268
269
                 */
270
//                echo "Here we go";
271
            }
272
        }
273
274
275
        // setting a variable
276 49 View Code Duplication
        if ($this->variable($var) && $this->assign() &&
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

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

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
556 48
            return false;
557
        }
558
559 5
        $inParens = $this->inParens;
560 5
        if ($this->literal("(") &&
561 5
            ($this->inParens = true) && $this->expression($exp) &&
562 5
            $this->literal(")")
563
        ) {
564 3
            $out = $exp;
565 3
            $this->inParens = $inParens;
566
567 3
            return true;
568
        } else {
569 2
            $this->inParens = $inParens;
570 2
            $this->seek($s);
571
        }
572
573 2
        return false;
574
    }
575
576
    // a single value
577 48
    protected function value(&$value)
578
    {
579 48
        $s = $this->seek();
580
581
        // speed shortcut
582 48
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === "-") {
583
            // negation
584 7
            if ($this->literal("-", false) &&
585 7
                (($this->variable($inner) && $inner = ["variable", $inner]) ||
586 6
                    $this->unit($inner) ||
587 7
                    $this->parenValue($inner))
588
            ) {
589 6
                $value = ["unary", "-", $inner];
590
591 6
                return true;
592
            } else {
593 2
                $this->seek($s);
594
            }
595
        }
596
597 48
        if ($this->parenValue($value)) {
598 3
            return true;
599
        }
600 48
        if ($this->unit($value)) {
601 38
            return true;
602
        }
603 48
        if ($this->color($value)) {
604 13
            return true;
605
        }
606 48
        if ($this->func($value)) {
607 25
            return true;
608
        }
609 48
        if ($this->stringValue($value)) {
610 21
            return true;
611
        }
612
613 48
        if ($this->keyword($word)) {
614 35
            $value = ['keyword', $word];
615
616 35
            return true;
617
        }
618
619
        // try a variable
620 48
        if ($this->variable($var)) {
621 29
            $value = ['variable', $var];
622
623 29
            return true;
624
        }
625
626
        // unquote string (should this work on any type?
627 48
        if ($this->literal("~") && $this->stringValue($str)) {
628 4
            $value = ["escape", $str];
629
630 4
            return true;
631
        } else {
632 48
            $this->seek($s);
633
        }
634
635
        // css hack: \0
636 48
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
637 1
            $value = ['keyword', '\\' . $m[1]];
638
639 1
            return true;
640
        } else {
641 48
            $this->seek($s);
642
        }
643
644 48
        return false;
645
    }
646
647
    // an import statement
648 49
    protected function import(&$out)
649
    {
650 49
        if (!$this->literal('@import')) {
651 49
            return false;
652
        }
653
654
        // @import "something.css" media;
655
        // @import url("something.css") media;
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
656
        // @import url(something.css) media;
657
658 3
        if ($this->propertyValue($value)) {
659 3
            $out = ["import", $value];
660
661 3
            return true;
662
        }
663
664
        return null;
665
    }
666
667 3
    protected function mediaQueryList(&$out)
668
    {
669 3
        if ($this->genericList($list, "mediaQuery", ",", false)) {
670 3
            $out = $list[2];
671
672 3
            return true;
673
        }
674
675
        return false;
676
    }
677
678 3
    protected function mediaQuery(&$out)
679
    {
680 3
        $s = $this->seek();
681
682 3
        $expressions = null;
683 3
        $parts = [];
684
685 3
        if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) &&
686 3
            $this->keyword($mediaType)
687
        ) {
688 3
            $prop = ["mediaType"];
689 3
            if (isset($only)) {
690
                $prop[] = "only";
691
            }
692 3
            if (isset($not)) {
693
                $prop[] = "not";
694
            }
695 3
            $prop[] = $mediaType;
696 3
            $parts[] = $prop;
697
        } else {
698 1
            $this->seek($s);
699
        }
700
701
702 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...
703
            // ~
704
        } else {
705 1
            $this->genericList($expressions, "mediaExpression", "and", false);
706 1
            if (is_array($expressions)) {
707 1
                $parts = array_merge($parts, $expressions[2]);
708
            }
709
        }
710
711 3
        if (count($parts) === 0) {
712
            $this->seek($s);
713
714
            return false;
715
        }
716
717 3
        $out = $parts;
718
719 3
        return true;
720
    }
721
722 1
    protected function mediaExpression(&$out)
723
    {
724 1
        $s = $this->seek();
725 1
        $value = null;
726 1
        if ($this->literal("(") &&
727 1
            $this->keyword($feature) &&
728 1
            ($this->literal(":") && $this->expression($value) || true) &&
729 1
            $this->literal(")")
730
        ) {
731 1
            $out = ["mediaExp", $feature];
732 1
            if ($value) {
733 1
                $out[] = $value;
734
            }
735
736 1
            return true;
737 1
        } elseif ($this->variable($variable)) {
738 1
            $out = ['variable', $variable];
739
740 1
            return true;
741
        }
742
743
        $this->seek($s);
744
745
        return false;
746
    }
747
748
    // an unbounded string stopped by $end
749 26
    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
750
    {
751 26
        $oldWhite = $this->eatWhiteDefault;
752 26
        $this->eatWhiteDefault = false;
753
754 26
        $stop = ["'", '"', "@{", $end];
755 26
        $stop = array_map(['\LesserPhp\Compiler', "pregQuote"], $stop);
756
        // $stop[] = self::$commentMulti;
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
757
758 26
        if (!is_null($rejectStrs)) {
759 25
            $stop = array_merge($stop, $rejectStrs);
760
        }
761
762 26
        $patt = '(.*?)(' . implode("|", $stop) . ')';
763
764 26
        $nestingLevel = 0;
765
766 26
        $content = [];
767 26
        while ($this->match($patt, $m, false)) {
768 26
            if (!empty($m[1])) {
769 25
                $content[] = $m[1];
770 25
                if ($nestingOpen) {
771
                    $nestingLevel += substr_count($m[1], $nestingOpen);
772
                }
773
            }
774
775 26
            $tok = $m[2];
776
777 26
            $this->count -= strlen($tok);
778 26
            if ($tok == $end) {
779 14
                if ($nestingLevel === 0) {
780 14
                    break;
781
                } else {
782
                    $nestingLevel--;
783
                }
784
            }
785
786 24
            if (($tok === "'" || $tok === '"') && $this->stringValue($str)) {
787 13
                $content[] = $str;
788 13
                continue;
789
            }
790
791 23
            if ($tok === "@{" && $this->interpolation($inter)) {
792 3
                $content[] = $inter;
793 3
                continue;
794
            }
795
796 23
            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
797 23
                break;
798
            }
799
800
            $content[] = $tok;
801
            $this->count += strlen($tok);
802
        }
803
804 26
        $this->eatWhiteDefault = $oldWhite;
805
806 26
        if (count($content) === 0) {
807 4
            return false;
808
        }
809
810
        // trim the end
811 25
        if (is_string(end($content))) {
812 25
            $content[count($content) - 1] = rtrim(end($content));
813
        }
814
815 25
        $out = ["string", "", $content];
816
817 25
        return true;
818
    }
819
820 48
    protected function stringValue(&$out)
821
    {
822 48
        $s = $this->seek();
823 48
        if ($this->literal('"', false)) {
824 22
            $delim = '"';
825 48
        } elseif ($this->literal("'", false)) {
826 12
            $delim = "'";
827
        } else {
828 48
            return false;
829
        }
830
831 24
        $content = [];
832
833
        // look for either ending delim , escape, or string interpolation
834
        $patt = '([^\n]*?)(@\{|\\\\|' .
835 24
            \LesserPhp\Compiler::pregQuote($delim) . ')';
836
837 24
        $oldWhite = $this->eatWhiteDefault;
838 24
        $this->eatWhiteDefault = false;
839
840 24
        while ($this->match($patt, $m, false)) {
841 24
            $content[] = $m[1];
842 24
            if ($m[2] === "@{") {
843 6
                $this->count -= strlen($m[2]);
844 6
                if ($this->interpolation($inter)) {
845 6
                    $content[] = $inter;
846
                } else {
847 1
                    $this->count += strlen($m[2]);
848 6
                    $content[] = "@{"; // ignore it
849
                }
850 24
            } elseif ($m[2] === '\\') {
851 2
                $content[] = $m[2];
852 2
                if ($this->literal($delim, false)) {
853 2
                    $content[] = $delim;
854
                }
855
            } else {
856 24
                $this->count -= strlen($delim);
857 24
                break; // delim
858
            }
859
        }
860
861 24
        $this->eatWhiteDefault = $oldWhite;
862
863 24
        if ($this->literal($delim)) {
864 24
            $out = ["string", $delim, $content];
865
866 24
            return true;
867
        }
868
869 1
        $this->seek($s);
870
871 1
        return false;
872
    }
873
874 19
    protected function interpolation(&$out)
875
    {
876 19
        $oldWhite = $this->eatWhiteDefault;
877 19
        $this->eatWhiteDefault = true;
878
879 19
        $s = $this->seek();
880 19
        if ($this->literal("@{") &&
881 19
            $this->openString("}", $interp, null, ["'", '"', ";"]) &&
882 19
            $this->literal("}", false)
883
        ) {
884 7
            $out = ["interpolate", $interp];
885 7
            $this->eatWhiteDefault = $oldWhite;
886 7
            if ($this->eatWhiteDefault) {
887 1
                $this->whitespace();
888
            }
889
890 7
            return true;
891
        }
892
893 16
        $this->eatWhiteDefault = $oldWhite;
894 16
        $this->seek($s);
895
896 16
        return false;
897
    }
898
899 49
    protected function unit(&$unit)
900
    {
901
        // speed shortcut
902 49
        if (isset($this->buffer[$this->count])) {
903 49
            $char = $this->buffer[$this->count];
904 49
            if (!ctype_digit($char) && $char !== ".") {
905 49
                return false;
906
            }
907
        }
908
909 49
        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
910 38
            $unit = ["number", $m[1], empty($m[2]) ? "" : $m[2]];
911
912 38
            return true;
913
        }
914
915 49
        return false;
916
    }
917
918
    // a # color
919 48
    protected function color(&$out)
920
    {
921 48
        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
922 13
            if (strlen($m[1]) > 7) {
923 1
                $out = ["string", "", [$m[1]]];
924
            } else {
925 13
                $out = ["raw_color", $m[1]];
926
            }
927
928 13
            return true;
929
        }
930
931 48
        return false;
932
    }
933
934
    // consume an argument definition list surrounded by ()
935
    // each argument is a variable name with optional value
936
    // or at the end a ... or a variable named followed by ...
937
    // arguments are separated by , unless a ; is in the list, then ; is the
938
    // delimiter.
939 45
    protected function argumentDef(&$args, &$isVararg)
940
    {
941 45
        $s = $this->seek();
942 45
        if (!$this->literal('(')) {
943 45
            return false;
944
        }
945
946 19
        $values = [];
947 19
        $delim = ",";
948 19
        $method = "expressionList";
949
950 19
        $isVararg = false;
951 19
        while (true) {
952 19
            if ($this->literal("...")) {
953 2
                $isVararg = true;
954 2
                break;
955
            }
956
957 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...
958 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...
959 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...
960 16
                    $ss = $this->seek();
961
962 16
                    if ($this->assign() && $this->$method($rhs)) {
963 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...
964
                    } else {
965 13
                        $this->seek($ss);
966 13
                        if ($this->literal("...")) {
967 2
                            $arg[0] = "rest";
968 2
                            $isVararg = true;
969
                        }
970
                    }
971
972 16
                    $values[] = $arg;
973 16
                    if ($isVararg) {
974 2
                        break;
975
                    }
976 16
                    continue;
977
                } else {
978 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...
979
                }
980
            }
981
982
983 19
            if (!$this->literal($delim)) {
984 19
                if ($delim === "," && $this->literal(";")) {
985
                    // found new delim, convert existing args
986 2
                    $delim = ";";
987 2
                    $method = "propertyValue";
988 2
                    $newArg = null;
989
990
                    // transform arg list
991 2
                    if (isset($values[1])) { // 2 items
992 2
                        $newList = [];
993 2
                        foreach ($values as $i => $arg) {
994 2
                            switch ($arg[0]) {
995 2
                                case "arg":
996 2
                                    if ($i) {
997
                                        throw new GeneralException("Cannot mix ; and , as delimiter types");
998
                                    }
999 2
                                    $newList[] = $arg[2];
1000 2
                                    break;
1001 2
                                case "lit":
1002 2
                                    $newList[] = $arg[1];
1003 2
                                    break;
1004
                                case "rest":
1005 2
                                    throw new GeneralException("Unexpected rest before semicolon");
1006
                            }
1007
                        }
1008
1009 2
                        $newList = ["list", ", ", $newList];
1010
1011 2
                        switch ($values[0][0]) {
1012 2
                            case "arg":
1013 2
                                $newArg = ["arg", $values[0][1], $newList];
1014 2
                                break;
1015 1
                            case "lit":
1016 1
                                $newArg = ["lit", $newList];
1017 2
                                break;
1018
                        }
1019
1020 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...
1021 2
                        $newArg = $values[0];
1022
                    }
1023
1024 2
                    if ($newArg !== null) {
1025 2
                        $values = [$newArg];
1026
                    }
1027
                } else {
1028 19
                    break;
1029
                }
1030
            }
1031
        }
1032
1033 19
        if (!$this->literal(')')) {
1034
            $this->seek($s);
1035
1036
            return false;
1037
        }
1038
1039 19
        $args = $values;
1040
1041 19
        return true;
1042
    }
1043
1044
    // consume a list of tags
1045
    // this accepts a hanging delimiter
1046 49 View Code Duplication
    protected function tags(&$tags, $simple = false, $delim = ',')
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1047
    {
1048 49
        $tags = [];
1049 49
        while ($this->tag($tt, $simple)) {
1050 46
            $tags[] = $tt;
1051 46
            if (!$this->literal($delim)) {
1052 46
                break;
1053
            }
1054
        }
1055
1056 49
        return count($tags) !== 0;
1057
    }
1058
1059
    // list of tags of specifying mixin path
1060
    // optionally separated by > (lazy, accepts extra >)
1061 49 View Code Duplication
    protected function mixinTags(&$tags)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1062
    {
1063 49
        $tags = [];
1064 49
        while ($this->tag($tt, true)) {
1065 22
            $tags[] = $tt;
1066 22
            $this->literal(">");
1067
        }
1068
1069 49
        return count($tags) !== 0;
1070
    }
1071
1072
    // a bracketed value (contained within in a tag definition)
1073 49
    protected function tagBracket(&$parts, &$hasExpression)
1074
    {
1075
        // speed shortcut
1076 49 View Code Duplication
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== "[") {
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1077 47
            return false;
1078
        }
1079
1080 49
        $s = $this->seek();
1081
1082 49
        $hasInterpolation = false;
1083
1084 49
        if ($this->literal("[", false)) {
1085 3
            $attrParts = ["["];
1086
            // keyword, string, operator
1087 3
            while (true) {
1088 3
                if ($this->literal("]", false)) {
1089 3
                    $this->count--;
1090 3
                    break; // get out early
1091
                }
1092
1093 3
                if ($this->match('\s+', $m)) {
1094
                    $attrParts[] = " ";
1095
                    continue;
1096
                }
1097 3
                if ($this->stringValue($str)) {
1098
                    // escape parent selector, (yuck)
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

Sometimes obsolete code just ends up commented out instead of removed. In this case it is better to remove the code once you have checked you do not need it.

The code might also have been commented out for debugging purposes. In this case it is vital that someone uncomments it again or your project may behave in very unexpected ways in production.

This check looks for comments that seem to be mostly valid code and reports them.

Loading history...
1099 3
                    foreach ($str[2] as &$chunk) {
1100 3
                        $chunk = str_replace($this->lessc->getParentSelector(), '$&$', $chunk);
1101
                    }
1102
1103 3
                    $attrParts[] = $str;
1104 3
                    $hasInterpolation = true;
1105 3
                    continue;
1106
                }
1107
1108 3
                if ($this->keyword($word)) {
1109 3
                    $attrParts[] = $word;
1110 3
                    continue;
1111
                }
1112
1113 3
                if ($this->interpolation($inter)) {
1114 1
                    $attrParts[] = $inter;
1115 1
                    $hasInterpolation = true;
1116 1
                    continue;
1117
                }
1118
1119
                // operator, handles attr namespace too
1120 3
                if ($this->match('[|-~\$\*\^=]+', $m)) {
1121 3
                    $attrParts[] = $m[0];
1122 3
                    continue;
1123
                }
1124
1125
                break;
1126
            }
1127
1128 3
            if ($this->literal("]", false)) {
1129 3
                $attrParts[] = "]";
1130 3
                foreach ($attrParts as $part) {
1131 3
                    $parts[] = $part;
1132
                }
1133 3
                $hasExpression = $hasExpression || $hasInterpolation;
1134
1135 3
                return true;
1136
            }
1137
            $this->seek($s);
1138
        }
1139
1140 49
        $this->seek($s);
1141
1142 49
        return false;
1143
    }
1144
1145
    // a space separated list of selectors
1146 49
    protected function tag(&$tag, $simple = false)
1147
    {
1148 49
        if ($simple) {
1149 49
            $chars = '^@,:;{}\][>\(\) "\'';
1150
        } else {
1151 49
            $chars = '^@,;{}["\'';
1152
        }
1153
1154 49
        $s = $this->seek();
1155
1156 49
        $hasExpression = false;
1157 49
        $parts = [];
1158 49
        while ($this->tagBracket($parts, $hasExpression)) {
1159
            ;
1160
        }
1161
1162 49
        $oldWhite = $this->eatWhiteDefault;
1163 49
        $this->eatWhiteDefault = false;
1164
1165 49
        while (true) {
1166 49
            if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
1167 46
                $parts[] = $m[1];
1168 46
                if ($simple) {
1169 45
                    break;
1170
                }
1171
1172 46
                while ($this->tagBracket($parts, $hasExpression)) {
1173
                    ;
1174
                }
1175 46
                continue;
1176
            }
1177
1178 49
            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === "@") {
1179 13
                if ($this->interpolation($interp)) {
1180 2
                    $hasExpression = true;
1181 2
                    $interp[2] = true; // don't unescape
1182 2
                    $parts[] = $interp;
1183 2
                    continue;
1184
                }
1185
1186 12
                if ($this->literal("@")) {
1187 12
                    $parts[] = "@";
1188 12
                    continue;
1189
                }
1190
            }
1191
1192 49
            if ($this->unit($unit)) { // for keyframes
1193 9
                $parts[] = $unit[1];
1194 9
                $parts[] = $unit[2];
1195 9
                continue;
1196
            }
1197
1198 49
            break;
1199
        }
1200
1201 49
        $this->eatWhiteDefault = $oldWhite;
1202 49
        if (!$parts) {
1203 49
            $this->seek($s);
1204
1205 49
            return false;
1206
        }
1207
1208 46
        if ($hasExpression) {
1209 4
            $tag = ["exp", ["string", "", $parts]];
1210
        } else {
1211 46
            $tag = trim(implode($parts));
1212
        }
1213
1214 46
        $this->whitespace();
1215
1216 46
        return true;
1217
    }
1218
1219
    // a css function
1220 48
    protected function func(&$func)
1221
    {
1222 48
        $s = $this->seek();
1223
1224 48
        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
1225 25
            $fname = $m[1];
1226
1227 25
            $sPreArgs = $this->seek();
1228
1229 25
            $args = [];
1230 25
            while (true) {
1231 25
                $ss = $this->seek();
1232
                // this ugly nonsense is for ie filter properties
1233 25
                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
1234 1
                    $args[] = ["string", "", [$name, "=", $value]];
1235
                } else {
1236 24
                    $this->seek($ss);
1237 24
                    if ($this->expressionList($value)) {
1238 21
                        $args[] = $value;
1239
                    }
1240
                }
1241
1242 25
                if (!$this->literal(',')) {
1243 25
                    break;
1244
                }
1245
            }
1246 25
            $args = ['list', ',', $args];
1247
1248 25
            if ($this->literal(')')) {
1249 25
                $func = ['function', $fname, $args];
1250
1251 25
                return true;
1252 7
            } elseif ($fname === 'url') {
1253
                // couldn't parse and in url? treat as string
1254 6
                $this->seek($sPreArgs);
1255 6
                if ($this->openString(")", $string) && $this->literal(")")) {
1256 6
                    $func = ['function', $fname, $string];
1257
1258 6
                    return true;
1259
                }
1260
            }
1261
        }
1262
1263 48
        $this->seek($s);
1264
1265 48
        return false;
1266
    }
1267
1268
    // consume a less variable
1269 49
    protected function variable(&$name)
1270
    {
1271 49
        $s = $this->seek();
1272 49
        if ($this->literal($this->lessc->getVPrefix(), false) &&
1273 49
            ($this->variable($sub) || $this->keyword($name))
1274
        ) {
1275 32
            if (!empty($sub)) {
1276 1
                $name = ['variable', $sub];
1277
            } else {
1278 32
                $name = $this->lessc->getVPrefix() . $name;
1279
            }
1280
1281 32
            return true;
1282
        }
1283
1284 49
        $name = null;
1285 49
        $this->seek($s);
1286
1287 49
        return false;
1288
    }
1289
1290
    /**
1291
     * Consume an assignment operator
1292
     * Can optionally take a name that will be set to the current property name
1293
     *
1294
     * @param string $name
1295
     *
1296
     * @return bool
1297
     */
1298 48
    protected function assign($name = null)
1299
    {
1300 48
        if ($name !== null) {
1301
            $this->currentProperty = $name;
1302
        }
1303
1304 48
        return $this->literal(':') || $this->literal('=');
1305
    }
1306
1307
    // consume a keyword
1308 49
    protected function keyword(&$word)
1309
    {
1310 49
        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
1311 48
            $word = $m[1];
1312
1313 48
            return true;
1314
        }
1315
1316 49
        return false;
1317
    }
1318
1319
    // consume an end of statement delimiter
1320 48
    protected function end()
1321
    {
1322 48
        if ($this->literal(';', false)) {
1323 48
            return true;
1324 12
        } elseif ($this->count == strlen($this->buffer) || $this->buffer[$this->count] === '}') {
1325
            // if there is end of file or a closing block next then we don't need a ;
1326 10
            return true;
1327
        }
1328
1329 2
        return false;
1330
    }
1331
1332 19
    protected function guards(&$guards)
1333
    {
1334 19
        $s = $this->seek();
1335
1336 19
        if (!$this->literal("when")) {
1337 19
            $this->seek($s);
1338
1339 19
            return false;
1340
        }
1341
1342 5
        $guards = [];
1343
1344 5
        while ($this->guardGroup($g)) {
1345 5
            $guards[] = $g;
1346 5
            if (!$this->literal(",")) {
1347 5
                break;
1348
            }
1349
        }
1350
1351 5
        if (count($guards) === 0) {
1352
            $guards = null;
1353
            $this->seek($s);
1354
1355
            return false;
1356
        }
1357
1358 5
        return true;
1359
    }
1360
1361
    // a bunch of guards that are and'd together
1362 5
    protected function guardGroup(&$guardGroup)
1363
    {
1364 5
        $s = $this->seek();
1365 5
        $guardGroup = [];
1366 5
        while ($this->guard($guard)) {
1367 5
            $guardGroup[] = $guard;
1368 5
            if (!$this->literal("and")) {
1369 5
                break;
1370
            }
1371
        }
1372
1373 5
        if (count($guardGroup) === 0) {
1374
            $guardGroup = null;
1375
            $this->seek($s);
1376
1377
            return false;
1378
        }
1379
1380 5
        return true;
1381
    }
1382
1383 5
    protected function guard(&$guard)
1384
    {
1385 5
        $s = $this->seek();
1386 5
        $negate = $this->literal("not");
1387
1388 5
        if ($this->literal("(") && $this->expression($exp) && $this->literal(")")) {
1389 5
            $guard = $exp;
1390 5
            if ($negate) {
1391 1
                $guard = ["negate", $guard];
1392
            }
1393
1394 5
            return true;
1395
        }
1396
1397
        $this->seek($s);
1398
1399
        return false;
1400
    }
1401
1402
    /* raw parsing functions */
1403
1404 49
    protected function literal($what, $eatWhitespace = null)
1405
    {
1406 49
        if ($eatWhitespace === null) {
1407 49
            $eatWhitespace = $this->eatWhiteDefault;
1408
        }
1409
1410
        // shortcut on single letter
1411 49
        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
1412 49
            if ($this->buffer[$this->count] == $what) {
1413 49
                if (!$eatWhitespace) {
1414 49
                    $this->count++;
1415
1416 49
                    return true;
1417
                }
1418
                // goes below...
1419
            } else {
1420 49
                return false;
1421
            }
1422
        }
1423
1424 49
        if (!isset(self::$literalCache[$what])) {
1425 11
            self::$literalCache[$what] = \LesserPhp\Compiler::pregQuote($what);
1426
        }
1427
1428 49
        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
1429
    }
1430
1431 3
    protected function genericList(&$out, $parseItem, $delim = "", $flatten = true)
1432
    {
1433 3
        $s = $this->seek();
1434 3
        $items = [];
1435 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...
1436 3
            $items[] = $value;
1437 3
            if ($delim) {
1438 3
                if (!$this->literal($delim)) {
1439 3
                    break;
1440
                }
1441
            }
1442
        }
1443
1444 3
        if (count($items) === 0) {
1445
            $this->seek($s);
1446
1447
            return false;
1448
        }
1449
1450 3
        if ($flatten && count($items) === 1) {
1451
            $out = $items[0];
1452
        } else {
1453 3
            $out = ["list", $delim, $items];
1454
        }
1455
1456 3
        return true;
1457
    }
1458
1459
1460
    // advance counter to next occurrence of $what
1461
    // $until - don't include $what in advance
1462
    // $allowNewline, if string, will be used as valid char set
1463
    protected function to($what, &$out, $until = false, $allowNewline = false)
1464
    {
1465
        if (is_string($allowNewline)) {
1466
            $validChars = $allowNewline;
1467
        } else {
1468
            $validChars = $allowNewline ? "." : "[^\n]";
1469
        }
1470
        if (!$this->match('(' . $validChars . '*?)' . \LesserPhp\Compiler::pregQuote($what), $m, !$until)) {
1471
            return false;
1472
        }
1473
        if ($until) {
1474
            $this->count -= strlen($what);
1475
        } // give back $what
1476
        $out = $m[1];
1477
1478
        return true;
1479
    }
1480
1481
    // try to match something on head of buffer
1482 49
    protected function match($regex, &$out, $eatWhitespace = null)
1483
    {
1484 49
        if ($eatWhitespace === null) {
1485 49
            $eatWhitespace = $this->eatWhiteDefault;
1486
        }
1487
1488 49
        $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais';
1489 49
        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
1490 49
            $this->count += strlen($out[0]);
1491 49
            if ($eatWhitespace && $this->writeComments) {
1492 1
                $this->whitespace();
1493
            }
1494
1495 49
            return true;
1496
        }
1497
1498 49
        return false;
1499
    }
1500
1501
    // match some whitespace
1502 49
    protected function whitespace()
1503
    {
1504 49
        if ($this->writeComments) {
1505 1
            $gotWhite = false;
1506 1
            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
1507 1
                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
1508 1
                    $this->append(["comment", $m[1]]);
1509 1
                    $this->seenComments[$this->count] = true;
1510
                }
1511 1
                $this->count += strlen($m[0]);
1512 1
                $gotWhite = true;
1513
            }
1514
1515 1
            return $gotWhite;
1516
        } else {
1517 49
            $this->match("", $m);
1518
1519 49
            return strlen($m[0]) > 0;
1520
        }
1521
    }
1522
1523
    // match something without consuming it
1524 24
    protected function peek($regex, &$out = null, $from = null)
1525
    {
1526 24
        if ($from === null) {
1527 20
            $from = $this->count;
1528
        }
1529 24
        $r = '/' . $regex . '/Ais';
1530
1531 24
        return preg_match($r, $this->buffer, $out, null, $from);
1532
    }
1533
1534
    // seek to a spot in the buffer or return where we are on no argument
1535 49
    protected function seek($where = null)
1536
    {
1537 49
        if ($where === null) {
1538 49
            return $this->count;
1539
        } else {
1540 49
            $this->count = $where;
1541
        }
1542
1543 49
        return true;
1544
    }
1545
1546
    /* misc functions */
1547
1548 5
    public function throwError($msg = "parse error", $count = null)
1549
    {
1550 5
        $count = $count === null ? $this->count : $count;
1551
1552 5
        $line = $this->line +
1553 5
            substr_count(substr($this->buffer, 0, $count), "\n");
1554
1555 5
        if (!empty($this->sourceName)) {
1556
            $loc = "$this->sourceName on line $line";
1557
        } else {
1558 5
            $loc = "line: $line";
1559
        }
1560
1561
        // TODO this depends on $this->count
1562 5
        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
1563 5
            throw new GeneralException("$msg: failed at `$m[1]` $loc");
1564
        } else {
1565
            throw new GeneralException("$msg: $loc");
1566
        }
1567
    }
1568
1569 49
    protected function pushBlock($selectors = null, $type = null)
1570
    {
1571 49
        $b = new \stdClass();
1572 49
        $b->parent = $this->env;
1573
1574 49
        $b->type = $type;
1575 49
        $b->id = self::$nextBlockId++;
1576
1577 49
        $b->isVararg = false; // TODO: kill me from here
1578 49
        $b->tags = $selectors;
1579
1580 49
        $b->props = [];
1581 49
        $b->children = [];
1582
1583
        // add a reference to the parser so
1584
        // we can access the parser to throw errors
1585
        // or retrieve the sourceName of this block.
1586 49
        $b->parser = $this;
1587
1588
        // so we know the position of this block
1589 49
        $b->count = $this->count;
1590
1591 49
        $this->env = $b;
1592
1593 49
        return $b;
1594
    }
1595
1596
    // push a block that doesn't multiply tags
1597 49
    protected function pushSpecialBlock($type)
1598
    {
1599 49
        return $this->pushBlock(null, $type);
1600
    }
1601
1602
    // append a property to the current block
1603 49
    protected function append($prop, $pos = null)
1604
    {
1605 49
        if ($pos !== null) {
1606 49
            $prop[-1] = $pos;
1607
        }
1608 49
        $this->env->props[] = $prop;
1609 49
    }
1610
1611
    // pop something off the stack
1612 46
    protected function pop()
1613
    {
1614 46
        $old = $this->env;
1615 46
        $this->env = $this->env->parent;
1616
1617 46
        return $old;
1618
    }
1619
1620
    // remove comments from $text
1621
    // todo: make it work for all functions, not just url
1622 49
    protected function removeComments($text)
1623
    {
1624
        $look = [
1625 49
            'url(',
1626
            '//',
1627
            '/*',
1628
            '"',
1629
            "'",
1630
        ];
1631
1632 49
        $out = '';
1633 49
        $min = null;
1634 49
        while (true) {
1635
            // find the next item
1636 49
            foreach ($look as $token) {
1637 49
                $pos = strpos($text, $token);
1638 49
                if ($pos !== false) {
1639 27
                    if (!isset($min) || $pos < $min[1]) {
1640 49
                        $min = [$token, $pos];
1641
                    }
1642
                }
1643
            }
1644
1645 49
            if ($min === null) {
1646 49
                break;
1647
            }
1648
1649 27
            $count = $min[1];
1650 27
            $skip = 0;
1651 27
            $newlines = 0;
1652 27
            switch ($min[0]) {
1653 27 View Code Duplication
                case 'url(':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1654 6
                    if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) {
1655 6
                        $count += strlen($m[0]) - strlen($min[0]);
1656
                    }
1657 6
                    break;
1658 27
                case '"':
1659 22
                case "'":
1660 24
                    if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count)) {
1661 24
                        $count += strlen($m[0]) - 1;
1662
                    }
1663 24
                    break;
1664 18
                case '//':
1665 18
                    $skip = strpos($text, "\n", $count);
1666 18
                    if ($skip === false) {
1667
                        $skip = strlen($text) - $count;
1668
                    } else {
1669 18
                        $skip -= $count;
1670
                    }
1671 18
                    break;
1672 4 View Code Duplication
                case '/*':
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated across your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1673 4
                    if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
1674 4
                        $skip = strlen($m[0]);
1675 4
                        $newlines = substr_count($m[0], "\n");
1676
                    }
1677 4
                    break;
1678
            }
1679
1680 27
            if ($skip == 0) {
1681 24
                $count += strlen($min[0]);
1682
            }
1683
1684 27
            $out .= substr($text, 0, $count) . str_repeat("\n", $newlines);
1685 27
            $text = substr($text, $count + $skip);
1686
1687 27
            $min = null;
1688
        }
1689
1690 49
        return $out . $text;
1691
    }
1692
1693
    /**
1694
     * @param bool $writeComments
1695
     */
1696 49
    public function setWriteComments($writeComments)
1697
    {
1698 49
        $this->writeComments = $writeComments;
1699 49
    }
1700
1701
    /**
1702
     * @param $s
1703
     *
1704
     * @return bool
1705
     */
1706 3
    protected function handleLiteralMedia($s)
1707
    {
1708
        // seriously, this || true is required for this statement to work!?
1709 3
        if (($this->mediaQueryList($mediaQueries) || true) && $this->literal('{')) {
1710 3
            $media = $this->pushSpecialBlock('media');
1711 3
            $media->queries = $mediaQueries === null ? [] : $mediaQueries;
1712
1713 3
            return true;
1714
        } else {
1715
            $this->seek($s);
1716
        }
1717
1718
        return false;
1719
    }
1720
1721
    /**
1722
     * @param string $directiveName
1723
     *
1724
     * @return bool
1725
     */
1726 4 View Code Duplication
    protected function handleDirectiveBlock($directiveName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1727
    {
1728
        // seriously, this || true is required for this statement to work!?
1729 4
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1730 4
            $dir = $this->pushSpecialBlock('directive');
1731 4
            $dir->name = $directiveName;
1732 4
            if ($directiveValue !== null) {
1733 2
                $dir->value = $directiveValue;
1734
            }
1735
1736 4
            return true;
1737
        }
1738
1739 1
        return false;
1740
    }
1741
1742
    /**
1743
     * @param string $directiveName
1744
     *
1745
     * @return bool
1746
     */
1747 1
    protected function handleDirectiveLine($directiveName)
1748
    {
1749 1
        if ($this->propertyValue($directiveValue) && $this->end()) {
1750 1
            $this->append(['directive', $directiveName, $directiveValue]);
1751
1752 1
            return true;
1753
        }
1754
1755
        return false;
1756
    }
1757
1758
    /**
1759
     * @param string $directiveName
1760
     *
1761
     * @return bool
1762
     */
1763 24 View Code Duplication
    protected function handleRulesetDefinition($directiveName)
0 ignored issues
show
Duplication introduced by
This method seems to be duplicated in your project.

Duplicated code is one of the most pungent code smells. If you need to duplicate the same code in three or more different places, we strongly encourage you to look into extracting the code into a single class or operation.

You can also find more detailed suggestions in the “Code” section of your repository.

Loading history...
1764
    {
1765
        //Ruleset Definition
1766
        // seriously, this || true is required for this statement to work!?
1767 24
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1768 1
            $dir = $this->pushBlock($this->fixTags(['@' . $directiveName]));
1769 1
            $dir->name = $directiveName;
1770 1
            if ($directiveValue !== null) {
1771
                $dir->value = $directiveValue;
1772
            }
1773
1774 1
            return true;
1775
        }
1776
1777 23
        return false;
1778
    }
1779
}
1780