Completed
Push — master ( b38731...5d7e29 )
by Marcus
02:03
created

Parser::mediaExpression()   D

Complexity

Conditions 9
Paths 4

Size

Total Lines 25
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 12
CRAP Score 9.2363

Importance

Changes 0
Metric Value
dl 0
loc 25
ccs 12
cts 14
cp 0.8571
rs 4.909
c 0
b 0
f 0
cc 9
eloc 16
nc 4
nop 1
crap 9.2363
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
    /** @var mixed $env Block Stack */
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 string              $sourceName
103 49
     */
104
    public function __construct(Compiler $lessc, $sourceName = null)
105 49
    {
106
        $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 49
        // reference to less needed for vPrefix, mPrefix, and parentSelector
108
        $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 49
110
        $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 49
112 1
        if (!self::$operatorString) {
113 1
            self::$operatorString =
114
                '(' . implode('|', array_map([Compiler::class, 'pregQuote'], array_keys(self::$precedence))) . ')';
115 1
116 1
            $commentSingle = Compiler::pregQuote(self::$commentSingle);
117 1
            $commentMultiLeft = Compiler::pregQuote(self::$commentMultiLeft);
118
            $commentMultiRight = Compiler::pregQuote(self::$commentMultiRight);
119 1
120 1
            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
121
            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
122 49
        }
123
    }
124
125
    /**
126
     * @param string $buffer
127
     *
128
     * @return Block
129
     * @throws \LesserPhp\Exception\GeneralException
130 49
     */
131
    public function parse($buffer)
132 49
    {
133 49
        $this->count = 0;
134
        $this->line = 1;
135 49
136 49
        $this->clearBlockStack();
137 49
        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
138 49
        $this->pushSpecialBlock('root');
139 49
        $this->eatWhiteDefault = true;
140
        $this->seenComments = [];
141 49
142
        $this->whitespace();
143
144 49
        // parse the entire file
145
        while (false !== $this->parseChunk()) {
146
            ;
147
        }
148 49
149
        if ($this->count !== strlen($this->buffer)) {
150
            //            var_dump($this->count);
151
//            var_dump($this->buffer);
152
            $this->throwError();
153
        }
154
155 49
        // TODO report where the block was opened
156
        if (!property_exists($this->env, 'parent') || $this->env->parent !== null) {
157
            throw new GeneralException('parse error: unclosed block');
158
        }
159 49
160
        return $this->env;
161
    }
162
163
    /**
164
     * Parse a single chunk off the head of the buffer and append it to the
165
     * current parse environment.
166
     * Returns false when the buffer is empty, or when there is an error.
167
     *
168
     * This function is called repeatedly until the entire document is
169
     * parsed.
170
     *
171
     * This parser is most similar to a recursive descent parser. Single
172
     * functions represent discrete grammatical rules for the language, and
173
     * they are able to capture the text that represents those rules.
174
     *
175
     * Consider the function \LesserPhp\Compiler::keyword(). (all parse functions are
176
     * structured the same)
177
     *
178
     * The function takes a single reference argument. When calling the
179
     * function it will attempt to match a keyword on the head of the buffer.
180
     * If it is successful, it will place the keyword in the referenced
181
     * argument, advance the position in the buffer, and return true. If it
182
     * fails then it won't advance the buffer and it will return false.
183
     *
184
     * All of these parse functions are powered by \LesserPhp\Compiler::match(), which behaves
185
     * the same way, but takes a literal regular expression. Sometimes it is
186
     * more convenient to use match instead of creating a new function.
187
     *
188
     * Because of the format of the functions, to parse an entire string of
189
     * grammatical rules, you can chain them together using &&.
190
     *
191
     * But, if some of the rules in the chain succeed before one fails, then
192
     * the buffer position will be left at an invalid state. In order to
193
     * avoid this, \LesserPhp\Compiler::seek() is used to remember and set buffer positions.
194
     *
195
     * Before parsing a chain, use $s = $this->seek() to remember the current
196
     * position into $s. Then if a chain fails, use $this->seek($s) to
197
     * go back where we started.
198
     * @throws \LesserPhp\Exception\GeneralException
199 49
     */
200
    protected function parseChunk()
201 49
    {
202
        if (empty($this->buffer)) {
203
            return false;
204 49
        }
205
        $s = $this->seek();
206 49
207 46
        if ($this->whitespace()) {
208
            return true;
209
        }
210
211 49
        // setting a property
212 47
        if ($this->keyword($key) && $this->assign() && $this->propertyValue($value, $key) && $this->end()) {
213
            $this->append(['assign', $key, $value], $s);
214 47
215
            return true;
216 49
        } else {
217
            $this->seek($s);
218
        }
219
220 49
        // look for special css blocks
221 26
        if ($this->literal('@', false)) {
222
            $this->count--;
223
224 26
            // media
225 3
            if ($this->literal('@media')) {
226
                return $this->handleLiteralMedia($s);
227
            }
228 26
229 26
            if ($this->literal('@', false) && $this->keyword($directiveName)) {
230 4
                if ($this->isDirective($directiveName, $this->blockDirectives)) {
231 4
                    if ($this->handleDirectiveBlock($directiveName) === true) {
232
                        return true;
233 25
                    }
234 1
                } elseif ($this->isDirective($directiveName, $this->lineDirectives)) {
235 1
                    if ($this->handleDirectiveLine($directiveName) === true) {
236
                        return true;
237 25
                    }
238 24
                } elseif ($this->literal(':', true)) {
239 1
                    if ($this->handleRulesetDefinition($directiveName) === true) {
240
                        return true;
241
                    }
242
                }
243
            }
244 25
245
            $this->seek($s);
246
        }
247 49
248 4
        if ($this->literal('&', false)) {
249 4
            $this->count--;
250
            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...
251
                // hierauf folgt was in runden klammern, und zwar das element, das erweitert werden soll
252
                // heißt also, das was in klammern steht wird um die aktuellen klassen erweitert
253
                /*
254
Aus
255
256
nav ul {
257
  &:extend(.inline);
258
  background: blue;
259
}
260
.inline {
261
  color: red;
262
}
263
264
265
Wird:
266
267
nav ul {
268
  background: blue;
269
}
270
.inline,
271
nav ul {
272
  color: red;
273
}
274
275
                 */
276
//                echo "Here we go";
277
            }
278
        }
279
280
281 49
        // setting a variable
282 49
        if ($this->variable($var) && $this->assign() &&
283
            $this->propertyValue($value) && $this->end()
284 23
        ) {
285
            $this->append(['assign', $var, $value], $s);
286 23
287
            return true;
288 49
        } else {
289
            $this->seek($s);
290
        }
291 49
292 3
        if ($this->import($importValue)) {
293
            $this->append($importValue, $s);
294 3
295
            return true;
296
        }
297
298 49
        // opening parametric mixin
299 49
        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
300 49
            ($this->guards($guards) || true) &&
301
            $this->literal('{')
302 18
        ) {
303 18
            $block = $this->pushBlock($this->fixTags([$tag]));
304 18
            $block->args = $args;
0 ignored issues
show
Bug introduced by
The property args does not seem to exist in LesserPhp\Block.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
305 18
            $block->isVararg = $isVararg;
306 5
            if (!empty($guards)) {
307
                $block->guards = $guards;
0 ignored issues
show
Bug introduced by
The property guards does not seem to exist in LesserPhp\Block.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
308
            }
309 18
310
            return true;
311 49
        } else {
312
            $this->seek($s);
313
        }
314
315 49
        // opening a simple block
316 46
        if ($this->tags($tags) && $this->literal('{', false)) {
317 46
            $tags = $this->fixTags($tags);
318
            $this->pushBlock($tags);
319 46
320
            return true;
321 49
        } else {
322
            $this->seek($s);
323
        }
324
325 49
        // closing a block
326
        if ($this->literal('}', false)) {
327 46
            try {
328
                $block = $this->pop();
329
            } catch (\Exception $e) {
330
                $this->seek($s);
331
                $this->throwError($e->getMessage());
332
333 46
                return false; // will never be reached, but silences the ide for now
334 46
            }
335 46
336 46
            $hidden = false;
337 46
            if ($block->type === null) {
338 46
                $hidden = true;
339 46
                if (!isset($block->args)) {
340 46
                    foreach ($block->tags as $tag) {
341
                        if (!is_string($tag) || $tag[0] !== $this->lessc->getMPrefix()) {
342
                            $hidden = false;
343
                            break;
344
                        }
345 46
                    }
346 46
                }
347 46
348
                foreach ($block->tags as $tag) {
349
                    if (is_string($tag)) {
350
                        $this->env->children[$tag][] = $block;
351
                    }
352 46
                }
353 46
            }
354
355
            if (!$hidden) {
356
                $this->append(['block', $block], $s);
357
            }
358 46
359
            // this is done here so comments aren't bundled into he block that
360 46
            // was just closed
361
            $this->whitespace();
362
363
            return true;
364 49
        }
365 49
366 49
        // mixin
367
        if ($this->mixinTags($tags) &&
368 22
            ($this->argumentDef($argv, $isVararg) || true) &&
369 22
            ($this->keyword($suffix) || true) && $this->end()
370
        ) {
371 22
            $tags = $this->fixTags($tags);
372
            $this->append(['mixin', $tags, $argv, $suffix], $s);
373 49
374
            return true;
375
        } else {
376
            $this->seek($s);
377 49
        }
378 4
379
        // spare ;
380
        if ($this->literal(';')) {
381 49
            return true;
382
        }
383
384
        return false; // got nothing, throw error
385
    }
386
387
    /**
388
     * @param string $directiveName
389
     * @param array  $directives
390 26
     *
391
     * @return bool
392
     */
393 26
    protected function isDirective($directiveName, array $directives)
394 26
    {
395
        // TODO: cache pattern in parser
396 26
        $pattern = implode('|', array_map([Compiler::class, 'pregQuote'], $directives));
397
        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
398
399
        return (preg_match($pattern, $directiveName) === 1);
400
    }
401
402
    /**
403
     * @param array $tags
404 46
     *
405
     * @return array
406
     */
407 46
    protected function fixTags(array $tags)
408 46
    {
409 46
        // move @ tags out of variable namespace
410
        foreach ($tags as &$tag) {
411
            if ($tag[0] === $this->lessc->getVPrefix()) {
412
                $tag[0] = $this->lessc->getMPrefix();
413 46
            }
414
        }
415
416
        return $tags;
417
    }
418
419
    /**
420
     * a list of expressions
421
     *
422
     * @param $exps
423 48
     *
424
     * @return bool
425 48
     */
426
    protected function expressionList(&$exps)
427 48
    {
428 48
        $values = [];
429
430
        while ($this->expression($exp)) {
431 48
            $values[] = $exp;
432 26
        }
433
434
        if (count($values) === 0) {
435 48
            return false;
436
        }
437 48
438
        $exps = Compiler::compressList($values, ' ');
439
440
        return true;
441
    }
442
443
    /**
444
     * Attempt to consume an expression.
445
     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
446
     *
447
     * @param $out
448 48
     *
449
     * @return bool
450 48
     */
451 48
    protected function expression(&$out)
452
    {
453
        if ($this->value($lhs)) {
454 48
            $out = $this->expHelper($lhs, 0);
455 2
456 2
            // look for / shorthand
457 2
            if (!empty($this->env->supressedDivision)) {
458
                unset($this->env->supressedDivision);
459 2
                $s = $this->seek();
460 2
                if ($this->literal('/') && $this->value($rhs)) {
461 2
                    $out = [
462
                        'list',
463
                        '',
464
                        [$out, ['keyword', '/'], $rhs],
465
                    ];
466
                } else {
467
                    $this->seek($s);
468 48
                }
469
            }
470
471 48
            return true;
472
        }
473
474
        return false;
475
    }
476
477
    /**
478
     * recursively parse infix equation with $lhs at precedence $minP
479
     *
480
     * @param     $lhs
481
     * @param int $minP
482 48
     *
483
     * @return array
484 48
     */
485 48
    protected function expHelper($lhs, $minP)
486
    {
487 48
        $this->inExp = true;
488 48
        $ss = $this->seek();
489
490
        while (true) {
491
            $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
492 48
493
            // If there is whitespace before the operator, then we require
494 48
            // whitespace after the operator for it to be an expression
495 48
            $needWhite = $whiteBefore && !$this->inParens;
496
497 20
            if ($this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) &&
498 20
                self::$precedence[$m[1]] >= $minP
499 20
            ) {
500 20
                if (!$this->inParens &&
501
                    isset($this->env->currentProperty) &&
502 5
                    $m[1] === '/' &&
503 5
                    empty($this->env->supressedDivision)
504 2
                ) {
505 5
                    foreach (self::$supressDivisionProps as $pattern) {
506
                        if (preg_match($pattern, $this->env->currentProperty)) {
507
                            $this->env->supressedDivision = true;
508
                            break 2;
509
                        }
510 20
                    }
511
                }
512 20
513
                $whiteAfter = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
514
515
                if (!$this->value($rhs)) {
516
                    break;
517 20
                }
518 20
519
                // peek for next operator to see what to do with rhs
520 1
                if ($this->peek(self::$operatorString, $next) &&
521
                    self::$precedence[$next[1]] > self::$precedence[$m[1]]
522
                ) {
523 20
                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
524 20
                }
525
526 20
                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
527
                $ss = $this->seek();
528
529 48
                continue;
530
            }
531
532 48
            break;
533
        }
534 48
535
        $this->seek($ss);
536
537
        return $lhs;
538
    }
539
540
    /**
541
     * consume a list of values for a property
542
     *
543
     * @param        $value
544
     * @param string $keyName
545 48
     *
546
     * @return bool
547 48
     */
548
    public function propertyValue(&$value, $keyName = null)
549 48
    {
550 47
        $values = [];
551
552
        if ($keyName !== null) {
553 48
            $this->env->currentProperty = $keyName;
554 48
        }
555 48
556 48
        $s = null;
557 48
        while ($this->expressionList($v)) {
558 48
            $values[] = $v;
559
            $s = $this->seek();
560
            if (!$this->literal(',')) {
561
                break;
562 48
            }
563 48
        }
564
565
        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...
566 48
            $this->seek($s);
567 47
        }
568
569
        if ($keyName !== null) {
570 48
            unset($this->env->currentProperty);
571 3
        }
572
573
        if (count($values) === 0) {
574 48
            return false;
575
        }
576 48
577
        $value = Compiler::compressList($values, ', ');
578
579
        return true;
580
    }
581
582
    /**
583
     * @param $out
584 48
     *
585
     * @return bool
586 48
     */
587
    protected function parenValue(&$out)
588
    {
589 48
        $s = $this->seek();
590 48
591
        // speed shortcut
592
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== '(') {
593 5
            return false;
594 5
        }
595 5
596 5
        $inParens = $this->inParens;
597
        if ($this->literal('(') &&
598 3
            ($this->inParens = true) && $this->expression($exp) &&
599 3
            $this->literal(')')
600
        ) {
601 3
            $out = $exp;
602
            $this->inParens = $inParens;
603 2
604 2
            return true;
605
        } else {
606
            $this->inParens = $inParens;
607 2
            $this->seek($s);
608
        }
609
610
        return false;
611
    }
612
613
    /**
614
     * a single value
615
     *
616
     * @param array $value
617 48
     *
618
     * @return bool
619 48
     */
620
    protected function value(&$value)
621
    {
622 48
        $s = $this->seek();
623
624 7
        // speed shortcut
625 7
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '-') {
626 6
            // negation
627 7
            if ($this->literal('-', false) &&
628
                (($this->variable($inner) && $inner = ['variable', $inner]) ||
629 6
                    $this->unit($inner) ||
630
                    $this->parenValue($inner))
631 6
            ) {
632
                $value = ['unary', '-', $inner];
633 2
634
                return true;
635
            } else {
636
                $this->seek($s);
637 48
            }
638 3
        }
639
640 48
        if ($this->parenValue($value)) {
641 38
            return true;
642
        }
643 48
        if ($this->unit($value)) {
644 13
            return true;
645
        }
646 48
        if ($this->color($value)) {
647 25
            return true;
648
        }
649 48
        if ($this->func($value)) {
650 21
            return true;
651
        }
652
        if ($this->stringValue($value)) {
653 48
            return true;
654 35
        }
655
656 35
        if ($this->keyword($word)) {
657
            $value = ['keyword', $word];
658
659
            return true;
660 48
        }
661 29
662
        // try a variable
663 29
        if ($this->variable($var)) {
664
            $value = ['variable', $var];
665
666
            return true;
667 48
        }
668 4
669
        // unquote string (should this work on any type?
670 4
        if ($this->literal('~') && $this->stringValue($str)) {
671
            $value = ['escape', $str];
672 48
673
            return true;
674
        } else {
675
            $this->seek($s);
676 48
        }
677 1
678
        // css hack: \0
679 1
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
680
            $value = ['keyword', '\\' . $m[1]];
681 48
682
            return true;
683
        } else {
684 48
            $this->seek($s);
685
        }
686
687
        return false;
688
    }
689
690
    /**
691
     * an import statement
692
     *
693
     * @param array $out
694 49
     *
695
     * @return bool|null
696 49
     */
697 49
    protected function import(&$out)
698
    {
699
        if (!$this->literal('@import')) {
700
            return false;
701
        }
702
703
        // @import "something.css" media;
704 3
        // @import url("something.css") media;
705 3
        // @import url(something.css) media;
706
707 3
        if ($this->propertyValue($value)) {
708
            $out = ['import', $value];
709
710
            return true;
711
        }
712
713
        return false;
714
    }
715
716
    /**
717
     * @param $out
718 3
     *
719
     * @return bool
720 3
     */
721 3
    protected function mediaQueryList(&$out)
722
    {
723 3
        if ($this->genericList($list, 'mediaQuery', ',', false)) {
724
            $out = $list[2];
725
726
            return true;
727
        }
728
729
        return false;
730
    }
731
732
    /**
733
     * @param $out
734 3
     *
735
     * @return bool
736 3
     */
737
    protected function mediaQuery(&$out)
738 3
    {
739 3
        $s = $this->seek();
740
741 3
        $expressions = null;
742 3
        $parts = [];
743
744 3
        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
745 3
            $this->keyword($mediaType)
746
        ) {
747
            $prop = ['mediaType'];
748 3
            if (isset($only)) {
749
                $prop[] = 'only';
750
            }
751 3
            if (isset($not)) {
752 3
                $prop[] = 'not';
753
            }
754 1
            $prop[] = $mediaType;
755
            $parts[] = $prop;
756
        } else {
757
            $this->seek($s);
758 3
        }
759
760
761 1
        if (!empty($mediaType) && !$this->literal('and')) {
0 ignored issues
show
Unused Code introduced by
This if statement is empty and can be removed.

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

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

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

could be turned into

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

This is much more concise to read.

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

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1912
1913 18
            return true;
1914
        } else {
1915 18
            $this->seek($s);
1916 4
        }
1917 4
1918 4
        return false;
1919 4
    }
1920
1921 4
    /**
1922
     * @param string $directiveName
1923
     *
1924 27
     * @return bool
1925 24
     */
1926
    protected function handleDirectiveBlock($directiveName)
1927
    {
1928 27
        // seriously, this || true is required for this statement to work!?
1929 27
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1930
            $dir = $this->pushSpecialBlock('directive');
1931 27
            $dir->name = $directiveName;
0 ignored issues
show
Bug introduced by
The property name does not seem to exist in LesserPhp\Block.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1932
            if ($directiveValue !== null) {
1933
                $dir->value = $directiveValue;
0 ignored issues
show
Bug introduced by
The property value does not seem to exist in LesserPhp\Block.

An attempt at access to an undefined property has been detected. This may either be a typographical error or the property has been renamed but there are still references to its old name.

If you really want to allow access to undefined properties, you can define magic methods to allow access. See the php core documentation on Overloading.

Loading history...
1934 49
            }
1935
1936
            return true;
1937
        }
1938
1939
        return false;
1940 49
    }
1941
1942 49
    /**
1943 49
     * @param string $directiveName
1944
     *
1945
     * @return bool
1946
     */
1947
    protected function handleDirectiveLine($directiveName)
1948
    {
1949
        if ($this->propertyValue($directiveValue) && $this->end()) {
1950 3
            $this->append(['directive', $directiveName, $directiveValue]);
1951
1952
            return true;
1953 3
        }
1954 3
1955 3
        return false;
1956
    }
1957 3
1958
    /**
1959
     * @param string $directiveName
1960
     *
1961
     * @return bool
1962
     */
1963
    protected function handleRulesetDefinition($directiveName)
1964
    {
1965
        //Ruleset Definition
1966
        $this->openString('{', $directiveValue, null, [';']);
1967
1968
        if ($this->literal('{')) {
1969
            $dir = $this->pushBlock($this->fixTags(['@' . $directiveName]), 'ruleset');
1970 4
            if (!$dir instanceof Block\Ruleset) {
1971
                throw new \RuntimeException('Block factory did not produce a Ruleset');
1972
            }
1973 4
1974 4
            $dir->name = $directiveName;
1975 4
            if ($directiveValue !== null) {
1976 4
                $dir->value = $directiveValue;
1977 2
            }
1978
1979
            return true;
1980 4
        }
1981
1982
        return false;
1983 1
    }
1984
1985
    private function clearBlockStack()
1986
    {
1987
        $this->env = null;
1988
    }
1989
}
1990