Completed
Pull Request — master (#8)
by Marcus
02:04
created

Parser::clearBlockStack()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 2

Importance

Changes 0
Metric Value
dl 0
loc 4
ccs 0
cts 1
cp 0
rs 10
c 0
b 0
f 0
cc 1
eloc 2
nc 1
nop 0
crap 2
1
<?php
2
3
namespace LesserPhp;
4
5
use LesserPhp\Exception\GeneralException;
6
7
/**
8
 * lesserphp
9
 * https://www.maswaba.de/lesserphp
10
 *
11
 * LESS CSS compiler, adapted from http://lesscss.org
12
 *
13
 * Copyright 2013, Leaf Corcoran <[email protected]>
14
 * Copyright 2016, Marcus Schwarz <[email protected]>
15
 * Licensed under MIT or GPLv3, see LICENSE
16
 * @package LesserPhp
17
 * // responsible for taking a string of LESS code and converting it into a
18
 * // syntax tree
19
 */
20
class Parser
21
{
22
    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 null                $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 $buffer
127
     *
128
     * @return mixed
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;
305 18
            $block->isVararg = $isVararg;
306 5
            if (!empty($guards)) {
307
                $block->guards = $guards;
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
                return false; // will never be reached, but silences the ide for now
333 46
            }
334 46
335 46
            $hidden = false;
336 46
            if ($block->type === null) {
337 46
                $hidden = true;
338 46
                if (!isset($block->args)) {
339 46
                    foreach ($block->tags as $tag) {
340 46
                        if (!is_string($tag) || $tag[0] !== $this->lessc->getMPrefix()) {
341
                            $hidden = false;
342
                            break;
343
                        }
344
                    }
345 46
                }
346 46
347 46
                foreach ($block->tags as $tag) {
348
                    if (is_string($tag)) {
349
                        $this->env->children[$tag][] = $block;
350
                    }
351
                }
352 46
            }
353 46
354
            if (!$hidden) {
355
                $this->append(['block', $block], $s);
356
            }
357
358 46
            // this is done here so comments aren't bundled into he block that
359
            // was just closed
360 46
            $this->whitespace();
361
362
            return true;
363
        }
364 49
365 49
        // mixin
366 49
        if ($this->mixinTags($tags) &&
367
            ($this->argumentDef($argv, $isVararg) || true) &&
368 22
            ($this->keyword($suffix) || true) && $this->end()
369 22
        ) {
370
            $tags = $this->fixTags($tags);
371 22
            $this->append(['mixin', $tags, $argv, $suffix], $s);
372
373 49
            return true;
374
        } else {
375
            $this->seek($s);
376
        }
377 49
378 4
        // spare ;
379
        if ($this->literal(';')) {
380
            return true;
381 49
        }
382
383
        return false; // got nothing, throw error
384
    }
385
386
    /**
387
     * @param string $directiveName
388
     * @param array $directives
389
     *
390 26
     * @return bool
391
     */
392
    protected function isDirective($directiveName, array $directives)
393 26
    {
394 26
        // TODO: cache pattern in parser
395
        $pattern = implode('|', array_map([Compiler::class, 'pregQuote'], $directives));
396 26
        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
397
398
        return (preg_match($pattern, $directiveName) === 1);
399
    }
400
401
    /**
402
     * @param array $tags
403
     *
404 46
     * @return mixed
405
     */
406
    protected function fixTags(array $tags)
407 46
    {
408 46
        // move @ tags out of variable namespace
409 46
        foreach ($tags as &$tag) {
410
            if ($tag[0] === $this->lessc->getVPrefix()) {
411
                $tag[0] = $this->lessc->getMPrefix();
412
            }
413 46
        }
414
415
        return $tags;
416
    }
417
418
    /**
419
     * a list of expressions
420
     *
421
     * @param $exps
422
     *
423 48
     * @return bool
424
     */
425 48
    protected function expressionList(&$exps)
426
    {
427 48
        $values = [];
428 48
429
        while ($this->expression($exp)) {
430
            $values[] = $exp;
431 48
        }
432 26
433
        if (count($values) === 0) {
434
            return false;
435 48
        }
436
437 48
        $exps = Compiler::compressList($values, ' ');
438
439
        return true;
440
    }
441
442
    /**
443
     * Attempt to consume an expression.
444
     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
445
     *
446
     * @param $out
447
     *
448 48
     * @return bool
449
     */
450 48
    protected function expression(&$out)
451 48
    {
452
        if ($this->value($lhs)) {
453
            $out = $this->expHelper($lhs, 0);
454 48
455 2
            // look for / shorthand
456 2
            if (!empty($this->env->supressedDivision)) {
457 2
                unset($this->env->supressedDivision);
458
                $s = $this->seek();
459 2
                if ($this->literal('/') && $this->value($rhs)) {
460 2
                    $out = [
461 2
                        'list',
462
                        '',
463
                        [$out, ['keyword', '/'], $rhs],
464
                    ];
465
                } else {
466
                    $this->seek($s);
467
                }
468 48
            }
469
470
            return true;
471 48
        }
472
473
        return false;
474
    }
475
476
    /**
477
     * recursively parse infix equation with $lhs at precedence $minP
478
     *
479
     * @param $lhs
480
     * @param $minP
481
     *
482 48
     * @return array
483
     */
484 48
    protected function expHelper($lhs, $minP)
485 48
    {
486
        $this->inExp = true;
487 48
        $ss = $this->seek();
488 48
489
        while (true) {
490
            $whiteBefore = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
491
492 48
            // If there is whitespace before the operator, then we require
493
            // whitespace after the operator for it to be an expression
494 48
            $needWhite = $whiteBefore && !$this->inParens;
495 48
496
            if ($this->match(self::$operatorString . ($needWhite ? '\s' : ''), $m) &&
497 20
                self::$precedence[$m[1]] >= $minP
498 20
            ) {
499 20
                if (!$this->inParens &&
500 20
                    isset($this->env->currentProperty) &&
501
                    $m[1] === '/' &&
502 5
                    empty($this->env->supressedDivision)
503 5
                ) {
504 2
                    foreach (self::$supressDivisionProps as $pattern) {
505 5
                        if (preg_match($pattern, $this->env->currentProperty)) {
506
                            $this->env->supressedDivision = true;
507
                            break 2;
508
                        }
509
                    }
510 20
                }
511
512 20
                $whiteAfter = isset($this->buffer[$this->count - 1]) && ctype_space($this->buffer[$this->count - 1]);
513
514
                if (!$this->value($rhs)) {
515
                    break;
516
                }
517 20
518 20
                // peek for next operator to see what to do with rhs
519
                if ($this->peek(self::$operatorString, $next) &&
520 1
                    self::$precedence[$next[1]] > self::$precedence[$m[1]]
521
                ) {
522
                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
523 20
                }
524 20
525
                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
526 20
                $ss = $this->seek();
527
528
                continue;
529 48
            }
530
531
            break;
532 48
        }
533
534 48
        $this->seek($ss);
535
536
        return $lhs;
537
    }
538
539
    /**
540
     * consume a list of values for a property
541
     *
542
     * @param      $value
543
     * @param string $keyName
544
     *
545 48
     * @return bool
546
     */
547 48
    public function propertyValue(&$value, $keyName = null)
548
    {
549 48
        $values = [];
550 47
551
        if ($keyName !== null) {
552
            $this->env->currentProperty = $keyName;
553 48
        }
554 48
555 48
        $s = null;
556 48
        while ($this->expressionList($v)) {
557 48
            $values[] = $v;
558 48
            $s = $this->seek();
559
            if (!$this->literal(',')) {
560
                break;
561
            }
562 48
        }
563 48
564
        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...
565
            $this->seek($s);
566 48
        }
567 47
568
        if ($keyName !== null) {
569
            unset($this->env->currentProperty);
570 48
        }
571 3
572
        if (count($values) === 0) {
573
            return false;
574 48
        }
575
576 48
        $value = Compiler::compressList($values, ', ');
577
578
        return true;
579
    }
580
581
    /**
582
     * @param $out
583
     *
584 48
     * @return bool
585
     */
586 48
    protected function parenValue(&$out)
587
    {
588
        $s = $this->seek();
589 48
590 48
        // speed shortcut
591
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== '(') {
592
            return false;
593 5
        }
594 5
595 5
        $inParens = $this->inParens;
596 5
        if ($this->literal('(') &&
597
            ($this->inParens = true) && $this->expression($exp) &&
598 3
            $this->literal(')')
599 3
        ) {
600
            $out = $exp;
601 3
            $this->inParens = $inParens;
602
603 2
            return true;
604 2
        } else {
605
            $this->inParens = $inParens;
606
            $this->seek($s);
607 2
        }
608
609
        return false;
610
    }
611
612
    /**
613
     * a single value
614
     *
615
     * @param array $value
616
     *
617 48
     * @return bool
618
     */
619 48
    protected function value(&$value)
620
    {
621
        $s = $this->seek();
622 48
623
        // speed shortcut
624 7
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '-') {
625 7
            // negation
626 6
            if ($this->literal('-', false) &&
627 7
                (($this->variable($inner) && $inner = ['variable', $inner]) ||
628
                    $this->unit($inner) ||
629 6
                    $this->parenValue($inner))
630
            ) {
631 6
                $value = ['unary', '-', $inner];
632
633 2
                return true;
634
            } else {
635
                $this->seek($s);
636
            }
637 48
        }
638 3
639
        if ($this->parenValue($value)) {
640 48
            return true;
641 38
        }
642
        if ($this->unit($value)) {
643 48
            return true;
644 13
        }
645
        if ($this->color($value)) {
646 48
            return true;
647 25
        }
648
        if ($this->func($value)) {
649 48
            return true;
650 21
        }
651
        if ($this->stringValue($value)) {
652
            return true;
653 48
        }
654 35
655
        if ($this->keyword($word)) {
656 35
            $value = ['keyword', $word];
657
658
            return true;
659
        }
660 48
661 29
        // try a variable
662
        if ($this->variable($var)) {
663 29
            $value = ['variable', $var];
664
665
            return true;
666
        }
667 48
668 4
        // unquote string (should this work on any type?
669
        if ($this->literal('~') && $this->stringValue($str)) {
670 4
            $value = ['escape', $str];
671
672 48
            return true;
673
        } else {
674
            $this->seek($s);
675
        }
676 48
677 1
        // css hack: \0
678
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
679 1
            $value = ['keyword', '\\' . $m[1]];
680
681 48
            return true;
682
        } else {
683
            $this->seek($s);
684 48
        }
685
686
        return false;
687
    }
688
689
    /**
690
     * an import statement
691
     *
692
     * @param array $out
693
     *
694 49
     * @return bool|null
695
     */
696 49
    protected function import(&$out)
697 49
    {
698
        if (!$this->literal('@import')) {
699
            return false;
700
        }
701
702
        // @import "something.css" media;
703
        // @import url("something.css") media;
704 3
        // @import url(something.css) media;
705 3
706
        if ($this->propertyValue($value)) {
707 3
            $out = ['import', $value];
708
709
            return true;
710
        }
711
712
        return false;
713
    }
714
715
    /**
716
     * @param $out
717
     *
718 3
     * @return bool
719
     */
720 3
    protected function mediaQueryList(&$out)
721 3
    {
722
        if ($this->genericList($list, 'mediaQuery', ',', false)) {
723 3
            $out = $list[2];
724
725
            return true;
726
        }
727
728
        return false;
729
    }
730
731
    /**
732
     * @param $out
733
     *
734 3
     * @return bool
735
     */
736 3
    protected function mediaQuery(&$out)
737
    {
738 3
        $s = $this->seek();
739 3
740
        $expressions = null;
741 3
        $parts = [];
742 3
743
        if (($this->literal('only') && ($only = true) || $this->literal('not') && ($not = true) || true) &&
744 3
            $this->keyword($mediaType)
745 3
        ) {
746
            $prop = ['mediaType'];
747
            if (isset($only)) {
748 3
                $prop[] = 'only';
749
            }
750
            if (isset($not)) {
751 3
                $prop[] = 'not';
752 3
            }
753
            $prop[] = $mediaType;
754 1
            $parts[] = $prop;
755
        } else {
756
            $this->seek($s);
757
        }
758 3
759
760
        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...
761 1
            // ~
762 1
        } else {
763 1
            $this->genericList($expressions, 'mediaExpression', 'and', false);
764
            if (is_array($expressions)) {
765
                $parts = array_merge($parts, $expressions[2]);
766
            }
767 3
        }
768
769
        if (count($parts) === 0) {
770
            $this->seek($s);
771
772
            return false;
773 3
        }
774
775 3
        $out = $parts;
776
777
        return true;
778
    }
779
780
    /**
781
     * @param $out
782
     *
783 1
     * @return bool
784
     */
785 1
    protected function mediaExpression(&$out)
786 1
    {
787 1
        $s = $this->seek();
788 1
        $value = null;
789 1
        if ($this->literal('(') &&
790 1
            $this->keyword($feature) &&
791
            ($this->literal(':') && $this->expression($value) || true) &&
792 1
            $this->literal(')')
793 1
        ) {
794 1
            $out = ['mediaExp', $feature];
795
            if ($value) {
796
                $out[] = $value;
797 1
            }
798 1
799 1
            return true;
800
        } elseif ($this->variable($variable)) {
801 1
            $out = ['variable', $variable];
802
803
            return true;
804
        }
805
806
        $this->seek($s);
807
808
        return false;
809
    }
810
811
    /**
812
     * an unbounded string stopped by $end
813
     *
814
     * @param      $end
815
     * @param      $out
816
     * @param null $nestingOpen
817
     * @param null $rejectStrs
818
     *
819 26
     * @return bool
820
     */
821 26
    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
822 26
    {
823
        $oldWhite = $this->eatWhiteDefault;
824 26
        $this->eatWhiteDefault = false;
825 26
826
        $stop = ["'", '"', '@{', $end];
827
        $stop = array_map([Compiler::class, 'pregQuote'], $stop);
828 26
        // $stop[] = self::$commentMulti;
829 25
830
        if ($rejectStrs !== null) {
831
            $stop = array_merge($stop, $rejectStrs);
832 26
        }
833
834 26
        $patt = '(.*?)(' . implode('|', $stop) . ')';
835
836 26
        $nestingLevel = 0;
837 26
838 26
        $content = [];
839 25
        while ($this->match($patt, $m, false)) {
840 25
            if (!empty($m[1])) {
841
                $content[] = $m[1];
842
                if ($nestingOpen) {
843
                    $nestingLevel += substr_count($m[1], $nestingOpen);
844
                }
845 26
            }
846
847 26
            $tok = $m[2];
848 26
849 14
            $this->count -= strlen($tok);
850 14
            if ($tok == $end) {
851
                if ($nestingLevel === 0) {
852
                    break;
853
                } else {
854
                    $nestingLevel--;
855
                }
856 24
            }
857 13
858 13
            if (($tok === "'" || $tok === '"') && $this->stringValue($str)) {
859
                $content[] = $str;
860
                continue;
861 23
            }
862 3
863 3
            if ($tok === '@{' && $this->interpolation($inter)) {
864
                $content[] = $inter;
865
                continue;
866 23
            }
867 23
868
            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
869
                break;
870
            }
871
872
            $content[] = $tok;
873
            $this->count += strlen($tok);
874 26
        }
875
876 26
        $this->eatWhiteDefault = $oldWhite;
877 4
878
        if (count($content) === 0) {
879
            return false;
880
        }
881 25
882 25
        // trim the end
883
        if (is_string(end($content))) {
884
            $content[count($content) - 1] = rtrim(end($content));
885 25
        }
886
887 25
        $out = ['string', '', $content];
888
889
        return true;
890
    }
891
892
    /**
893
     * @param $out
894
     *
895 48
     * @return bool
896
     */
897 48
    protected function stringValue(&$out)
898 48
    {
899 22
        $s = $this->seek();
900 48
        if ($this->literal('"', false)) {
901 12
            $delim = '"';
902
        } elseif ($this->literal("'", false)) {
903 48
            $delim = "'";
904
        } else {
905
            return false;
906 24
        }
907
908
        $content = [];
909
910 24
        // look for either ending delim , escape, or string interpolation
911
        $patt = '([^\n]*?)(@\{|\\\\|' .
912 24
            Compiler::pregQuote($delim) . ')';
913 24
914
        $oldWhite = $this->eatWhiteDefault;
915 24
        $this->eatWhiteDefault = false;
916 24
917 24
        while ($this->match($patt, $m, false)) {
918 6
            $content[] = $m[1];
919 6
            if ($m[2] === '@{') {
920 6
                $this->count -= strlen($m[2]);
921
                if ($this->interpolation($inter)) {
922 1
                    $content[] = $inter;
923 6
                } else {
924
                    $this->count += strlen($m[2]);
925 24
                    $content[] = '@{'; // ignore it
926 2
                }
927 2
            } elseif ($m[2] === '\\') {
928 2
                $content[] = $m[2];
929
                if ($this->literal($delim, false)) {
930
                    $content[] = $delim;
931 24
                }
932 24
            } else {
933
                $this->count -= strlen($delim);
934
                break; // delim
935
            }
936 24
        }
937
938 24
        $this->eatWhiteDefault = $oldWhite;
939 24
940
        if ($this->literal($delim)) {
941 24
            $out = ['string', $delim, $content];
942
943
            return true;
944 1
        }
945
946 1
        $this->seek($s);
947
948
        return false;
949
    }
950
951
    /**
952
     * @param $out
953
     *
954 19
     * @return bool
955
     */
956 19
    protected function interpolation(&$out)
957 19
    {
958
        $oldWhite = $this->eatWhiteDefault;
959 19
        $this->eatWhiteDefault = true;
960 19
961 19
        $s = $this->seek();
962 19
        if ($this->literal('@{') &&
963
            $this->openString('}', $interp, null, ["'", '"', ';']) &&
964 7
            $this->literal('}', false)
965 7
        ) {
966 7
            $out = ['interpolate', $interp];
967 1
            $this->eatWhiteDefault = $oldWhite;
968
            if ($this->eatWhiteDefault) {
969
                $this->whitespace();
970 7
            }
971
972
            return true;
973 16
        }
974 16
975
        $this->eatWhiteDefault = $oldWhite;
976 16
        $this->seek($s);
977
978
        return false;
979
    }
980
981
    /**
982
     * @param $unit
983
     *
984 49
     * @return bool
985
     */
986
    protected function unit(&$unit)
987 49
    {
988 49
        // speed shortcut
989 49
        if (isset($this->buffer[$this->count])) {
990 49
            $char = $this->buffer[$this->count];
991
            if (!ctype_digit($char) && $char !== '.') {
992
                return false;
993
            }
994 49
        }
995 38
996
        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
997 38
            $unit = ['number', $m[1], empty($m[2]) ? '' : $m[2]];
998
999
            return true;
1000 49
        }
1001
1002
        return false;
1003
    }
1004
1005
1006
    /**
1007
     * a # color
1008
     *
1009
     * @param $out
1010
     *
1011 48
     * @return bool
1012
     */
1013 48
    protected function color(&$out)
1014 13
    {
1015 1
        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
1016
            if (strlen($m[1]) > 7) {
1017 13
                $out = ['string', '', [$m[1]]];
1018
            } else {
1019
                $out = ['raw_color', $m[1]];
1020 13
            }
1021
1022
            return true;
1023 48
        }
1024
1025
        return false;
1026
    }
1027
1028
    /**
1029
     * consume an argument definition list surrounded by ()
1030
     * each argument is a variable name with optional value
1031
     * or at the end a ... or a variable named followed by ...
1032
     * arguments are separated by , unless a ; is in the list, then ; is the
1033
     * delimiter.
1034
     *
1035
     * @param $args
1036
     * @param $isVararg
1037
     *
1038
     * @return bool
1039 45
     * @throws \LesserPhp\Exception\GeneralException
1040
     */
1041 45
    protected function argumentDef(&$args, &$isVararg)
1042 45
    {
1043 45
        $s = $this->seek();
1044
        if (!$this->literal('(')) {
1045
            return false;
1046 19
        }
1047 19
1048 19
        $values = [];
1049
        $delim = ',';
1050 19
        $method = 'expressionList';
1051 19
        $value = [];
1052 19
        $rhs = null;
1053 2
1054 2
        $isVararg = false;
1055
        while (true) {
1056
            if ($this->literal('...')) {
1057 19
                $isVararg = true;
1058 16
                break;
1059 16
            }
1060 16
1061
            if ($this->$method($value)) {
1062 16
                if ($value[0] === 'variable') {
1063 9
                    $arg = ['arg', $value[1]];
1064
                    $ss = $this->seek();
1065 13
1066 13
                    if ($this->assign() && $this->$method($rhs)) {
1067 2
                        $arg[] = $rhs;
1068 2
                    } else {
1069
                        $this->seek($ss);
1070
                        if ($this->literal('...')) {
1071
                            $arg[0] = 'rest';
1072 16
                            $isVararg = true;
1073 16
                        }
1074 2
                    }
1075
1076 16
                    $values[] = $arg;
1077
                    if ($isVararg) {
1078 15
                        break;
1079
                    }
1080
                    continue;
1081
                } else {
1082
                    $values[] = ['lit', $value];
1083 19
                }
1084 19
            }
1085
1086 2
1087 2
            if (!$this->literal($delim)) {
1088 2
                if ($delim === ',' && $this->literal(';')) {
1089
                    // found new delim, convert existing args
1090
                    $delim = ';';
1091 2
                    $method = 'propertyValue';
1092 2
                    $newArg = null;
1093 2
1094 2
                    // transform arg list
1095 2
                    if (isset($values[1])) { // 2 items
1096 2
                        $newList = [];
1097
                        foreach ($values as $i => $arg) {
1098
                            switch ($arg[0]) {
1099 2
                                case 'arg':
1100 2
                                    if ($i) {
1101 2
                                        throw new GeneralException('Cannot mix ; and , as delimiter types');
1102 2
                                    }
1103 2
                                    $newList[] = $arg[2];
1104
                                    break;
1105 2
                                case 'lit':
1106
                                    $newList[] = $arg[1];
1107
                                    break;
1108
                                case 'rest':
1109 2
                                    throw new GeneralException('Unexpected rest before semicolon');
1110
                            }
1111 2
                        }
1112 2
1113 2
                        $newList = ['list', ', ', $newList];
1114 2
1115 1
                        switch ($values[0][0]) {
1116 1
                            case 'arg':
1117 2
                                $newArg = ['arg', $values[0][1], $newList];
1118
                                break;
1119 2
                            case 'lit':
1120 2
                                $newArg = ['lit', $newList];
1121
                                break;
1122
                        }
1123 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...
1124 2
                        $newArg = $values[0];
1125
                    }
1126
1127 19
                    if ($newArg !== null) {
1128
                        $values = [$newArg];
1129
                    }
1130
                } else {
1131
                    break;
1132 19
                }
1133
            }
1134
        }
1135
1136
        if (!$this->literal(')')) {
1137
            $this->seek($s);
1138 19
1139
            return false;
1140 19
        }
1141
1142
        $args = $values;
1143
1144
        return true;
1145
    }
1146
1147
    /**
1148
     * consume a list of tags
1149
     * this accepts a hanging delimiter
1150
     *
1151
     * @param array  $tags
1152
     * @param bool   $simple
1153 49
     * @param string $delim
1154
     *
1155 49
     * @return bool
1156 49
     */
1157 46
    protected function tags(&$tags, $simple = false, $delim = ',')
1158 46
    {
1159 46
        $tags = [];
1160
        while ($this->tag($tt, $simple)) {
1161
            $tags[] = $tt;
1162
            if (!$this->literal($delim)) {
1163 49
                break;
1164
            }
1165
        }
1166
1167
        return count($tags) !== 0;
1168
    }
1169
1170
    /**
1171
     * list of tags of specifying mixin path
1172
     * optionally separated by > (lazy, accepts extra >)
1173
     *
1174 49
     * @param array $tags
1175
     *
1176 49
     * @return bool
1177 49
     */
1178 22
    protected function mixinTags(&$tags)
1179 22
    {
1180
        $tags = [];
1181
        while ($this->tag($tt, true)) {
1182 49
            $tags[] = $tt;
1183
            $this->literal('>');
1184
        }
1185
1186
        return count($tags) !== 0;
1187
    }
1188
1189
    /**
1190
     * a bracketed value (contained within in a tag definition)
1191
     *
1192
     * @param array $parts
1193 49
     * @param bool $hasExpression
1194
     *
1195
     * @return bool
1196 49
     */
1197 47
    protected function tagBracket(&$parts, &$hasExpression)
1198
    {
1199
        // speed shortcut
1200 49
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== '[') {
1201
            return false;
1202 49
        }
1203
1204 49
        $s = $this->seek();
1205 3
1206
        $hasInterpolation = false;
1207 3
1208 3
        if ($this->literal('[', false)) {
1209 3
            $attrParts = ['['];
1210 3
            // keyword, string, operator
1211
            while (true) {
1212
                if ($this->literal(']', false)) {
1213 3
                    $this->count--;
1214
                    break; // get out early
1215
                }
1216
1217 3
                if ($this->match('\s+', $m)) {
1218
                    $attrParts[] = ' ';
1219 3
                    continue;
1220 3
                }
1221
                if ($this->stringValue($str)) {
1222
                    // escape parent selector, (yuck)
1223 3
                    foreach ($str[2] as &$chunk) {
1224 3
                        $chunk = str_replace($this->lessc->getParentSelector(), '$&$', $chunk);
1225 3
                    }
1226
1227
                    $attrParts[] = $str;
1228 3
                    $hasInterpolation = true;
1229 3
                    continue;
1230 3
                }
1231
1232
                if ($this->keyword($word)) {
1233 3
                    $attrParts[] = $word;
1234 1
                    continue;
1235 1
                }
1236 1
1237
                if ($this->interpolation($inter)) {
1238
                    $attrParts[] = $inter;
1239
                    $hasInterpolation = true;
1240 3
                    continue;
1241 3
                }
1242 3
1243
                // operator, handles attr namespace too
1244
                if ($this->match('[|-~\$\*\^=]+', $m)) {
1245
                    $attrParts[] = $m[0];
1246
                    continue;
1247
                }
1248 3
1249 3
                break;
1250 3
            }
1251 3
1252
            if ($this->literal(']', false)) {
1253 3
                $attrParts[] = ']';
1254
                foreach ($attrParts as $part) {
1255 3
                    $parts[] = $part;
1256
                }
1257
                $hasExpression = $hasExpression || $hasInterpolation;
1258
1259
                return true;
1260 49
            }
1261
            $this->seek($s);
1262 49
        }
1263
1264
        $this->seek($s);
1265
1266
        return false;
1267
    }
1268
1269
    /**
1270
     * a space separated list of selectors
1271
     *
1272
     * @param      $tag
1273 49
     * @param bool $simple
1274
     *
1275 49
     * @return bool
1276 49
     */
1277
    protected function tag(&$tag, $simple = false)
1278 49
    {
1279
        if ($simple) {
1280
            $chars = '^@,:;{}\][>\(\) "\'';
1281 49
        } else {
1282
            $chars = '^@,;{}["\'';
1283 49
        }
1284 49
1285 49
        $s = $this->seek();
1286
1287
        $hasExpression = false;
1288
        $parts = [];
1289 49
        while ($this->tagBracket($parts, $hasExpression)) {
1290 49
            ;
1291
        }
1292 49
1293 49
        $oldWhite = $this->eatWhiteDefault;
1294 46
        $this->eatWhiteDefault = false;
1295 46
1296 45
        while (true) {
1297
            if ($this->match('([' . $chars . '0-9][' . $chars . ']*)', $m)) {
1298
                $parts[] = $m[1];
1299 46
                if ($simple) {
1300
                    break;
1301
                }
1302 46
1303
                while ($this->tagBracket($parts, $hasExpression)) {
1304
                    ;
1305 49
                }
1306 13
                continue;
1307 2
            }
1308 2
1309 2
            if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === '@') {
1310 2
                if ($this->interpolation($interp)) {
1311
                    $hasExpression = true;
1312
                    $interp[2] = true; // don't unescape
1313 12
                    $parts[] = $interp;
1314 12
                    continue;
1315 12
                }
1316
1317
                if ($this->literal('@')) {
1318
                    $parts[] = '@';
1319 49
                    continue;
1320 9
                }
1321 9
            }
1322 9
1323
            if ($this->unit($unit)) { // for keyframes
1324
                $parts[] = $unit[1];
1325 49
                $parts[] = $unit[2];
1326
                continue;
1327
            }
1328 49
1329 49
            break;
1330 49
        }
1331
1332 49
        $this->eatWhiteDefault = $oldWhite;
1333
        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...
1334
            $this->seek($s);
1335 46
1336 4
            return false;
1337
        }
1338 46
1339
        if ($hasExpression) {
1340
            $tag = ['exp', ['string', '', $parts]];
1341 46
        } else {
1342
            $tag = trim(implode($parts));
1343 46
        }
1344
1345
        $this->whitespace();
1346
1347
        return true;
1348
    }
1349
1350
    /**
1351
     * a css function
1352
     *
1353 48
     * @param array $func
1354
     *
1355 48
     * @return bool
1356
     */
1357 48
    protected function func(&$func)
1358 25
    {
1359
        $s = $this->seek();
1360 25
1361
        if ($this->match('(%|[\w\-_][\w\-_:\.]+|[\w_])', $m) && $this->literal('(')) {
1362 25
            $fname = $m[1];
1363 25
1364 25
            $sPreArgs = $this->seek();
1365
1366 25
            $args = [];
1367 1
            while (true) {
1368
                $ss = $this->seek();
1369 24
                // this ugly nonsense is for ie filter properties
1370 24
                if ($this->keyword($name) && $this->literal('=') && $this->expressionList($value)) {
1371 21
                    $args[] = ['string', '', [$name, '=', $value]];
1372
                } else {
1373
                    $this->seek($ss);
1374
                    if ($this->expressionList($value)) {
1375 25
                        $args[] = $value;
1376 25
                    }
1377
                }
1378
1379 25
                if (!$this->literal(',')) {
1380
                    break;
1381 25
                }
1382 25
            }
1383
            $args = ['list', ',', $args];
1384 25
1385 7
            if ($this->literal(')')) {
1386
                $func = ['function', $fname, $args];
1387 6
1388 6
                return true;
1389 6
            } elseif ($fname === 'url') {
1390
                // couldn't parse and in url? treat as string
1391 6
                $this->seek($sPreArgs);
1392
                if ($this->openString(')', $string) && $this->literal(')')) {
1393
                    $func = ['function', $fname, $string];
1394
1395
                    return true;
1396 48
                }
1397
            }
1398 48
        }
1399
1400
        $this->seek($s);
1401
1402
        return false;
1403
    }
1404
1405
    /**
1406
     * consume a less variable
1407
     *
1408 49
     * @param $name
1409
     *
1410 49
     * @return bool
1411 49
     */
1412 49
    protected function variable(&$name)
1413
    {
1414 32
        $s = $this->seek();
1415 1
        if ($this->literal($this->lessc->getVPrefix(), false) &&
1416
            ($this->variable($sub) || $this->keyword($name))
1417 32
        ) {
1418
            if (!empty($sub)) {
1419
                $name = ['variable', $sub];
1420 32
            } else {
1421
                $name = $this->lessc->getVPrefix() . $name;
1422
            }
1423 49
1424 49
            return true;
1425
        }
1426 49
1427
        $name = null;
1428
        $this->seek($s);
1429
1430
        return false;
1431
    }
1432
1433
    /**
1434
     * Consume an assignment operator
1435
     * Can optionally take a name that will be set to the current property name
1436
     *
1437 48
     * @param string $name
1438
     *
1439 48
     * @return bool
1440
     */
1441
    protected function assign($name = null)
1442
    {
1443 48
        if ($name !== null) {
1444
            $this->currentProperty = $name;
1445
        }
1446
1447
        return $this->literal(':') || $this->literal('=');
1448
    }
1449
1450
    /**
1451
     * consume a keyword
1452
     *
1453 49
     * @param $word
1454
     *
1455 49
     * @return bool
1456 48
     */
1457
    protected function keyword(&$word)
1458 48
    {
1459
        if ($this->match('([\w_\-\*!"][\w\-_"]*)', $m)) {
1460
            $word = $m[1];
1461 49
1462
            return true;
1463
        }
1464
1465
        return false;
1466
    }
1467
1468
    /**
1469 48
     * consume an end of statement delimiter
1470
     *
1471 48
     * @return bool
1472 48
     */
1473 12
    protected function end()
1474
    {
1475 10
        if ($this->literal(';', false)) {
1476
            return true;
1477
        } elseif ($this->count === strlen($this->buffer) || $this->buffer[$this->count] === '}') {
1478 2
            // if there is end of file or a closing block next then we don't need a ;
1479
            return true;
1480
        }
1481
1482
        return false;
1483
    }
1484
1485
    /**
1486 19
     * @param $guards
1487
     *
1488 19
     * @return bool
1489
     */
1490 19
    protected function guards(&$guards)
1491 19
    {
1492
        $s = $this->seek();
1493 19
1494
        if (!$this->literal('when')) {
1495
            $this->seek($s);
1496 5
1497
            return false;
1498 5
        }
1499 5
1500 5
        $guards = [];
1501 5
1502
        while ($this->guardGroup($g)) {
1503
            $guards[] = $g;
1504
            if (!$this->literal(',')) {
1505 5
                break;
1506
            }
1507
        }
1508
1509
        if (count($guards) === 0) {
1510
            $guards = null;
1511
            $this->seek($s);
1512 5
1513
            return false;
1514
        }
1515
1516
        return true;
1517
    }
1518
1519
    /**
1520
     * a bunch of guards that are and'd together
1521
     *
1522 5
     * @param $guardGroup
1523
     *
1524 5
     * @return bool
1525 5
     */
1526 5
    protected function guardGroup(&$guardGroup)
1527 5
    {
1528 5
        $s = $this->seek();
1529 5
        $guardGroup = [];
1530
        while ($this->guard($guard)) {
1531
            $guardGroup[] = $guard;
1532
            if (!$this->literal('and')) {
1533 5
                break;
1534
            }
1535
        }
1536
1537
        if (count($guardGroup) === 0) {
1538
            $guardGroup = null;
1539
            $this->seek($s);
1540 5
1541
            return false;
1542
        }
1543
1544
        return true;
1545
    }
1546
1547
    /**
1548 5
     * @param $guard
1549
     *
1550 5
     * @return bool
1551 5
     */
1552
    protected function guard(&$guard)
1553 5
    {
1554 5
        $s = $this->seek();
1555 5
        $negate = $this->literal('not');
1556 1
1557
        if ($this->literal('(') && $this->expression($exp) && $this->literal(')')) {
1558
            $guard = $exp;
1559 5
            if ($negate) {
1560
                $guard = ['negate', $guard];
1561
            }
1562
1563
            return true;
1564
        }
1565
1566
        $this->seek($s);
1567
1568
        return false;
1569
    }
1570
1571
    /* raw parsing functions */
1572
1573
    /**
1574
     * @param string $what
1575 49
     * @param bool $eatWhitespace
1576
     *
1577 49
     * @return bool
1578 49
     */
1579
    protected function literal($what, $eatWhitespace = null)
1580
    {
1581
        if ($eatWhitespace === null) {
1582 49
            $eatWhitespace = $this->eatWhiteDefault;
1583 49
        }
1584 49
1585 49
        // shortcut on single letter
1586
        if (!isset($what[1]) && isset($this->buffer[$this->count])) {
1587 49
            if ($this->buffer[$this->count] === $what) {
1588
                if (!$eatWhitespace) {
1589
                    $this->count++;
1590
1591 49
                    return true;
1592
                }
1593
                // goes below...
1594
            } else {
1595 49
                return false;
1596 11
            }
1597
        }
1598
1599 49
        if (!isset(self::$literalCache[$what])) {
1600
            self::$literalCache[$what] = Compiler::pregQuote($what);
1601
        }
1602
1603
        return $this->match(self::$literalCache[$what], $m, $eatWhitespace);
1604
    }
1605
1606
    /**
1607
     * @param        $out
1608
     * @param string $parseItem
1609
     * @param string $delim
1610 3
     * @param bool   $flatten
1611
     *
1612
     * @return bool
1613 3
     */
1614 3
    protected function genericList(&$out, $parseItem, $delim = "", $flatten = true)
1615 3
    {
1616 3
        // $parseItem is one of mediaQuery, mediaExpression
1617 3
        $s = $this->seek();
1618 3
        $items = [];
1619 3
        $value = null;
1620
        while ($this->$parseItem($value)) {
1621
            $items[] = $value;
1622
            if ($delim) {
1623
                if (!$this->literal($delim)) {
1624 3
                    break;
1625
                }
1626
            }
1627
        }
1628
1629
        if (count($items) === 0) {
1630 3
            $this->seek($s);
1631
1632
            return false;
1633 3
        }
1634
1635
        if ($flatten && count($items) === 1) {
1636 3
            $out = $items[0];
1637
        } else {
1638
            $out = ['list', $delim, $items];
1639
        }
1640
1641
        return true;
1642
    }
1643
1644
    /**
1645
     * try to match something on head of buffer
1646
     *
1647
     * @param string $regex
1648
     * @param      $out
1649
     * @param bool $eatWhitespace
1650
     *
1651
     * @return bool
1652
     */
1653
    protected function match($regex, &$out, $eatWhitespace = null)
1654
    {
1655
        if ($eatWhitespace === null) {
1656
            $eatWhitespace = $this->eatWhiteDefault;
1657
        }
1658
1659
        $r = '/' . $regex . ($eatWhitespace && !$this->writeComments ? '\s*' : '') . '/Ais';
1660
        if (preg_match($r, $this->buffer, $out, null, $this->count)) {
1661
            $this->count += strlen($out[0]);
1662
            if ($eatWhitespace && $this->writeComments) {
1663
                $this->whitespace();
1664
            }
1665
1666
            return true;
1667
        }
1668
1669
        return false;
1670
    }
1671
1672
    /**
1673
     * match some whitespace
1674
     *
1675
     * @return bool
1676
     */
1677
    protected function whitespace()
1678
    {
1679 49
        if ($this->writeComments) {
1680
            $gotWhite = false;
1681 49
            while (preg_match(self::$whitePattern, $this->buffer, $m, null, $this->count)) {
1682 49
                if (isset($m[1]) && empty($this->seenComments[$this->count])) {
1683
                    $this->append(['comment', $m[1]]);
1684
                    $this->seenComments[$this->count] = true;
1685 49
                }
1686 49
                $this->count += mb_strlen($m[0]);
1687 49
                $gotWhite = true;
1688 49
            }
1689 1
1690
            return $gotWhite;
1691
        }
1692 49
1693
        $this->match('', $m);
1694
        return mb_strlen($m[0]) > 0;
1695 49
    }
1696
1697
    /**
1698
     * match something without consuming it
1699
     *
1700
     * @param string $regex
1701
     * @param array $out
1702
     * @param int $from
1703 49
     *
1704
     * @return int
1705 49
     */
1706 1
    protected function peek($regex, &$out = null, $from = null)
1707 1
    {
1708 1
        if ($from === null) {
1709 1
            $from = $this->count;
1710 1
        }
1711
        $r = '/' . $regex . '/Ais';
1712 1
1713 1
        return preg_match($r, $this->buffer, $out, null, $from);
1714
    }
1715
1716 1
    /**
1717
     * seek to a spot in the buffer or return where we are on no argument
1718
     *
1719 49
     * @param int $where
1720 49
     *
1721
     * @return int
1722
     */
1723
    protected function seek($where = null)
1724
    {
1725
        if ($where !== null) {
1726
            $this->count = $where;
1727
        }
1728
1729
        return $this->count;
1730
    }
1731
1732 24
    /* misc functions */
1733
1734 24
    /**
1735 20
     * @param string $msg
1736
     * @param int $count
1737 24
     *
1738
     * @throws \LesserPhp\Exception\GeneralException
1739 24
     */
1740
    public function throwError($msg = 'parse error', $count = null)
1741
    {
1742
        $count = $count === null ? $this->count : $count;
1743
1744
        $line = $this->line + substr_count(substr($this->buffer, 0, $count), "\n");
1745
1746
        if (!empty($this->sourceName)) {
1747
            $loc = "$this->sourceName on line $line";
1748
        } else {
1749 49
            $loc = "line: $line";
1750
        }
1751 49
1752 49
        // TODO this depends on $this->count
1753
        if ($this->peek("(.*?)(\n|$)", $m, $count)) {
1754
            throw new GeneralException("$msg: failed at `$m[1]` $loc");
1755 49
        } else {
1756
            throw new GeneralException("$msg: $loc");
1757
        }
1758
    }
1759
1760
    /**
1761
     * @param null $selectors
1762
     * @param null $type
1763
     *
1764
     * @return \stdClass
1765
     */
1766 5
    protected function pushBlock($selectors = null, $type = null)
1767
    {
1768 5
        $b = new \stdClass();
1769
        $b->parent = $this->env;
1770 5
1771
        $b->type = $type;
1772 5
        $b->id = self::$nextBlockId++;
1773
1774
        $b->isVararg = false; // TODO: kill me from here
1775 5
        $b->tags = $selectors;
1776
1777
        $b->props = [];
1778
        $b->children = [];
1779 5
1780 5
        // add a reference to the parser so
1781
        // we can access the parser to throw errors
1782
        // or retrieve the sourceName of this block.
1783
        $b->parser = $this;
1784
1785
        // so we know the position of this block
1786
        $b->count = $this->count;
1787
1788
        $this->env = $b;
1789
1790
        return $b;
1791
    }
1792 49
1793
    /**
1794 49
     * push a block that doesn't multiply tags
1795 49
     *
1796
     * @param $type
1797 49
     *
1798 49
     * @return \stdClass
1799
     */
1800 49
    protected function pushSpecialBlock($type)
1801 49
    {
1802
        return $this->pushBlock(null, $type);
1803 49
    }
1804 49
1805
    /**
1806
     * append a property to the current block
1807
     *
1808
     * @param      $prop
1809 49
     * @param  $pos
1810
     */
1811
    protected function append($prop, $pos = null)
1812 49
    {
1813
        if ($pos !== null) {
1814 49
            $prop[-1] = $pos;
1815
        }
1816 49
        $this->env->props[] = $prop;
1817
    }
1818
1819
    /**
1820
     * pop something off the stack
1821
     *
1822
     * @return mixed
1823
     */
1824
    protected function pop()
1825
    {
1826 49
        $old = $this->env;
1827
        $this->env = $this->env->parent;
1828 49
1829
        return $old;
1830
    }
1831
1832
    /**
1833
     * remove comments from $text
1834
     * todo: make it work for all functions, not just url
1835
     *
1836
     * @param string $text
1837 49
     *
1838
     * @return string
1839 49
     */
1840 49
    protected function removeComments($text)
1841
    {
1842 49
        $look = [
1843 49
            'url(',
1844
            '//',
1845
            '/*',
1846
            '"',
1847
            "'",
1848
        ];
1849
1850 46
        $out = '';
1851
        $min = null;
1852 46
        while (true) {
1853 46
            // find the next item
1854
            foreach ($look as $token) {
1855 46
                $pos = mb_strpos($text, $token);
1856
                if ($pos !== false) {
1857
                    if ($min === null || $pos < $min[1]) {
1858
                        $min = [$token, $pos];
1859
                    }
1860
                }
1861
            }
1862
1863
            if ($min === null) {
1864
                break;
1865
            }
1866 49
1867
            $count = $min[1];
1868
            $skip = 0;
1869 49
            $newlines = 0;
1870
            switch ($min[0]) {
1871
                case 'url(':
1872
                    if (preg_match('/url\(.*?\)/', $text, $m, 0, $count)) {
1873
                        $count += mb_strlen($m[0]) - mb_strlen($min[0]);
1874
                    }
1875
                    break;
1876 49
                case '"':
1877 49
                case "'":
1878 49
                    if (preg_match('/' . $min[0] . '.*?(?<!\\\\)' . $min[0] . '/', $text, $m, 0, $count)) {
1879
                        $count += mb_strlen($m[0]) - 1;
1880 49
                    }
1881 49
                    break;
1882 49
                case '//':
1883 27
                    $skip = mb_strpos($text, "\n", $count);
1884 49
                    if ($skip === false) {
1885
                        $skip = mb_strlen($text) - $count;
1886
                    } else {
1887
                        $skip -= $count;
1888
                    }
1889 49
                    break;
1890 49
                case '/*':
1891
                    if (preg_match('/\/\*.*?\*\//s', $text, $m, 0, $count)) {
1892
                        $skip = mb_strlen($m[0]);
1893 27
                        $newlines = mb_substr_count($m[0], "\n");
1894 27
                    }
1895 27
                    break;
1896 27
            }
1897 27
1898 6
            if ($skip === 0) {
1899 6
                $count += mb_strlen($min[0]);
1900
            }
1901 6
1902 27
            $out .= mb_substr($text, 0, $count) . str_repeat("\n", $newlines);
1903 22
            $text = mb_substr($text, $count + $skip);
1904 24
1905 24
            $min = null;
1906
        }
1907 24
1908 18
        return $out . $text;
1909 18
    }
1910 18
1911
    /**
1912
     * @param bool $writeComments
1913 18
     */
1914
    public function setWriteComments($writeComments)
1915 18
    {
1916 4
        $this->writeComments = $writeComments;
1917 4
    }
1918 4
1919 4
    /**
1920
     * @param $s
1921 4
     *
1922
     * @return bool
1923
     */
1924 27
    protected function handleLiteralMedia($s)
1925 24
    {
1926
        // seriously, this || true is required for this statement to work!?
1927
        if (($this->mediaQueryList($mediaQueries) || true) && $this->literal('{')) {
1928 27
            $media = $this->pushSpecialBlock('media');
1929 27
            $media->queries = $mediaQueries === null ? [] : $mediaQueries;
1930
1931 27
            return true;
1932
        } else {
1933
            $this->seek($s);
1934 49
        }
1935
1936
        return false;
1937
    }
1938
1939
    /**
1940 49
     * @param string $directiveName
1941
     *
1942 49
     * @return bool
1943 49
     */
1944
    protected function handleDirectiveBlock($directiveName)
1945
    {
1946
        // seriously, this || true is required for this statement to work!?
1947
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1948
            $dir = $this->pushSpecialBlock('directive');
1949
            $dir->name = $directiveName;
1950 3
            if ($directiveValue !== null) {
1951
                $dir->value = $directiveValue;
1952
            }
1953 3
1954 3
            return true;
1955 3
        }
1956
1957 3
        return false;
1958
    }
1959
1960
    /**
1961
     * @param string $directiveName
1962
     *
1963
     * @return bool
1964
     */
1965
    protected function handleDirectiveLine($directiveName)
1966
    {
1967
        if ($this->propertyValue($directiveValue) && $this->end()) {
1968
            $this->append(['directive', $directiveName, $directiveValue]);
1969
1970 4
            return true;
1971
        }
1972
1973 4
        return false;
1974 4
    }
1975 4
1976 4
    /**
1977 2
     * @param string $directiveName
1978
     *
1979
     * @return bool
1980 4
     */
1981
    protected function handleRulesetDefinition($directiveName)
1982
    {
1983 1
        //Ruleset Definition
1984
        // seriously, this || true is required for this statement to work!?
1985
        if (($this->openString('{', $directiveValue, null, [';']) || true) && $this->literal('{')) {
1986
            $dir = $this->pushBlock($this->fixTags(['@' . $directiveName]));
1987
            $dir->name = $directiveName;
1988
            if ($directiveValue !== null) {
1989
                $dir->value = $directiveValue;
1990
            }
1991 1
1992
            return true;
1993 1
        }
1994 1
1995
        return false;
1996 1
    }
1997
1998
    private function clearBlockStack()
1999
    {
2000
        $this->env = null;
2001
    }
2002
}
2003