Completed
Push — master ( 14f6f1...9a86cf )
by Marcus
02:46
created

src/LesserPhp/Parser.php (11 issues)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace LesserPhp;
4
5
use LesserPhp\Exception\GeneralException;
6
7
/**
8
 * lesserphp
9
 * https://www.maswaba.de/lesserphp
10
 *
11
 * LESS CSS compiler, adapted from http://lesscss.org
12
 *
13
 * Copyright 2013, Leaf Corcoran <[email protected]>
14
 * Copyright 2016, Marcus Schwarz <[email protected]>
15
 * Licensed under MIT or GPLv3, see LICENSE
16
 * @package LesserPhp
17
 * // responsible for taking a string of LESS code and converting it into a
18
 * // syntax tree
19
 */
20
class Parser
21
{
22
23
    static protected $nextBlockId = 0; // used to uniquely identify blocks
24
25
    static protected $precedence = [
26
        '=<' => 0,
27
        '>=' => 0,
28
        '=' => 0,
29
        '<' => 0,
30
        '>' => 0,
31
32
        '+' => 1,
33
        '-' => 1,
34
        '*' => 2,
35
        '/' => 2,
36
        '%' => 2,
37
    ];
38
39
    static protected $whitePattern;
40
    static protected $commentMulti;
41
42
    static protected $commentSingle = "//";
43
    static protected $commentMultiLeft = "/*";
44
    static protected $commentMultiRight = "*/";
45
46
    // regex string to match any of the operators
47
    static protected $operatorString;
48
49
    // these properties will supress division unless it's inside parenthases
50
    static protected $supressDivisionProps =
51
        ['/border-radius$/i', '/^font$/i'];
52
53
    protected $blockDirectives = [
54
        'font-face',
55
        'keyframes',
56
        'page',
57
        '-moz-document',
58
        'viewport',
59
        '-moz-viewport',
60
        '-o-viewport',
61
        '-ms-viewport',
62
    ];
63
    protected $lineDirectives = ['charset'];
64
65
    /**
66
     * if we are in parens we can be more liberal with whitespace around
67
     * operators because it must evaluate to a single value and thus is less
68
     * ambiguous.
69
     *
70
     * Consider:
71
     *     property1: 10 -5; // is two numbers, 10 and -5
72
     *     property2: (10 -5); // should evaluate to 5
73
     */
74
    protected $inParens = false;
75
76
    // caches preg escaped literals
77
    static protected $literalCache = [];
78
    /** @var int */
79
    public $count;
80
    /** @var int */
81
    private $line;
82
    /** @var array */
83
    private $seenComments;
84
    /** @var string */
85
    public $buffer;
86
87
    private $env;
88
    /** @var bool */
89
    private $inExp;
90
    /** @var string */
91
    private $currentProperty;
92
93
    /**
94
     * @var bool
95
     */
96
    private $writeComments = false;
97
98
    /**
99
     * Parser constructor.
100
     *
101
     * @param \LesserPhp\Compiler $lessc
102
     * @param null                $sourceName
103
     */
104 49
    public function __construct(Compiler $lessc, $sourceName = null)
105
    {
106 49
        $this->eatWhiteDefault = true;
107
        // reference to less needed for vPrefix, mPrefix, and parentSelector
108 49
        $this->lessc = $lessc;
109
110 49
        $this->sourceName = $sourceName; // name used for error messages
111
112 49
        if (!self::$operatorString) {
113 1
            self::$operatorString =
114 1
                '(' . implode('|', array_map(['\LesserPhp\Compiler', 'pregQuote'],
115 1
                    array_keys(self::$precedence))) . ')';
116
117 1
            $commentSingle = \LesserPhp\Compiler::pregQuote(self::$commentSingle);
118 1
            $commentMultiLeft = \LesserPhp\Compiler::pregQuote(self::$commentMultiLeft);
119 1
            $commentMultiRight = \LesserPhp\Compiler::pregQuote(self::$commentMultiRight);
120
121 1
            self::$commentMulti = $commentMultiLeft . '.*?' . $commentMultiRight;
122 1
            self::$whitePattern = '/' . $commentSingle . '[^\n]*\s*|(' . self::$commentMulti . ')\s*|\s+/Ais';
123
        }
124 49
    }
125
126 49
    public function parse($buffer)
127
    {
128 49
        $this->count = 0;
129 49
        $this->line = 1;
130
131 49
        $this->env = null; // block stack
132 49
        $this->buffer = $this->writeComments ? $buffer : $this->removeComments($buffer);
133 49
        $this->pushSpecialBlock('root');
134 49
        $this->eatWhiteDefault = true;
135 49
        $this->seenComments = [];
136
137 49
        $this->whitespace();
138
139
        // parse the entire file
140 49
        while (false !== $this->parseChunk()) {
141
            ;
142
        }
143
144 49
        if ($this->count !== strlen($this->buffer)) {
145
//            var_dump($this->count);
0 ignored issues
show
Unused Code Comprehensibility introduced by
59% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
146
//            var_dump($this->buffer);
147
            $this->throwError();
148
        }
149
150
        // TODO report where the block was opened
151 49
        if (!property_exists($this->env, 'parent') || !is_null($this->env->parent)) {
152
            throw new \Exception('parse error: unclosed block');
153
        }
154
155 49
        return $this->env;
156
    }
157
158
    /**
159
     * Parse a single chunk off the head of the buffer and append it to the
160
     * current parse environment.
161
     * Returns false when the buffer is empty, or when there is an error.
162
     *
163
     * This function is called repeatedly until the entire document is
164
     * parsed.
165
     *
166
     * This parser is most similar to a recursive descent parser. Single
167
     * functions represent discrete grammatical rules for the language, and
168
     * they are able to capture the text that represents those rules.
169
     *
170
     * Consider the function \LesserPhp\Compiler::keyword(). (all parse functions are
171
     * structured the same)
172
     *
173
     * The function takes a single reference argument. When calling the
174
     * function it will attempt to match a keyword on the head of the buffer.
175
     * If it is successful, it will place the keyword in the referenced
176
     * argument, advance the position in the buffer, and return true. If it
177
     * fails then it won't advance the buffer and it will return false.
178
     *
179
     * All of these parse functions are powered by \LesserPhp\Compiler::match(), which behaves
180
     * the same way, but takes a literal regular expression. Sometimes it is
181
     * more convenient to use match instead of creating a new function.
182
     *
183
     * Because of the format of the functions, to parse an entire string of
184
     * grammatical rules, you can chain them together using &&.
185
     *
186
     * But, if some of the rules in the chain succeed before one fails, then
187
     * the buffer position will be left at an invalid state. In order to
188
     * avoid this, \LesserPhp\Compiler::seek() is used to remember and set buffer positions.
189
     *
190
     * Before parsing a chain, use $s = $this->seek() to remember the current
191
     * position into $s. Then if a chain fails, use $this->seek($s) to
192
     * go back where we started.
193
     */
194 49
    protected function parseChunk()
195
    {
196 49
        if (empty($this->buffer)) {
197
            return false;
198
        }
199 49
        $s = $this->seek();
200
201 49
        if ($this->whitespace()) {
202 46
            return true;
203
        }
204
205
        // setting a property
206 49 View Code Duplication
        if ($this->keyword($key) && $this->assign() &&
207 49
            $this->propertyValue($value, $key) && $this->end()
208
        ) {
209 47
            $this->append(['assign', $key, $value], $s);
210
211 47
            return true;
212
        } else {
213 49
            $this->seek($s);
214
        }
215
216
217
        // look for special css blocks
218 49
        if ($this->literal('@', false)) {
219 26
            $this->count--;
220
221
            // media
222 26
            if ($this->literal('@media')) {
223 3
                if (($this->mediaQueryList($mediaQueries) || true)
224 3
                    && $this->literal('{')
225
                ) {
226 3
                    $media = $this->pushSpecialBlock("media");
227 3
                    $media->queries = $mediaQueries === null ? [] : $mediaQueries;
228
229 3
                    return true;
230
                } else {
231
                    $this->seek($s);
232
233
                    return false;
234
                }
235
            }
236
237 26
            if ($this->literal("@", false) && $this->keyword($dirName)) {
238 26
                if ($this->isDirective($dirName, $this->blockDirectives)) {
239 4 View Code Duplication
                    if (($this->openString("{", $dirValue, null, [";"]) || true) &&
240 4
                        $this->literal("{")
241
                    ) {
242 4
                        $dir = $this->pushSpecialBlock("directive");
243 4
                        $dir->name = $dirName;
244 4
                        if (isset($dirValue)) {
245 2
                            $dir->value = $dirValue;
246
                        }
247
248 4
                        return true;
249
                    }
250 25
                } elseif ($this->isDirective($dirName, $this->lineDirectives)) {
251 1
                    if ($this->propertyValue($dirValue) && $this->end()) {
252 1
                        $this->append(["directive", $dirName, $dirValue]);
253
254 1
                        return true;
255
                    }
256 25
                } elseif ($this->literal(":", true)) {
257
                    //Ruleset Definition
258 24 View Code Duplication
                    if (($this->openString("{", $dirValue, null, [";"]) || true) &&
259 24
                        $this->literal("{")
260
                    ) {
261 1
                        $dir = $this->pushBlock($this->fixTags(["@" . $dirName]));
262 1
                        $dir->name = $dirName;
263 1
                        if (isset($dirValue)) {
264
                            $dir->value = $dirValue;
265
                        }
266
267 1
                        return true;
268
                    }
269
                }
270
            }
271
272 25
            $this->seek($s);
273
        }
274
275
276 49
        if ($this->literal('&', false)) {
277 4
            $this->count--;
278 4
            if ($this->literal('&:extend')) {
0 ignored issues
show
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...
279
                // hierauf folgt was in runden klammern, und zwar das element, das erweitert werden soll
280
                // heißt also, das was in klammern steht wird um die aktuellen klassen erweitert
281
                /*
282
Aus
283
284
nav ul {
285
  &:extend(.inline);
286
  background: blue;
287
}
288
.inline {
289
  color: red;
290
}
291
292
293
Wird:
294
295
nav ul {
296
  background: blue;
297
}
298
.inline,
299
nav ul {
300
  color: red;
301
}
302
303
                 */
304
//                echo "Here we go";
305
            }
306
        }
307
308
309
        // setting a variable
310 49 View Code Duplication
        if ($this->variable($var) && $this->assign() &&
311 49
            $this->propertyValue($value) && $this->end()
312
        ) {
313 23
            $this->append(['assign', $var, $value], $s);
314
315 23
            return true;
316
        } else {
317 49
            $this->seek($s);
318
        }
319
320 49
        if ($this->import($importValue)) {
321 3
            $this->append($importValue, $s);
322
323 3
            return true;
324
        }
325
326
        // opening parametric mixin
327 49
        if ($this->tag($tag, true) && $this->argumentDef($args, $isVararg) &&
328 49
            ($this->guards($guards) || true) &&
329 49
            $this->literal('{')
330
        ) {
331 18
            $block = $this->pushBlock($this->fixTags([$tag]));
332 18
            $block->args = $args;
333 18
            $block->isVararg = $isVararg;
334 18
            if (!empty($guards)) {
335 5
                $block->guards = $guards;
336
            }
337
338 18
            return true;
339
        } else {
340 49
            $this->seek($s);
341
        }
342
343
        // opening a simple block
344 49
        if ($this->tags($tags) && $this->literal('{', false)) {
345 46
            $tags = $this->fixTags($tags);
346 46
            $this->pushBlock($tags);
347
348 46
            return true;
349
        } else {
350 49
            $this->seek($s);
351
        }
352
353
        // closing a block
354 49
        if ($this->literal('}', false)) {
355
            try {
356 46
                $block = $this->pop();
357
            } catch (\Exception $e) {
358
                $this->seek($s);
359
                $this->throwError($e->getMessage());
360
            }
361
362 46
            $hidden = false;
363 46
            if ($block->type === null) {
364 46
                $hidden = true;
365 46
                if (!isset($block->args)) {
366 46
                    foreach ($block->tags as $tag) {
367 46
                        if (!is_string($tag) || $tag[0] !== $this->lessc->getMPrefix()) {
368 46
                            $hidden = false;
369 46
                            break;
370
                        }
371
                    }
372
                }
373
374 46
                foreach ($block->tags as $tag) {
375 46
                    if (is_string($tag)) {
376 46
                        $this->env->children[$tag][] = $block;
377
                    }
378
                }
379
            }
380
381 46
            if (!$hidden) {
382 46
                $this->append(['block', $block], $s);
383
            }
384
385
            // this is done here so comments aren't bundled into he block that
386
            // was just closed
387 46
            $this->whitespace();
388
389 46
            return true;
390
        }
391
392
        // mixin
393 49
        if ($this->mixinTags($tags) &&
394 49
            ($this->argumentDef($argv, $isVararg) || true) &&
395 49
            ($this->keyword($suffix) || true) && $this->end()
396
        ) {
397 22
            $tags = $this->fixTags($tags);
398 22
            $this->append(['mixin', $tags, $argv, $suffix], $s);
399
400 22
            return true;
401
        } else {
402 49
            $this->seek($s);
403
        }
404
405
        // spare ;
406 49
        if ($this->literal(';')) {
407 4
            return true;
408
        }
409
410 49
        return false; // got nothing, throw error
411
    }
412
413 26
    protected function isDirective($dirname, $directives)
414
    {
415
        // TODO: cache pattern in parser
416 26
        $pattern = implode("|",
417 26
            array_map(['\LesserPhp\Compiler', "pregQuote"], $directives));
418 26
        $pattern = '/^(-[a-z-]+-)?(' . $pattern . ')$/i';
419
420 26
        return preg_match($pattern, $dirname);
421
    }
422
423
    /**
424
     * @param array $tags
425
     *
426
     * @return mixed
427
     */
428 46
    protected function fixTags(array $tags)
429
    {
430
        // move @ tags out of variable namespace
431 46
        foreach ($tags as &$tag) {
432 46
            if ($tag[0] === $this->lessc->getVPrefix()) {
433 46
                $tag[0] = $this->lessc->getMPrefix();
434
            }
435
        }
436
437 46
        return $tags;
438
    }
439
440
    // a list of expressions
441 48
    protected function expressionList(&$exps)
442
    {
443 48
        $values = [];
444
445 48
        while ($this->expression($exp)) {
446 48
            $values[] = $exp;
447
        }
448
449 48
        if (count($values) === 0) {
450 26
            return false;
451
        }
452
453 48
        $exps = \LesserPhp\Compiler::compressList($values, ' ');
454
455 48
        return true;
456
    }
457
458
    /**
459
     * Attempt to consume an expression.
460
     * @link http://en.wikipedia.org/wiki/Operator-precedence_parser#Pseudo-code
461
     */
462 48
    protected function expression(&$out)
463
    {
464 48
        if ($this->value($lhs)) {
465 48
            $out = $this->expHelper($lhs, 0);
466
467
            // look for / shorthand
468 48
            if (!empty($this->env->supressedDivision)) {
469 2
                unset($this->env->supressedDivision);
470 2
                $s = $this->seek();
471 2
                if ($this->literal("/") && $this->value($rhs)) {
472
                    $out = [
473 2
                        "list",
474 2
                        "",
475 2
                        [$out, ["keyword", "/"], $rhs],
476
                    ];
477
                } else {
478
                    $this->seek($s);
479
                }
480
            }
481
482 48
            return true;
483
        }
484
485 48
        return false;
486
    }
487
488
    /**
489
     * recursively parse infix equation with $lhs at precedence $minP
490
     */
491 48
    protected function expHelper($lhs, $minP)
492
    {
493 48
        $this->inExp = true;
494 48
        $ss = $this->seek();
495
496 48
        while (true) {
497 48
            $whiteBefore = isset($this->buffer[$this->count - 1]) &&
498 48
                ctype_space($this->buffer[$this->count - 1]);
499
500
            // If there is whitespace before the operator, then we require
501
            // whitespace after the operator for it to be an expression
502 48
            $needWhite = $whiteBefore && !$this->inParens;
503
504 48
            if ($this->match(self::$operatorString . ($needWhite ? '\s' : ''),
505 48
                    $m) && self::$precedence[$m[1]] >= $minP
506
            ) {
507 20
                if (!$this->inParens &&
508 20
                    isset($this->env->currentProperty) &&
509 20
                    $m[1] === "/" &&
510 20
                    empty($this->env->supressedDivision)
511
                ) {
512 5
                    foreach (self::$supressDivisionProps as $pattern) {
513 5
                        if (preg_match($pattern, $this->env->currentProperty)) {
514 2
                            $this->env->supressedDivision = true;
515 5
                            break 2;
516
                        }
517
                    }
518
                }
519
520
521 20
                $whiteAfter = isset($this->buffer[$this->count - 1]) &&
522 20
                    ctype_space($this->buffer[$this->count - 1]);
523
524 20
                if (!$this->value($rhs)) {
525
                    break;
526
                }
527
528
                // peek for next operator to see what to do with rhs
529 20
                if ($this->peek(self::$operatorString,
530 20
                        $next) && self::$precedence[$next[1]] > self::$precedence[$m[1]]
531
                ) {
532 1
                    $rhs = $this->expHelper($rhs, self::$precedence[$next[1]]);
533
                }
534
535 20
                $lhs = ['expression', $m[1], $lhs, $rhs, $whiteBefore, $whiteAfter];
536 20
                $ss = $this->seek();
537
538 20
                continue;
539
            }
540
541 48
            break;
542
        }
543
544 48
        $this->seek($ss);
545
546 48
        return $lhs;
547
    }
548
549
    // consume a list of values for a property
550 48
    public function propertyValue(&$value, $keyName = null)
551
    {
552 48
        $values = [];
553
554 48
        if ($keyName !== null) {
555 47
            $this->env->currentProperty = $keyName;
556
        }
557
558 48
        $s = null;
559 48
        while ($this->expressionList($v)) {
560 48
            $values[] = $v;
561 48
            $s = $this->seek();
562 48
            if (!$this->literal(',')) {
563 48
                break;
564
            }
565
        }
566
567 48
        if ($s) {
568 48
            $this->seek($s);
569
        }
570
571 48
        if ($keyName !== null) {
572 47
            unset($this->env->currentProperty);
573
        }
574
575 48
        if (count($values) === 0) {
576 3
            return false;
577
        }
578
579 48
        $value = \LesserPhp\Compiler::compressList($values, ', ');
580
581 48
        return true;
582
    }
583
584 48
    protected function parenValue(&$out)
585
    {
586 48
        $s = $this->seek();
587
588
        // speed shortcut
589 48 View Code Duplication
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== "(") {
590 48
            return false;
591
        }
592
593 5
        $inParens = $this->inParens;
594 5
        if ($this->literal("(") &&
595 5
            ($this->inParens = true) && $this->expression($exp) &&
596 5
            $this->literal(")")
597
        ) {
598 3
            $out = $exp;
599 3
            $this->inParens = $inParens;
600
601 3
            return true;
602
        } else {
603 2
            $this->inParens = $inParens;
604 2
            $this->seek($s);
605
        }
606
607 2
        return false;
608
    }
609
610
    // a single value
611 48
    protected function value(&$value)
612
    {
613 48
        $s = $this->seek();
614
615
        // speed shortcut
616 48
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] === "-") {
617
            // negation
618 7
            if ($this->literal("-", false) &&
619 7
                (($this->variable($inner) && $inner = ["variable", $inner]) ||
620 6
                    $this->unit($inner) ||
621 7
                    $this->parenValue($inner))
622
            ) {
623 6
                $value = ["unary", "-", $inner];
624
625 6
                return true;
626
            } else {
627 2
                $this->seek($s);
628
            }
629
        }
630
631 48
        if ($this->parenValue($value)) {
632 3
            return true;
633
        }
634 48
        if ($this->unit($value)) {
635 38
            return true;
636
        }
637 48
        if ($this->color($value)) {
638 13
            return true;
639
        }
640 48
        if ($this->func($value)) {
641 25
            return true;
642
        }
643 48
        if ($this->stringValue($value)) {
644 21
            return true;
645
        }
646
647 48
        if ($this->keyword($word)) {
648 35
            $value = ['keyword', $word];
649
650 35
            return true;
651
        }
652
653
        // try a variable
654 48
        if ($this->variable($var)) {
655 29
            $value = ['variable', $var];
656
657 29
            return true;
658
        }
659
660
        // unquote string (should this work on any type?
661 48
        if ($this->literal("~") && $this->stringValue($str)) {
662 4
            $value = ["escape", $str];
663
664 4
            return true;
665
        } else {
666 48
            $this->seek($s);
667
        }
668
669
        // css hack: \0
670 48
        if ($this->literal('\\') && $this->match('([0-9]+)', $m)) {
671 1
            $value = ['keyword', '\\' . $m[1]];
672
673 1
            return true;
674
        } else {
675 48
            $this->seek($s);
676
        }
677
678 48
        return false;
679
    }
680
681
    // an import statement
682 49
    protected function import(&$out)
683
    {
684 49
        if (!$this->literal('@import')) {
685 49
            return false;
686
        }
687
688
        // @import "something.css" media;
689
        // @import url("something.css") media;
0 ignored issues
show
Unused Code Comprehensibility introduced by
40% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
690
        // @import url(something.css) media;
691
692 3
        if ($this->propertyValue($value)) {
693 3
            $out = ["import", $value];
694
695 3
            return true;
696
        }
697
698
        return null;
699
    }
700
701 3
    protected function mediaQueryList(&$out)
702
    {
703 3
        if ($this->genericList($list, "mediaQuery", ",", false)) {
704 3
            $out = $list[2];
705
706 3
            return true;
707
        }
708
709
        return false;
710
    }
711
712 3
    protected function mediaQuery(&$out)
713
    {
714 3
        $s = $this->seek();
715
716 3
        $expressions = null;
717 3
        $parts = [];
718
719 3
        if (($this->literal("only") && ($only = true) || $this->literal("not") && ($not = true) || true) &&
720 3
            $this->keyword($mediaType)) {
721 3
            $prop = ["mediaType"];
722 3
            if (isset($only)) {
723
                $prop[] = "only";
724
            }
725 3
            if (isset($not)) {
726
                $prop[] = "not";
727
            }
728 3
            $prop[] = $mediaType;
729 3
            $parts[] = $prop;
730
        } else {
731 1
            $this->seek($s);
732
        }
733
734
735 3
        if (!empty($mediaType) && !$this->literal("and")) {
0 ignored issues
show
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...
736
            // ~
737
        } else {
738 1
            $this->genericList($expressions, "mediaExpression", "and", false);
739 1
            if (is_array($expressions)) {
740 1
                $parts = array_merge($parts, $expressions[2]);
741
            }
742
        }
743
744 3
        if (count($parts) === 0) {
745
            $this->seek($s);
746
747
            return false;
748
        }
749
750 3
        $out = $parts;
751
752 3
        return true;
753
    }
754
755 1
    protected function mediaExpression(&$out)
756
    {
757 1
        $s = $this->seek();
758 1
        $value = null;
759 1
        if ($this->literal("(") &&
760 1
            $this->keyword($feature) &&
761 1
            ($this->literal(":") && $this->expression($value) || true) &&
762 1
            $this->literal(")")
763
        ) {
764 1
            $out = ["mediaExp", $feature];
765 1
            if ($value) {
766 1
                $out[] = $value;
767
            }
768
769 1
            return true;
770 1
        } elseif ($this->variable($variable)) {
771 1
            $out = ['variable', $variable];
772
773 1
            return true;
774
        }
775
776
        $this->seek($s);
777
778
        return false;
779
    }
780
781
    // an unbounded string stopped by $end
782 26
    protected function openString($end, &$out, $nestingOpen = null, $rejectStrs = null)
783
    {
784 26
        $oldWhite = $this->eatWhiteDefault;
785 26
        $this->eatWhiteDefault = false;
786
787 26
        $stop = ["'", '"', "@{", $end];
788 26
        $stop = array_map(['\LesserPhp\Compiler', "pregQuote"], $stop);
789
        // $stop[] = self::$commentMulti;
0 ignored issues
show
Unused Code Comprehensibility introduced by
64% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

Loading history...
790
791 26
        if (!is_null($rejectStrs)) {
792 25
            $stop = array_merge($stop, $rejectStrs);
793
        }
794
795 26
        $patt = '(.*?)(' . implode("|", $stop) . ')';
796
797 26
        $nestingLevel = 0;
798
799 26
        $content = [];
800 26
        while ($this->match($patt, $m, false)) {
801 26
            if (!empty($m[1])) {
802 25
                $content[] = $m[1];
803 25
                if ($nestingOpen) {
804
                    $nestingLevel += substr_count($m[1], $nestingOpen);
805
                }
806
            }
807
808 26
            $tok = $m[2];
809
810 26
            $this->count -= strlen($tok);
811 26
            if ($tok == $end) {
812 14
                if ($nestingLevel === 0) {
813 14
                    break;
814
                } else {
815
                    $nestingLevel--;
816
                }
817
            }
818
819 24
            if (($tok === "'" || $tok === '"') && $this->stringValue($str)) {
820 13
                $content[] = $str;
821 13
                continue;
822
            }
823
824 23
            if ($tok === "@{" && $this->interpolation($inter)) {
825 3
                $content[] = $inter;
826 3
                continue;
827
            }
828
829 23
            if (!empty($rejectStrs) && in_array($tok, $rejectStrs)) {
830 23
                break;
831
            }
832
833
            $content[] = $tok;
834
            $this->count += strlen($tok);
835
        }
836
837 26
        $this->eatWhiteDefault = $oldWhite;
838
839 26
        if (count($content) === 0) {
840 4
            return false;
841
        }
842
843
        // trim the end
844 25
        if (is_string(end($content))) {
845 25
            $content[count($content) - 1] = rtrim(end($content));
846
        }
847
848 25
        $out = ["string", "", $content];
849
850 25
        return true;
851
    }
852
853 48
    protected function stringValue(&$out)
854
    {
855 48
        $s = $this->seek();
856 48
        if ($this->literal('"', false)) {
857 22
            $delim = '"';
858 48
        } elseif ($this->literal("'", false)) {
859 12
            $delim = "'";
860
        } else {
861 48
            return false;
862
        }
863
864 24
        $content = [];
865
866
        // look for either ending delim , escape, or string interpolation
867
        $patt = '([^\n]*?)(@\{|\\\\|' .
868 24
            \LesserPhp\Compiler::pregQuote($delim) . ')';
869
870 24
        $oldWhite = $this->eatWhiteDefault;
871 24
        $this->eatWhiteDefault = false;
872
873 24
        while ($this->match($patt, $m, false)) {
874 24
            $content[] = $m[1];
875 24
            if ($m[2] === "@{") {
876 6
                $this->count -= strlen($m[2]);
877 6
                if ($this->interpolation($inter)) {
878 6
                    $content[] = $inter;
879
                } else {
880 1
                    $this->count += strlen($m[2]);
881 6
                    $content[] = "@{"; // ignore it
882
                }
883 24
            } elseif ($m[2] === '\\') {
884 2
                $content[] = $m[2];
885 2
                if ($this->literal($delim, false)) {
886 2
                    $content[] = $delim;
887
                }
888
            } else {
889 24
                $this->count -= strlen($delim);
890 24
                break; // delim
891
            }
892
        }
893
894 24
        $this->eatWhiteDefault = $oldWhite;
895
896 24
        if ($this->literal($delim)) {
897 24
            $out = ["string", $delim, $content];
898
899 24
            return true;
900
        }
901
902 1
        $this->seek($s);
903
904 1
        return false;
905
    }
906
907 19
    protected function interpolation(&$out)
908
    {
909 19
        $oldWhite = $this->eatWhiteDefault;
910 19
        $this->eatWhiteDefault = true;
911
912 19
        $s = $this->seek();
913 19
        if ($this->literal("@{") &&
914 19
            $this->openString("}", $interp, null, ["'", '"', ";"]) &&
915 19
            $this->literal("}", false)
916
        ) {
917 7
            $out = ["interpolate", $interp];
918 7
            $this->eatWhiteDefault = $oldWhite;
919 7
            if ($this->eatWhiteDefault) {
920 1
                $this->whitespace();
921
            }
922
923 7
            return true;
924
        }
925
926 16
        $this->eatWhiteDefault = $oldWhite;
927 16
        $this->seek($s);
928
929 16
        return false;
930
    }
931
932 49
    protected function unit(&$unit)
933
    {
934
        // speed shortcut
935 49
        if (isset($this->buffer[$this->count])) {
936 49
            $char = $this->buffer[$this->count];
937 49
            if (!ctype_digit($char) && $char !== ".") {
938 49
                return false;
939
            }
940
        }
941
942 49
        if ($this->match('([0-9]+(?:\.[0-9]*)?|\.[0-9]+)([%a-zA-Z]+)?', $m)) {
943 38
            $unit = ["number", $m[1], empty($m[2]) ? "" : $m[2]];
944
945 38
            return true;
946
        }
947
948 49
        return false;
949
    }
950
951
    // a # color
952 48
    protected function color(&$out)
953
    {
954 48
        if ($this->match('(#(?:[0-9a-f]{8}|[0-9a-f]{6}|[0-9a-f]{3}))', $m)) {
955 13
            if (strlen($m[1]) > 7) {
956 1
                $out = ["string", "", [$m[1]]];
957
            } else {
958 13
                $out = ["raw_color", $m[1]];
959
            }
960
961 13
            return true;
962
        }
963
964 48
        return false;
965
    }
966
967
    // consume an argument definition list surrounded by ()
968
    // each argument is a variable name with optional value
969
    // or at the end a ... or a variable named followed by ...
970
    // arguments are separated by , unless a ; is in the list, then ; is the
971
    // delimiter.
972 45
    protected function argumentDef(&$args, &$isVararg)
973
    {
974 45
        $s = $this->seek();
975 45
        if (!$this->literal('(')) {
976 45
            return false;
977
        }
978
979 19
        $values = [];
980 19
        $delim = ",";
981 19
        $method = "expressionList";
982
983 19
        $isVararg = false;
984 19
        while (true) {
985 19
            if ($this->literal("...")) {
986 2
                $isVararg = true;
987 2
                break;
988
            }
989
990 19
            if ($this->$method($value)) {
0 ignored issues
show
The variable $value does not exist. Did you mean $values?

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

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

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

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

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

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

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

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

Loading history...
993 16
                    $ss = $this->seek();
994
995 16
                    if ($this->assign() && $this->$method($rhs)) {
996 9
                        $arg[] = $rhs;
997
                    } else {
998 13
                        $this->seek($ss);
999 13
                        if ($this->literal("...")) {
1000 2
                            $arg[0] = "rest";
1001 2
                            $isVararg = true;
1002
                        }
1003
                    }
1004
1005 16
                    $values[] = $arg;
1006 16
                    if ($isVararg) {
1007 2
                        break;
1008
                    }
1009 16
                    continue;
1010
                } else {
1011 15
                    $values[] = ["lit", $value];
0 ignored issues
show
The variable $value does not exist. Did you mean $values?

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

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

Loading history...
1012
                }
1013
            }
1014
1015
1016 19
            if (!$this->literal($delim)) {
1017 19
                if ($delim === "," && $this->literal(";")) {
1018
                    // found new delim, convert existing args
1019 2
                    $delim = ";";
1020 2
                    $method = "propertyValue";
1021 2
                    $newArg = null;
1022
1023
                    // transform arg list
1024 2
                    if (isset($values[1])) { // 2 items
1025 2
                        $newList = [];
1026 2
                        foreach ($values as $i => $arg) {
1027 2
                            switch ($arg[0]) {
1028 2
                                case "arg":
1029 2
                                    if ($i) {
1030
                                        throw new GeneralException("Cannot mix ; and , as delimiter types");
1031
                                    }
1032 2
                                    $newList[] = $arg[2];
1033 2
                                    break;
1034 2
                                case "lit":
1035 2
                                    $newList[] = $arg[1];
1036 2
                                    break;
1037
                                case "rest":
1038 2
                                    throw new GeneralException("Unexpected rest before semicolon");
1039
                            }
1040
                        }
1041
1042 2
                        $newList = ["list", ", ", $newList];
1043
1044 2
                        switch ($values[0][0]) {
1045 2
                            case "arg":
1046 2
                                $newArg = ["arg", $values[0][1], $newList];
1047 2
                                break;
1048 1
                            case "lit":
1049 1
                                $newArg = ["lit", $newList];
1050 2
                                break;
1051
                        }
1052
1053 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...
1054 2
                        $newArg = $values[0];
1055
                    }
1056
1057 2
                    if ($newArg !== null) {
1058 2
                        $values = [$newArg];
1059
                    }
1060
                } else {
1061 19
                    break;
1062
                }
1063
            }
1064
        }
1065
1066 19
        if (!$this->literal(')')) {
1067
            $this->seek($s);
1068
1069
            return false;
1070
        }
1071
1072 19
        $args = $values;
1073
1074 19
        return true;
1075
    }
1076
1077
    // consume a list of tags
1078
    // this accepts a hanging delimiter
1079 49 View Code Duplication
    protected function tags(&$tags, $simple = false, $delim = ',')
1080
    {
1081 49
        $tags = [];
1082 49
        while ($this->tag($tt, $simple)) {
1083 46
            $tags[] = $tt;
1084 46
            if (!$this->literal($delim)) {
1085 46
                break;
1086
            }
1087
        }
1088 49
        return count($tags) !== 0;
1089
    }
1090
1091
    // list of tags of specifying mixin path
1092
    // optionally separated by > (lazy, accepts extra >)
1093 49 View Code Duplication
    protected function mixinTags(&$tags)
1094
    {
1095 49
        $tags = [];
1096 49
        while ($this->tag($tt, true)) {
1097 22
            $tags[] = $tt;
1098 22
            $this->literal(">");
1099
        }
1100
1101 49
        return count($tags) !== 0;
1102
    }
1103
1104
    // a bracketed value (contained within in a tag definition)
1105 49
    protected function tagBracket(&$parts, &$hasExpression)
1106
    {
1107
        // speed shortcut
1108 49 View Code Duplication
        if (isset($this->buffer[$this->count]) && $this->buffer[$this->count] !== "[") {
1109 47
            return false;
1110
        }
1111
1112 49
        $s = $this->seek();
1113
1114 49
        $hasInterpolation = false;
1115
1116 49
        if ($this->literal("[", false)) {
1117 3
            $attrParts = ["["];
1118
            // keyword, string, operator
1119 3
            while (true) {
1120 3
                if ($this->literal("]", false)) {
1121 3
                    $this->count--;
1122 3
                    break; // get out early
1123
                }
1124
1125 3
                if ($this->match('\s+', $m)) {
1126
                    $attrParts[] = " ";
1127
                    continue;
1128
                }
1129 3
                if ($this->stringValue($str)) {
1130
                    // escape parent selector, (yuck)
0 ignored issues
show
Unused Code Comprehensibility introduced by
37% of this comment could be valid code. Did you maybe forget this after debugging?

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

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

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

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